riot
Version:
Simple and elegant component-based UI library
1,814 lines (1,634 loc) • 988 kB
JavaScript
/* Riot v10.1.0, @license MIT */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.riot = {}));
})(this, (function (exports) { 'use strict';
// Riot.js constants that can be used across more modules
const COMPONENTS_IMPLEMENTATION_MAP = new Map(),
DOM_COMPONENT_INSTANCE_PROPERTY = Symbol('riot-component'),
PLUGINS_SET = new Set(),
IS_DIRECTIVE = 'is',
VALUE_ATTRIBUTE = 'value',
REF_ATTRIBUTE = 'ref',
EVENT_ATTRIBUTE_RE = /^on/,
MOUNT_METHOD_KEY = 'mount',
UPDATE_METHOD_KEY = 'update',
UNMOUNT_METHOD_KEY = 'unmount',
SHOULD_UPDATE_KEY = 'shouldUpdate',
ON_BEFORE_MOUNT_KEY = 'onBeforeMount',
ON_MOUNTED_KEY = 'onMounted',
ON_BEFORE_UPDATE_KEY = 'onBeforeUpdate',
ON_UPDATED_KEY = 'onUpdated',
ON_BEFORE_UNMOUNT_KEY = 'onBeforeUnmount',
ON_UNMOUNTED_KEY = 'onUnmounted',
PROPS_KEY = 'props',
STATE_KEY = 'state',
SLOTS_KEY = 'slots',
ROOT_KEY = 'root',
IS_PURE_SYMBOL = Symbol('pure'),
IS_COMPONENT_UPDATING = Symbol('is_updating'),
PARENT_KEY_SYMBOL = Symbol('parent'),
TEMPLATE_KEY_SYMBOL = Symbol('template'),
ROOT_ATTRIBUTES_KEY_SYMBOL = Symbol('root-attributes');
/**
* Quick type checking
* @param {*} element - anything
* @param {string} type - type definition
* @returns {boolean} true if the type corresponds
*/
function checkType(element, type) {
return typeof element === type
}
/**
* Check if an element is part of an svg
* @param {HTMLElement} el - element to check
* @returns {boolean} true if we are in an svg context
*/
function isSvg(el) {
const owner = el.ownerSVGElement;
return !!owner || owner === null
}
/**
* Check if an element is a template tag
* @param {HTMLElement} el - element to check
* @returns {boolean} true if it's a <template>
*/
function isTemplate(el) {
return el.tagName.toLowerCase() === 'template'
}
/**
* Check that will be passed if its argument is a function
* @param {*} value - value to check
* @returns {boolean} - true if the value is a function
*/
function isFunction(value) {
return checkType(value, 'function')
}
/**
* Check if a value is a Boolean
* @param {*} value - anything
* @returns {boolean} true only for the value is a boolean
*/
function isBoolean(value) {
return checkType(value, 'boolean')
}
/**
* Check if a value is an Object
* @param {*} value - anything
* @returns {boolean} true only for the value is an object
*/
function isObject(value) {
return !isNil(value) && value.constructor === Object
}
/**
* Check if a value is null or undefined
* @param {*} value - anything
* @returns {boolean} true only for the 'undefined' and 'null' types
*/
function isNil(value) {
return value === null || value === undefined
}
/**
* Detect node js environment
* @returns {boolean} true if the runtime is node
*/
function isNode() {
return typeof globalThis.process !== 'undefined'
}
/**
* Check if an attribute is a DOM handler
* @param {string} attribute - attribute string
* @returns {boolean} true only for dom listener attribute nodes
*/
function isEventAttribute$1(attribute) {
return EVENT_ATTRIBUTE_RE.test(attribute)
}
const ATTRIBUTE = 0;
const EVENT = 1;
const TEXT$1 = 2;
const VALUE = 3;
const REF = 4;
const expressionTypes = {
ATTRIBUTE,
EVENT,
TEXT: TEXT$1,
VALUE,
REF,
};
/**
* Convert a string from camel case to dash-case
* @param {string} string - probably a component tag name
* @returns {string} component name normalized
*/
function camelToDashCase(string) {
return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
}
/**
* Convert a string containing dashes to camel case
* @param {string} string - input string
* @returns {string} my-string -> myString
*/
function dashToCamelCase(string) {
return string.replace(/-(\w)/g, (_, c) => c.toUpperCase())
}
/**
* Throw an error with a descriptive message
* @param { string } message - error message
* @param { string } cause - optional error cause object
* @returns { undefined } hoppla... at this point the program should stop working
*/
function panic$1(message, cause) {
throw new Error(message, { cause })
}
/**
* Returns the memoized (cached) function.
* // borrowed from https://www.30secondsofcode.org/js/s/memoize
* @param {Function} fn - function to memoize
* @returns {Function} memoize function
*/
function memoize$1(fn) {
const cache = new Map();
const cached = (val) => {
return cache.has(val)
? cache.get(val)
: cache.set(val, fn.call(this, val)) && cache.get(val)
};
cached.cache = cache;
return cached
}
/**
* Generate key-value pairs from a list of attributes
* @param {Array} attributes - list of attributes generated by the riot compiler, each containing type, name, and evaluate function
* @param {Object} scope - the scope in which the attribute values will be evaluated
* @returns {Object} An object containing key-value pairs representing the computed attribute values
*/
function generatePropsFromAttributes(attributes, scope) {
return attributes.reduce((acc, { type, name, evaluate }) => {
const value = evaluate(scope);
switch (true) {
// spread attribute
case !name && type === ATTRIBUTE:
return {
...acc,
...value,
}
// ref attribute
case type === REF:
acc.ref = value;
break
// value attribute
case type === VALUE:
acc.value = value;
break
// normal attributes
default:
acc[dashToCamelCase(name)] = value;
}
return acc
}, {})
}
const EACH = 0;
const IF = 1;
const SIMPLE = 2;
const TAG$1 = 3;
const SLOT = 4;
const bindingTypes = {
EACH,
IF,
SIMPLE,
TAG: TAG$1,
SLOT,
};
/**
* Get all the element attributes as object
* @param {HTMLElement} element - DOM node we want to parse
* @returns {Object} all the attributes found as a key value pairs
*/
function DOMattributesToObject(element) {
return Array.from(element.attributes).reduce((acc, attribute) => {
acc[dashToCamelCase(attribute.name)] = attribute.value;
return acc
}, {})
}
/**
* Move all the child nodes from a source tag to another
* @param {HTMLElement} source - source node
* @param {HTMLElement} target - target node
* @returns {undefined} it's a void method ¯\_(ツ)_/¯
*/
// Ignore this helper because it's needed only for svg tags
function moveChildren(source, target) {
// eslint-disable-next-line fp/no-loops
while (source.firstChild) target.appendChild(source.firstChild);
}
/**
* Remove the child nodes from any DOM node
* @param {HTMLElement} node - target node
* @returns {undefined}
*/
function cleanNode(node) {
// eslint-disable-next-line fp/no-loops
while (node.firstChild) node.removeChild(node.firstChild);
}
/**
* Clear multiple children in a node
* @param {HTMLElement[]} children - direct children nodes
* @returns {undefined}
*/
function clearChildren(children) {
// eslint-disable-next-line fp/no-loops,fp/no-let
for (let i = 0; i < children.length; i++) removeChild(children[i]);
}
/**
* Remove a node
* @param {HTMLElement}node - node to remove
* @returns {undefined}
*/
const removeChild = (node) => node.remove();
/**
* Insert before a node
* @param {HTMLElement} newNode - node to insert
* @param {HTMLElement} refNode - ref child
* @returns {undefined}
*/
const insertBefore = (newNode, refNode) =>
refNode &&
refNode.parentNode &&
refNode.parentNode.insertBefore(newNode, refNode);
/**
* Replace a node
* @param {HTMLElement} newNode - new node to add to the DOM
* @param {HTMLElement} replaced - node to replace
* @returns {undefined}
*/
const replaceChild = (newNode, replaced) =>
replaced &&
replaced.parentNode &&
replaced.parentNode.replaceChild(newNode, replaced);
// does simply nothing
function noop$1() {
return this
}
/**
* Autobind the methods of a source object to itself
* @param {Object} source - probably a riot tag instance
* @param {Array<string>} methods - list of the methods to autobind
* @returns {Object} the original object received
*/
function autobindMethods(source, methods) {
methods.forEach((method) => {
source[method] = source[method].bind(source);
});
return source
}
/**
* Call the first argument received only if it's a function otherwise return it as it is
* @param {*} source - anything
* @returns {*} anything
*/
function callOrAssign(source) {
return isFunction(source)
? source.prototype && source.prototype.constructor
? new source()
: source()
: source
}
/**
* Helper function to set an immutable property
* @param {Object} source - object where the new property will be set
* @param {string} key - object key where the new property will be stored
* @param {*} value - value of the new property
* @param {Object} options - set the property overriding the default options
* @returns {Object} - the original object modified
*/
function defineProperty(source, key, value, options = {}) {
/* eslint-disable fp/no-mutating-methods */
Object.defineProperty(source, key, {
value,
enumerable: false,
writable: false,
configurable: true,
...options,
});
/* eslint-enable fp/no-mutating-methods */
return source
}
/**
* Define multiple properties on a target object
* @param {Object} source - object where the new properties will be set
* @param {Object} properties - object containing as key pair the key + value properties
* @param {Object} options - set the property overriding the default options
* @returns {Object} the original object modified
*/
function defineProperties(source, properties, options) {
Object.entries(properties).forEach(([key, value]) => {
defineProperty(source, key, value, options);
});
return source
}
/**
* Define default properties if they don't exist on the source object
* @param {Object} source - object that will receive the default properties
* @param {Object} defaults - object containing additional optional keys
* @returns {Object} the original object received enhanced
*/
function defineDefaults(source, defaults) {
Object.entries(defaults).forEach(([key, value]) => {
if (!source[key]) source[key] = value;
});
return source
}
/* Riot Compiler, @license MIT */
const TAG_LOGIC_PROPERTY = 'exports';
const TAG_CSS_PROPERTY = 'css';
const TAG_TEMPLATE_PROPERTY = 'template';
const TAG_NAME_PROPERTY = 'name';
const RIOT_MODULE_ID = 'riot';
const RIOT_INTERFACE_WRAPPER_NAME = 'RiotComponentWrapper';
const RIOT_TAG_INTERFACE_NAME = 'RiotComponent';
const JAVASCRIPT_OUTPUT_NAME = 'javascript';
const CSS_OUTPUT_NAME = 'css';
const TEMPLATE_OUTPUT_NAME = 'template';
// Tag names
const JAVASCRIPT_TAG = 'script';
const STYLE_TAG = 'style';
const TEXTAREA_TAG = 'textarea';
// Boolean attributes
const IS_RAW = 'isRaw';
const IS_SELF_CLOSING = 'isSelfClosing';
const IS_VOID = 'isVoid';
const IS_BOOLEAN = 'isBoolean';
const IS_CUSTOM = 'isCustom';
const IS_SPREAD = 'isSpread';
var c = /*#__PURE__*/Object.freeze({
__proto__: null,
CSS_OUTPUT_NAME: CSS_OUTPUT_NAME,
IS_BOOLEAN: IS_BOOLEAN,
IS_CUSTOM: IS_CUSTOM,
IS_RAW: IS_RAW,
IS_SELF_CLOSING: IS_SELF_CLOSING,
IS_SPREAD: IS_SPREAD,
IS_VOID: IS_VOID,
JAVASCRIPT_OUTPUT_NAME: JAVASCRIPT_OUTPUT_NAME,
JAVASCRIPT_TAG: JAVASCRIPT_TAG,
STYLE_TAG: STYLE_TAG,
TEMPLATE_OUTPUT_NAME: TEMPLATE_OUTPUT_NAME,
TEXTAREA_TAG: TEXTAREA_TAG
});
/**
* Not all the types are handled in this module.
* @enum {number}
* @readonly
*/
const TAG = 1; /* TAG */
const ATTR = 2; /* ATTR */
const TEXT = 3; /* TEXT */
const CDATA = 4; /* CDATA */
const COMMENT = 8; /* COMMENT */
const DOCUMENT = 9; /* DOCUMENT */
const DOCTYPE = 10; /* DOCTYPE */
const DOCUMENT_FRAGMENT = 11; /* DOCUMENT_FRAGMENT */
var types$2 = /*#__PURE__*/Object.freeze({
__proto__: null,
ATTR: ATTR,
CDATA: CDATA,
COMMENT: COMMENT,
DOCTYPE: DOCTYPE,
DOCUMENT: DOCUMENT,
DOCUMENT_FRAGMENT: DOCUMENT_FRAGMENT,
TAG: TAG,
TEXT: TEXT
});
const rootTagNotFound = 'Root tag not found.';
const unclosedTemplateLiteral = 'Unclosed ES6 template literal.';
const unexpectedEndOfFile = 'Unexpected end of file.';
const unclosedComment = 'Unclosed comment.';
const unclosedNamedBlock = 'Unclosed "%1" block.';
const duplicatedNamedTag =
'Multiple inline "<%1>" tags are not supported.';
const unexpectedCharInExpression = 'Unexpected character %1.';
const unclosedExpression = 'Unclosed expression.';
/**
* Matches the start of valid tags names; used with the first 2 chars after the `'<'`.
* @constant
* @private
*/
const TAG_2C = /^(?:\/[a-zA-Z]|[a-zA-Z][^\s>/]?)/;
/**
* Matches valid tags names AFTER the validation with `TAG_2C`.
* $1: tag name including any `'/'`, $2: non self-closing brace (`>`) w/o attributes.
* @constant
* @private
*/
const TAG_NAME = /(\/?[^\s>/]+)\s*(>)?/g;
/**
* Matches an attribute name-value pair (both can be empty).
* $1: attribute name, $2: value including any quotes.
* @constant
* @private
*/
const ATTR_START = /(\S[^>/=\s]*)(?:\s*=\s*([^>/])?)?/g;
/**
* Matches the spread operator
* it will be used for the spread attributes
* @type {RegExp}
*/
const SPREAD_OPERATOR = /\.\.\./;
/**
* Matches the closing tag of a `script` and `style` block.
* Used by parseText fo find the end of the block.
* @constant
* @private
*/
const RE_SCRYLE = {
script: /<\/script\s*>/gi,
style: /<\/style\s*>/gi,
textarea: /<\/textarea\s*>/gi,
};
// Do not touch text content inside this tags
const RAW_TAGS = /^\/?(?:pre|textarea)$/;
/**
* Add an item into a collection, if the collection is not an array
* we create one and add the item to it
* @param {Array} collection - target collection
* @param {*} item - item to add to the collection
* @returns {Array} array containing the new item added to it
*/
function addToCollection(collection = [], item) {
collection.push(item);
return collection
}
/**
* Run RegExp.exec starting from a specific position
* @param {RegExp} re - regex
* @param {number} pos - last index position
* @param {string} string - regex target
* @returns {Array} regex result
*/
function execFromPos(re, pos, string) {
re.lastIndex = pos;
return re.exec(string)
}
/**
* Escape special characters in a given string, in preparation to create a regex.
* @param {string} str - Raw string
* @returns {string} Escaped string.
*/
var escapeStr = (str) => str.replace(/(?=[-[\](){^*+?.$|\\])/g, '\\');
function formatError(data, message, pos) {
if (!pos) {
pos = data.length;
}
// count unix/mac/win eols
const line = (data.slice(0, pos).match(/\r\n?|\n/g) || '').length + 1;
let col = 0;
while (--pos >= 0 && !/[\r\n]/.test(data[pos])) {
++col;
}
return `[${line},${col}]: ${message}`
}
const $_ES6_BQ = '`';
/**
* Searches the next backquote that signals the end of the ES6 Template Literal
* or the "${" sequence that starts a JS expression, skipping any escaped
* character.
* @param {string} code - Whole code
* @param {number} pos - The start position of the template
* @param {string[]} stack - To save nested ES6 TL count
* @returns {number} The end of the string (-1 if not found)
*/
function skipES6TL(code, pos, stack) {
// we are in the char following the backquote (`),
// find the next unescaped backquote or the sequence "${"
const re = /[`$\\]/g;
let c;
while (((re.lastIndex = pos), re.exec(code))) {
pos = re.lastIndex;
c = code[pos - 1];
if (c === '`') {
return pos
}
if (c === '$' && code[pos++] === '{') {
stack.push($_ES6_BQ, '}');
return pos
}
// else this is an escaped char
}
throw formatError(code, unclosedTemplateLiteral, pos)
}
/**
* Custom error handler can be implemented replacing this method.
* The `state` object includes the buffer (`data`)
* The error position (`loc`) contains line (base 1) and col (base 0).
* @param {string} data - string containing the error
* @param {string} msg - Error message
* @param {number} pos - Position of the error
* @returns {undefined} throw an exception error
*/
function panic(data, msg, pos) {
const message = formatError(data, msg, pos);
throw new Error(message)
}
// forked from https://github.com/aMarCruz/skip-regex
// safe characters to precced a regex (including `=>`, `**`, and `...`)
const beforeReChars = '[{(,;:?=|&!^~>%*/';
const beforeReSign = `${beforeReChars}+-`;
// keyword that can preceed a regex (`in` is handled as special case)
const beforeReWords = [
'case',
'default',
'do',
'else',
'in',
'instanceof',
'prefix',
'return',
'typeof',
'void',
'yield',
];
// Last chars of all the beforeReWords elements to speed up the process.
const wordsEndChar = beforeReWords.reduce((s, w) => s + w.slice(-1), '');
// Matches literal regex from the start of the buffer.
// The buffer to search must not include line-endings.
const RE_LIT_REGEX =
/^\/(?=[^*>/])[^[/\\]*(?:(?:\\.|\[(?:\\.|[^\]\\]*)*\])[^[\\/]*)*?\/[gimuy]*/;
// Valid characters for JavaScript variable names and literal numbers.
const RE_JS_VCHAR = /[$\w]/;
// Match dot characters that could be part of tricky regex
const RE_DOT_CHAR = /.*/g;
/**
* Searches the position of the previous non-blank character inside `code`,
* starting with `pos - 1`.
* @param {string} code - Buffer to search
* @param {number} pos - Starting position
* @returns {number} Position of the first non-blank character to the left.
* @private
*/
function _prev(code, pos) {
while (--pos >= 0 && /\s/.test(code[pos]));
return pos
}
/**
* Check if the character in the `start` position within `code` can be a regex
* and returns the position following this regex or `start+1` if this is not
* one.
*
* NOTE: Ensure `start` points to a slash (this is not checked).
* @function skipRegex
* @param {string} code - Buffer to test in
* @param {number} start - Position the first slash inside `code`
* @returns {number} Position of the char following the regex.
*/
/* c8 ignore next */
function skipRegex(code, start) {
let pos = (RE_DOT_CHAR.lastIndex = start++);
// `exec()` will extract from the slash to the end of the line
// and the chained `match()` will match the possible regex.
const match = (RE_DOT_CHAR.exec(code) || ' ')[0].match(RE_LIT_REGEX);
if (match) {
const next = pos + match[0].length; // result comes from `re.match`
pos = _prev(code, pos);
let c = code[pos];
// start of buffer or safe prefix?
if (pos < 0 || beforeReChars.includes(c)) {
return next
}
// from here, `pos` is >= 0 and `c` is code[pos]
if (c === '.') {
// can be `...` or something silly like 5./2
if (code[pos - 1] === '.') {
start = next;
}
} else {
if (c === '+' || c === '-') {
// tricky case
if (
code[--pos] !== c || // if have a single operator or
(pos = _prev(code, pos)) < 0 || // ...have `++` and no previous token
beforeReSign.includes((c = code[pos]))
) {
return next // ...this is a regex
}
}
if (wordsEndChar.includes(c)) {
// looks like a keyword?
const end = pos + 1;
// get the complete (previous) keyword
while (--pos >= 0 && RE_JS_VCHAR.test(code[pos]));
// it is in the allowed keywords list?
if (beforeReWords.includes(code.slice(pos + 1, end))) {
start = next;
}
}
}
}
return start
}
/*
* Mini-parser for expressions.
* The main pourpose of this module is to find the end of an expression
* and return its text without the enclosing brackets.
* Does not works with comments, but supports ES6 template strings.
*/
/**
* @exports exprExtr
*/
const S_SQ_STR = /'[^'\n\r\\]*(?:\\(?:\r\n?|[\S\s])[^'\n\r\\]*)*'/.source;
/**
* Matches double quoted JS strings taking care about nested quotes
* and EOLs (escaped EOLs are Ok).
* @constant
* @private
*/
const S_STRING = `${S_SQ_STR}|${S_SQ_STR.replace(/'/g, '"')}`;
/**
* Regex cache
* @type {{[key:string]: RegExp}}
* @constant
* @private
*/
const reBr = {};
/**
* Makes an optimal regex that matches quoted strings, brackets, backquotes
* and the closing brackets of an expression.
* @param {string} b - Closing brackets
* @returns {RegExp} - optimized regex
*/
function _regex(b) {
let re = reBr[b];
if (!re) {
let s = escapeStr(b);
if (b.length > 1) {
s = `${s}|[`;
} else {
s = /[{}[\]()]/.test(b) ? '[' : `[${s}`;
}
reBr[b] = re = new RegExp(`${S_STRING}|${s}\`/\\{}[\\]()]`, 'g');
}
return re
}
/**
* Update the scopes stack removing or adding closures to it
* @param {Array} stack - array stacking the expression closures
* @param {string} char - current char to add or remove from the stack
* @param {string} idx - matching index
* @param {string} code - expression code
* @returns {{char: string, index: number}} An object with properties:
* char: either the char received or the closing braces
* index: either a new index to skip part of the source code, or 0 to keep from parsing from the old position
*/
function updateStack(stack, char, idx, code) {
let index = 0;
switch (char) {
case '[':
case '(':
case '{':
stack.push(char === '[' ? ']' : char === '(' ? ')' : '}');
break
case ')':
case ']':
case '}':
if (char !== stack.pop()) {
panic(code, unexpectedCharInExpression.replace('%1', char), index);
}
if (char === '}' && stack[stack.length - 1] === $_ES6_BQ) {
char = stack.pop();
}
index = idx + 1;
break
case '/':
index = skipRegex(code, idx);
}
return { char, index }
}
/**
* Parses the code string searching the end of the expression.
* It skips braces, quoted strings, regexes, and ES6 template literals.
* @function exprExtr
* @param {string} code - Buffer to parse
* @param {number} start - Position of the opening brace
* @param {[string,string]} bp - Brackets pair
* @returns {object|undefined} Expression's end (after the closing brace) or -1
* if it is not an expr.
*/
function exprExtr(code, start, bp) {
const [openingBraces, closingBraces] = bp;
const offset = start + openingBraces.length; // skips the opening brace
const stack = []; // expected closing braces ('`' for ES6 TL)
const re = _regex(closingBraces);
re.lastIndex = offset; // begining of the expression
let end;
let match;
while ((match = re.exec(code))) {
const idx = match.index;
const str = match[0];
end = re.lastIndex;
// end the iteration
if (str === closingBraces && !stack.length) {
return {
text: code.slice(offset, idx),
start,
end,
}
}
const { char, index } = updateStack(stack, str[0], idx, code);
// update the end value depending on the new index received
end = index || end;
// update the regex last index
re.lastIndex = char === $_ES6_BQ ? skipES6TL(code, end, stack) : end;
}
if (stack.length) {
panic(code, unclosedExpression, end);
}
}
/**
* Outputs the last parsed node. Can be used with a builder too.
* @param {import("../..").ParserState} store - Parsing store
* @returns {undefined} void function
* @private
*/
function flush(store) {
const last = store.last;
store.last = null;
if (last && store.root) {
store.builder.push(last);
}
}
/**
* Get the code chunks from start and end range
* @param {string} source - source code
* @param {number} start - Start position of the chunk we want to extract
* @param {number} end - Ending position of the chunk we need
* @returns {string} chunk of code extracted from the source code received
* @private
*/
function getChunk(source, start, end) {
return source.slice(start, end)
}
/**
* states text in the last text node, or creates a new one if needed.
* @param {import('../..').ParserState} state - Current parser state
* @param {number} start - Start position of the tag
* @param {number} end - Ending position (last char of the tag)
* @param {import('../..').ExpressionContainer} extra - extra properties to add to the text node
* @param {import('../..').Expression[]} extra.expressions - Found expressions
* @param {string} extra.unescape - Brackets to unescape
* @returns {undefined} - void function
* @private
*/
function pushText(state, start, end, extra = {}) {
const text = getChunk(state.data, start, end);
const expressions = extra.expressions;
const unescape = extra.unescape;
let q = state.last;
state.pos = end;
if (q && q.type === TEXT) {
q.text += text;
q.end = end;
} else {
flush(state);
state.last = q = { type: TEXT, text, start, end };
}
if (expressions && expressions.length) {
q.expressions = (q.expressions || []).concat(expressions);
}
if (unescape) {
q.unescape = unescape;
}
return TEXT
}
/**
* Find the end of the attribute value or text node
* Extract expressions.
* Detect if value have escaped brackets.
* @param {import('../..').ParserState} state - Parser state
* @param {import('../..').ExpressionContainer} node - Node if attr, info if text
* @param {string} endingChars - Ends the value or text
* @param {number} start - Starting position
* @returns {number} Ending position
* @private
*/
function expr(state, node, endingChars, start) {
const re = b0re(state, endingChars);
re.lastIndex = start; // reset re position
const { unescape, expressions, end } = parseExpressions(state, re);
if (node) {
if (unescape) {
node.unescape = unescape;
}
if (expressions.length) {
node.expressions = expressions;
}
} else {
pushText(state, start, end, { expressions, unescape });
}
return end
}
/**
* Parse a text chunk finding all the expressions in it
* @param {import('../..').ParserState} state - Parser state
* @param {RegExp} re - regex to match the expressions contents
* @returns {object} result containing the expression found, the string to unescape and the end position
*/
function parseExpressions(state, re) {
const { data, options } = state;
const { brackets } = options;
const expressions = [];
let unescape, pos, match;
// Anything captured in $1 (closing quote or character) ends the loop...
while ((match = re.exec(data)) && !match[1]) {
// ...else, we have an opening bracket and maybe an expression.
pos = match.index;
if (data[pos - 1] === '\\') {
unescape = match[0]; // it is an escaped opening brace
} else {
const tmpExpr = exprExtr(data, pos, brackets);
if (tmpExpr) {
expressions.push(tmpExpr);
re.lastIndex = tmpExpr.end;
}
}
}
// Even for text, the parser needs match a closing char
if (!match) {
panic(data, unexpectedEndOfFile, pos);
}
return {
unescape,
expressions,
end: match.index,
}
}
/**
* Creates a regex for the given string and the left bracket.
* The string is captured in $1.
* @param {import('../..').ParserState} state - Parser state
* @param {string} str - String to search
* @returns {RegExp} Resulting regex.
* @private
*/
function b0re(state, str) {
const { brackets } = state.options;
const re = state.regexCache[str];
if (re) return re
const b0 = escapeStr(brackets[0]);
// cache the regex extending the regexCache object
Object.assign(state.regexCache, { [str]: new RegExp(`(${str})|${b0}`, 'g') });
return state.regexCache[str]
}
// similar to _.uniq
const uniq$1 = l => l.filter((x, i, a) => a.indexOf(x) === i);
/**
* SVG void elements that cannot be auto-closed and shouldn't contain child nodes.
* @const {Array}
*/
const VOID_SVG_TAGS_LIST$1 = [
'circle',
'ellipse',
'line',
'path',
'polygon',
'polyline',
'rect',
'stop',
'use'
];
/**
* List of html elements where the value attribute is allowed
* @type {Array}
*/
const HTML_ELEMENTS_HAVING_VALUE_ATTRIBUTE_LIST$1 = [
'button',
'data',
'input',
'select',
'li',
'meter',
'option',
'output',
'progress',
'textarea',
'param'
];
/**
* List of all the available svg tags
* @const {Array}
* @see {@link https://github.com/wooorm/svg-tag-names}
*/
const SVG_TAGS_LIST$1 = uniq$1([
'a',
'altGlyph',
'altGlyphDef',
'altGlyphItem',
'animate',
'animateColor',
'animateMotion',
'animateTransform',
'animation',
'audio',
'canvas',
'clipPath',
'color-profile',
'cursor',
'defs',
'desc',
'discard',
'feBlend',
'feColorMatrix',
'feComponentTransfer',
'feComposite',
'feConvolveMatrix',
'feDiffuseLighting',
'feDisplacementMap',
'feDistantLight',
'feDropShadow',
'feFlood',
'feFuncA',
'feFuncB',
'feFuncG',
'feFuncR',
'feGaussianBlur',
'feImage',
'feMerge',
'feMergeNode',
'feMorphology',
'feOffset',
'fePointLight',
'feSpecularLighting',
'feSpotLight',
'feTile',
'feTurbulence',
'filter',
'font',
'font-face',
'font-face-format',
'font-face-name',
'font-face-src',
'font-face-uri',
'foreignObject',
'g',
'glyph',
'glyphRef',
'handler',
'hatch',
'hatchpath',
'hkern',
'iframe',
'image',
'linearGradient',
'listener',
'marker',
'mask',
'mesh',
'meshgradient',
'meshpatch',
'meshrow',
'metadata',
'missing-glyph',
'mpath',
'pattern',
'prefetch',
'radialGradient',
'script',
'set',
'solidColor',
'solidcolor',
'style',
'svg',
'switch',
'symbol',
'tbreak',
'text',
'textArea',
'textPath',
'title',
'tref',
'tspan',
'unknown',
'video',
'view',
'vkern'
].concat(VOID_SVG_TAGS_LIST$1)).sort();
/**
* HTML void elements that cannot be auto-closed and shouldn't contain child nodes.
* @type {Array}
* @see {@link http://www.w3.org/TR/html-markup/syntax.html#syntax-elements}
* @see {@link http://www.w3.org/TR/html5/syntax.html#void-elements}
*/
const VOID_HTML_TAGS_LIST$1 = [
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'menuitem',
'meta',
'param',
'source',
'track',
'wbr'
];
/**
* List of all the html tags
* @const {Array}
* @see {@link https://github.com/sindresorhus/html-tags}
*/
const HTML_TAGS_LIST$1 = uniq$1([
'a',
'abbr',
'address',
'article',
'aside',
'audio',
'b',
'bdi',
'bdo',
'blockquote',
'body',
'canvas',
'caption',
'cite',
'code',
'colgroup',
'datalist',
'dd',
'del',
'details',
'dfn',
'dialog',
'div',
'dl',
'dt',
'em',
'fieldset',
'figcaption',
'figure',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'head',
'header',
'hgroup',
'html',
'i',
'iframe',
'ins',
'kbd',
'label',
'legend',
'main',
'map',
'mark',
'math',
'menu',
'nav',
'noscript',
'object',
'ol',
'optgroup',
'p',
'picture',
'pre',
'q',
'rb',
'rp',
'rt',
'rtc',
'ruby',
's',
'samp',
'script',
'section',
'select',
'slot',
'small',
'span',
'strong',
'style',
'sub',
'summary',
'sup',
'svg',
'table',
'tbody',
'td',
'template',
'tfoot',
'th',
'thead',
'time',
'title',
'tr',
'u',
'ul',
'var',
'video'
]
.concat(VOID_HTML_TAGS_LIST$1)
.concat(HTML_ELEMENTS_HAVING_VALUE_ATTRIBUTE_LIST$1)
).sort();
/**
* List of all boolean HTML attributes
* @const {RegExp}
* @see {@link https://www.w3.org/TR/html5/infrastructure.html#sec-boolean-attributes}
*/
const BOOLEAN_ATTRIBUTES_LIST$1 = [
'disabled',
'visible',
'checked',
'readonly',
'required',
'allowfullscreen',
'autofocus',
'autoplay',
'compact',
'controls',
'default',
'formnovalidate',
'hidden',
'ismap',
'itemscope',
'loop',
'multiple',
'muted',
'noresize',
'noshade',
'novalidate',
'nowrap',
'open',
'reversed',
'seamless',
'selected',
'sortable',
'truespeed',
'typemustmatch'
];
/**
* Join a list of items with the pipe symbol (usefull for regex list concatenation)
* @private
* @param {Array} list - list of strings
* @returns {string} the list received joined with pipes
*/
function joinWithPipe$1(list) {
return list.join('|')
}
/**
* Convert list of strings to regex in order to test against it ignoring the cases
* @private
* @param {...Array} lists - array of strings
* @returns {RegExp} regex that will match all the strings in the array received ignoring the cases
*/
function listsToRegex$1(...lists) {
return new RegExp(`^/?(?:${joinWithPipe$1(lists.map(joinWithPipe$1))})$`, 'i')
}
/**
* Regex matching all the html tags ignoring the cases
* @const {RegExp}
*/
const HTML_TAGS_RE = listsToRegex$1(HTML_TAGS_LIST$1);
/**
* Regex matching all the svg tags ignoring the cases
* @const {RegExp}
*/
const SVG_TAGS_RE = listsToRegex$1(SVG_TAGS_LIST$1);
/**
* Regex matching all the void html tags ignoring the cases
* @const {RegExp}
*/
const VOID_HTML_TAGS_RE = listsToRegex$1(VOID_HTML_TAGS_LIST$1);
/**
* Regex matching all the void svg tags ignoring the cases
* @const {RegExp}
*/
const VOID_SVG_TAGS_RE = listsToRegex$1(VOID_SVG_TAGS_LIST$1);
/**
* Regex matching all the html tags where the value tag is allowed
* @const {RegExp}
*/
listsToRegex$1(HTML_ELEMENTS_HAVING_VALUE_ATTRIBUTE_LIST$1);
/**
* Regex matching all the boolean attributes
* @const {RegExp}
*/
const BOOLEAN_ATTRIBUTES_RE = listsToRegex$1(BOOLEAN_ATTRIBUTES_LIST$1);
/**
* True if it's a self closing tag
* @param {string} tag - test tag
* @returns {boolean} true if void
* @example
* isVoid('meta') // true
* isVoid('circle') // true
* isVoid('IMG') // true
* isVoid('div') // false
* isVoid('mask') // false
*/
function isVoid(tag) {
return [
VOID_HTML_TAGS_RE,
VOID_SVG_TAGS_RE
].some(r => r.test(tag))
}
/**
* True if it's not SVG nor a HTML known tag
* @param {string} tag - test tag
* @returns {boolean} true if custom element
* @example
* isCustom('my-component') // true
* isCustom('div') // false
*/
function isCustom(tag) {
return [
HTML_TAGS_RE,
SVG_TAGS_RE
].every(l => !l.test(tag))
}
/**
* True if it's a boolean attribute
* @param {string} attribute - test attribute
* @returns {boolean} true if the attribute is a boolean type
* @example
* isBoolAttribute('selected') // true
* isBoolAttribute('class') // false
*/
function isBoolAttribute(attribute) {
return BOOLEAN_ATTRIBUTES_RE.test(attribute)
}
/**
* Memoization function
* @param {Function} fn - function to memoize
* @returns {*} return of the function to memoize
*/
function memoize(fn) {
const cache = new WeakMap();
return (...args) => {
if (cache.has(args[0])) return cache.get(args[0])
const ret = fn(...args);
cache.set(args[0], ret);
return ret
}
}
const expressionsContentRe = memoize((brackets) =>
RegExp(`(${brackets[0]}[^${brackets[1]}]*?${brackets[1]})`, 'g'),
);
const isSpreadAttribute$1 = (name) => SPREAD_OPERATOR.test(name);
const isAttributeExpression = (name, brackets) => name[0] === brackets[0];
const getAttributeEnd = (state, attr) =>
expr(state, attr, '[>/\\s]', attr.start);
/**
* The more complex parsing is for attributes as it can contain quoted or
* unquoted values or expressions.
* @param {import('../..').ParserState} state - Parser state
* @returns {number} New parser mode.
* @private
*/
function attr(state) {
const { data, last, pos, root } = state;
const tag = last; // the last (current) tag in the output
const _CH = /\S/g; // matches the first non-space char
const ch = execFromPos(_CH, pos, data);
switch (true) {
case !ch:
state.pos = data.length; // reaching the end of the buffer with
// NodeTypes.ATTR will generate error
break
case ch[0] === '>':
// closing char found. If this is a self-closing tag with the name of the
// Root tag, we need decrement the counter as we are changing mode.
state.pos = tag.end = _CH.lastIndex;
if (tag[IS_SELF_CLOSING]) {
state.scryle = null; // allow selfClosing script/style tags
if (root && root.name === tag.name) {
state.count--; // "pop" root tag
}
}
return TEXT
case ch[0] === '/':
state.pos = _CH.lastIndex; // maybe. delegate the validation
tag[IS_SELF_CLOSING] = true; // the next loop
break
default:
delete tag[IS_SELF_CLOSING]; // ensure unmark as selfclosing tag
setAttribute(state, ch.index, tag);
}
return ATTR
}
/**
* Parses an attribute and its expressions.
* @param {import('../..').ParserState} state - Parser state
* @param {number} pos - Starting position of the attribute
* @param {object} tag - Current parent tag
* @returns {undefined} void function
* @private
*/
function setAttribute(state, pos, tag) {
const { data } = state;
const expressionContent = expressionsContentRe(state.options.brackets);
const re = ATTR_START; // (\S[^>/=\s]*)(?:\s*=\s*([^>/])?)? g
const start = (re.lastIndex = expressionContent.lastIndex = pos); // first non-whitespace
const attrMatches = re.exec(data);
const isExpressionName = isAttributeExpression(
attrMatches[1],
state.options.brackets,
);
const match = isExpressionName
? [null, expressionContent.exec(data)[1], null]
: attrMatches;
if (match) {
const end = re.lastIndex;
const attr = parseAttribute(state, match, start, end, isExpressionName);
//assert(q && q.type === Mode.TAG, 'no previous tag for the attr!')
// Pushes the attribute and shifts the `end` position of the tag (`last`).
state.pos = tag.end = attr.end;
tag.attributes = addToCollection(tag.attributes, attr);
}
}
function parseNomalAttribute(state, attr, quote) {
const { data } = state;
let { end } = attr;
if (isBoolAttribute(attr.name)) {
attr[IS_BOOLEAN] = true;
}
// parse the whole value (if any) and get any expressions on it
if (quote) {
// Usually, the value's first char (`quote`) is a quote and the lastIndex
// (`end`) is the start of the value.
let valueStart = end;
// If it not, this is an unquoted value and we need adjust the start.
if (quote !== '"' && quote !== "'") {
quote = ''; // first char of value is not a quote
valueStart--; // adjust the starting position
}
end = expr(state, attr, quote || '[>/\\s]', valueStart);
// adjust the bounds of the value and save its content
return Object.assign(attr, {
value: getChunk(data, valueStart, end),
valueStart,
end: quote ? ++end : end,
})
}
return attr
}
/**
* Parse expression names <a {href}>
* @param {import('../..').ParserState} state - Parser state
* @param {object} attr - attribute object parsed
* @returns {object} normalized attribute object
*/
function parseSpreadAttribute(state, attr) {
const end = getAttributeEnd(state, attr);
return {
[IS_SPREAD]: true,
start: attr.start,
expressions: attr.expressions.map((expr) =>
Object.assign(expr, {
text: expr.text.replace(SPREAD_OPERATOR, '').trim(),
}),
),
end: end,
}
}
/**
* Parse expression names <a {href}>
* @param {import('../..').ParserState} state - Parser state
* @param {object} attr - attribute object parsed
* @returns {object} normalized attribute object
*/
function parseExpressionNameAttribute(state, attr) {
const end = getAttributeEnd(state, attr);
return {
start: attr.start,
name: attr.expressions[0].text.trim(),
expressions: attr.expressions,
end: end,
}
}
/**
* Parse the attribute values normalising the quotes
* @param {import('../..').ParserState} state - Parser state
* @param {Array} match - results of the attributes regex
* @param {number} start - attribute start position
* @param {number} end - attribute end position
* @param {boolean} isExpressionName - true if the attribute name is an expression
* @returns {object} attribute object
*/
function parseAttribute(state, match, start, end, isExpressionName) {
const attr = {
name: match[1],
value: '',
start,
end,
};
const quote = match[2]; // first letter of value or nothing
switch (true) {
case isSpreadAttribute$1(attr.name):
return parseSpreadAttribute(state, attr)
case isExpressionName === true:
return parseExpressionNameAttribute(state, attr)
default:
return parseNomalAttribute(state, attr, quote)
}
}
/**
* Function to curry any javascript method
* @param {Function} fn - the target function we want to curry
* @param {...[args]} acc - initial arguments
* @returns {Function|*} it will return a function until the target function
* will receive all of its arguments
*/
function curry$1(fn, ...acc) {
return (...args) => {
args = [...acc, ...args];
return args.length < fn.length ?
curry$1(fn, ...args) :
fn(...args)
}
}
/**
* Parses comments in long or short form
* (any DOCTYPE & CDATA blocks are parsed as comments).
* @param {import('../..').ParserState} state - Parser state
* @param {string} data - Buffer to parse
* @param {number} start - Position of the '<!' sequence
* @returns {number} node type id
* @private
*/
function comment(state, data, start) {
const pos = start + 2; // skip '<!'
const isLongComment = data.slice(pos, pos + 2) === '--';
const str = isLongComment ? '-->' : '>';
const end = data.indexOf(str, pos);
if (end < 0) {
panic(data, unclosedComment, start);
}
pushComment(
state,
start,
end + str.length,
data.substring(start, end + str.length),
);
return TEXT
}
/**
* Parse a comment.
* @param {import('../..').ParserState} state - Current parser state
* @param {number} start - Start position of the tag
* @param {number} end - Ending position (last char of the tag)
* @param {string} text - Comment content
* @returns {undefined} void function
* @private
*/
function pushComment(state, start, end, text) {
state.pos = end;
if (state.options.comments === true) {
flush(state);
state.last = {
type: COMMENT,
start,
end,
text,
};
}
}
/**
* Pushes a new *tag* and set `last` to this, so any attributes
* will be included on this and shifts the `end`.
* @param {import('../..').ParserState} state - Current parser state
* @param {string} name - Name of the node including any slash
* @param {number} start - Start position of the tag
* @param {number} end - Ending position (last char of the tag + 1)
* @returns {undefined} - void function
* @private
*/
function pushTag(state, name, start, end) {
const root = state.root;
const last = { type: TAG, name, start, end };
if (isCustom(name)) {
last[IS_CUSTOM] = true;
}
if (isVoid(name)) {
last[IS_VOID] = true;
}
state.pos = end;
if (root) {
if (name === root.name) {
state.count++;
} else if (name === root.close) {
state.count--;
}
flush(state);
} else {
// start with root (keep ref to output)
state.root = { name: last.name, close: `/${name}` };
state.count = 1;
}
state.last = last;
}
/**
* Parse the tag following a '<' character, or delegate to other parser
* if an invalid tag name is found.
* @param {import('../..').ParserState} state - Parser state
* @returns {number} New parser mode
* @private
*/
function tag(state) {
const { pos, data } = state; // pos of the char following '<'
const start = pos - 1; // pos of '<'
const str = data.substring(pos, pos + 2); // first two chars following '<'
switch (true) {
case str[0] === '!':
return comment(state, data, start)
case TAG_2C.test(str):
return parseTag(state, start)
default:
return pushText(state, start, pos) // pushes the '<' as text
}
}
function parseTag(state, start) {
const { data, pos } = state;
const re = TAG_NAME; // (\/?[^\s>/]+)\s*(>)? g
const match = execFromPos(re, pos, data);
const end = re.lastIndex;
const name = match[1].toLowerCase(); // $1: tag name including any '/'
// script/style block is parsed as another tag to extract attributes
if (name in RE_SCRYLE) {
state.scryle = name; // used by parseText
}
pushTag(state, name, start, end);
// only '>' can ends the tag here, the '/' is handled in parseAttribute
if (!match[2]) {
return ATTR
}
return TEXT
}
/**
* Parses regular text and script/style blocks ...scryle for short :-)
* (the content of script and style is text as well)
* @param {import('../..').ParserState} state - Parser state
* @returns {number} New parser mode.
* @private
*/
function text(state) {
const { pos, data, scryle } = state;
switch (true) {
case typeof scryle === 'string': {
const name = scryle;
const re = RE_SCRYLE[name];
const match = execFromPos(re, pos, data);
if (!match) {
panic(data, unclosedNamedBlock.replace('%1', name), pos - 1);
}
const start = match.index;
const end = re.lastIndex;
state.scryle = null; // reset the script/style flag now
// write the tag content
if (start > pos) {
parseSpecialTagsContent(state, name, match);
} else if (name !== TEXTAREA_TAG) {
state.last.text = {
type: TEXT,
text: '',
start: pos,
end: pos,
};
}
// now the closing tag, either </script> or </style>
pushTag(state, `/${name}`, start, end);
break
}
case data[pos] === '<':
state.pos++;
return TAG
default:
expr(state, null, '<', pos);
}
return TEXT
}
/**
* Parse the text content depending on the name
* @param {import('../..').ParserState} state - Parser state
* @param {string} name - one of the tags matched by the RE_SCRYLE regex
* @param {Array} match - result of the regex matching the content of the parsed tag
* @returns {undefined} void function
*/
function parseSpecialTagsContent(state, name, match) {
const { pos } = state;
const start = match.index;
if (name === TEXTAREA_TAG) {
expr(state, null, match[0], pos);
} else {
pushText(state, pos, start);
}
}
/*--------------------------------------------------------------------