UNPKG

riot

Version:

Simple and elegant component-based UI library

1,814 lines (1,634 loc) 988 kB
/* 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); } } /*--------------------------------------------------------------------