diff options
Diffstat (limited to 'wp-includes/js/dist/block-serialization-default-parser.js')
-rw-r--r-- | wp-includes/js/dist/block-serialization-default-parser.js | 485 |
1 files changed, 485 insertions, 0 deletions
diff --git a/wp-includes/js/dist/block-serialization-default-parser.js b/wp-includes/js/dist/block-serialization-default-parser.js new file mode 100644 index 0000000..a59495e --- /dev/null +++ b/wp-includes/js/dist/block-serialization-default-parser.js @@ -0,0 +1,485 @@ +/******/ (function() { // webpackBootstrap +/******/ "use strict"; +/******/ // The require scope +/******/ var __webpack_require__ = {}; +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ !function() { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = function(exports, definition) { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ }(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ !function() { +/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } +/******/ }(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ !function() { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ }(); +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ parse: function() { return /* binding */ parse; } +/* harmony export */ }); +/** + * @type {string} + */ +let document; +/** + * @type {number} + */ +let offset; +/** + * @type {ParsedBlock[]} + */ +let output; +/** + * @type {ParsedFrame[]} + */ +let stack; + +/** + * @typedef {Object|null} Attributes + */ + +/** + * @typedef {Object} ParsedBlock + * @property {string|null} blockName Block name. + * @property {Attributes} attrs Block attributes. + * @property {ParsedBlock[]} innerBlocks Inner blocks. + * @property {string} innerHTML Inner HTML. + * @property {Array<string|null>} innerContent Inner content. + */ + +/** + * @typedef {Object} ParsedFrame + * @property {ParsedBlock} block Block. + * @property {number} tokenStart Token start. + * @property {number} tokenLength Token length. + * @property {number} prevOffset Previous offset. + * @property {number|null} leadingHtmlStart Leading HTML start. + */ + +/** + * @typedef {'no-more-tokens'|'void-block'|'block-opener'|'block-closer'} TokenType + */ + +/** + * @typedef {[TokenType, string, Attributes, number, number]} Token + */ + +/** + * Matches block comment delimiters + * + * While most of this pattern is straightforward the attribute parsing + * incorporates a tricks to make sure we don't choke on specific input + * + * - since JavaScript has no possessive quantifier or atomic grouping + * we are emulating it with a trick + * + * we want a possessive quantifier or atomic group to prevent backtracking + * on the `}`s should we fail to match the remainder of the pattern + * + * we can emulate this with a positive lookahead and back reference + * (a++)*c === ((?=(a+))\1)*c + * + * let's examine an example: + * - /(a+)*c/.test('aaaaaaaaaaaaad') fails after over 49,000 steps + * - /(a++)*c/.test('aaaaaaaaaaaaad') fails after 85 steps + * - /(?>a+)*c/.test('aaaaaaaaaaaaad') fails after 126 steps + * + * this is because the possessive `++` and the atomic group `(?>)` + * tell the engine that all those `a`s belong together as a single group + * and so it won't split it up when stepping backwards to try and match + * + * if we use /((?=(a+))\1)*c/ then we get the same behavior as the atomic group + * or possessive and prevent the backtracking because the `a+` is matched but + * not captured. thus, we find the long string of `a`s and remember it, then + * reference it as a whole unit inside our pattern + * + * @see http://instanceof.me/post/52245507631/regex-emulate-atomic-grouping-with-lookahead + * @see http://blog.stevenlevithan.com/archives/mimic-atomic-groups + * @see https://javascript.info/regexp-infinite-backtracking-problem + * + * once browsers reliably support atomic grouping or possessive + * quantifiers natively we should remove this trick and simplify + * + * @type {RegExp} + * + * @since 3.8.0 + * @since 4.6.1 added optimization to prevent backtracking on attribute parsing + */ +const tokenizer = /<!--\s+(\/)?wp:([a-z][a-z0-9_-]*\/)?([a-z][a-z0-9_-]*)\s+({(?:(?=([^}]+|}+(?=})|(?!}\s+\/?-->)[^])*)\5|[^]*?)}\s+)?(\/)?-->/g; + +/** + * Constructs a block object. + * + * @param {string|null} blockName + * @param {Attributes} attrs + * @param {ParsedBlock[]} innerBlocks + * @param {string} innerHTML + * @param {string[]} innerContent + * @return {ParsedBlock} The block object. + */ +function Block(blockName, attrs, innerBlocks, innerHTML, innerContent) { + return { + blockName, + attrs, + innerBlocks, + innerHTML, + innerContent + }; +} + +/** + * Constructs a freeform block object. + * + * @param {string} innerHTML + * @return {ParsedBlock} The freeform block object. + */ +function Freeform(innerHTML) { + return Block(null, {}, [], innerHTML, [innerHTML]); +} + +/** + * Constructs a frame object. + * + * @param {ParsedBlock} block + * @param {number} tokenStart + * @param {number} tokenLength + * @param {number} prevOffset + * @param {number|null} leadingHtmlStart + * @return {ParsedFrame} The frame object. + */ +function Frame(block, tokenStart, tokenLength, prevOffset, leadingHtmlStart) { + return { + block, + tokenStart, + tokenLength, + prevOffset: prevOffset || tokenStart + tokenLength, + leadingHtmlStart + }; +} + +/** + * Parser function, that converts input HTML into a block based structure. + * + * @param {string} doc The HTML document to parse. + * + * @example + * Input post: + * ```html + * <!-- wp:columns {"columns":3} --> + * <div class="wp-block-columns has-3-columns"><!-- wp:column --> + * <div class="wp-block-column"><!-- wp:paragraph --> + * <p>Left</p> + * <!-- /wp:paragraph --></div> + * <!-- /wp:column --> + * + * <!-- wp:column --> + * <div class="wp-block-column"><!-- wp:paragraph --> + * <p><strong>Middle</strong></p> + * <!-- /wp:paragraph --></div> + * <!-- /wp:column --> + * + * <!-- wp:column --> + * <div class="wp-block-column"></div> + * <!-- /wp:column --></div> + * <!-- /wp:columns --> + * ``` + * + * Parsing code: + * ```js + * import { parse } from '@wordpress/block-serialization-default-parser'; + * + * parse( post ) === [ + * { + * blockName: "core/columns", + * attrs: { + * columns: 3 + * }, + * innerBlocks: [ + * { + * blockName: "core/column", + * attrs: null, + * innerBlocks: [ + * { + * blockName: "core/paragraph", + * attrs: null, + * innerBlocks: [], + * innerHTML: "\n<p>Left</p>\n" + * } + * ], + * innerHTML: '\n<div class="wp-block-column"></div>\n' + * }, + * { + * blockName: "core/column", + * attrs: null, + * innerBlocks: [ + * { + * blockName: "core/paragraph", + * attrs: null, + * innerBlocks: [], + * innerHTML: "\n<p><strong>Middle</strong></p>\n" + * } + * ], + * innerHTML: '\n<div class="wp-block-column"></div>\n' + * }, + * { + * blockName: "core/column", + * attrs: null, + * innerBlocks: [], + * innerHTML: '\n<div class="wp-block-column"></div>\n' + * } + * ], + * innerHTML: '\n<div class="wp-block-columns has-3-columns">\n\n\n\n</div>\n' + * } + * ]; + * ``` + * @return {ParsedBlock[]} A block-based representation of the input HTML. + */ +const parse = doc => { + document = doc; + offset = 0; + output = []; + stack = []; + tokenizer.lastIndex = 0; + do { + // twiddle our thumbs + } while (proceed()); + return output; +}; + +/** + * Parses the next token in the input document. + * + * @return {boolean} Returns true when there is more tokens to parse. + */ +function proceed() { + const stackDepth = stack.length; + const next = nextToken(); + const [tokenType, blockName, attrs, startOffset, tokenLength] = next; + + // We may have some HTML soup before the next block. + const leadingHtmlStart = startOffset > offset ? offset : null; + switch (tokenType) { + case 'no-more-tokens': + // If not in a block then flush output. + if (0 === stackDepth) { + addFreeform(); + return false; + } + + // Otherwise we have a problem + // This is an error + // we have options + // - treat it all as freeform text + // - assume an implicit closer (easiest when not nesting) + + // For the easy case we'll assume an implicit closer. + if (1 === stackDepth) { + addBlockFromStack(); + return false; + } + + // For the nested case where it's more difficult we'll + // have to assume that multiple closers are missing + // and so we'll collapse the whole stack piecewise. + while (0 < stack.length) { + addBlockFromStack(); + } + return false; + case 'void-block': + // easy case is if we stumbled upon a void block + // in the top-level of the document. + if (0 === stackDepth) { + if (null !== leadingHtmlStart) { + output.push(Freeform(document.substr(leadingHtmlStart, startOffset - leadingHtmlStart))); + } + output.push(Block(blockName, attrs, [], '', [])); + offset = startOffset + tokenLength; + return true; + } + + // Otherwise we found an inner block. + addInnerBlock(Block(blockName, attrs, [], '', []), startOffset, tokenLength); + offset = startOffset + tokenLength; + return true; + case 'block-opener': + // Track all newly-opened blocks on the stack. + stack.push(Frame(Block(blockName, attrs, [], '', []), startOffset, tokenLength, startOffset + tokenLength, leadingHtmlStart)); + offset = startOffset + tokenLength; + return true; + case 'block-closer': + // If we're missing an opener we're in trouble + // This is an error. + if (0 === stackDepth) { + // We have options + // - assume an implicit opener + // - assume _this_ is the opener + // - give up and close out the document. + addFreeform(); + return false; + } + + // If we're not nesting then this is easy - close the block. + if (1 === stackDepth) { + addBlockFromStack(startOffset); + offset = startOffset + tokenLength; + return true; + } + + // Otherwise we're nested and we have to close out the current + // block and add it as a innerBlock to the parent. + const stackTop = /** @type {ParsedFrame} */stack.pop(); + const html = document.substr(stackTop.prevOffset, startOffset - stackTop.prevOffset); + stackTop.block.innerHTML += html; + stackTop.block.innerContent.push(html); + stackTop.prevOffset = startOffset + tokenLength; + addInnerBlock(stackTop.block, stackTop.tokenStart, stackTop.tokenLength, startOffset + tokenLength); + offset = startOffset + tokenLength; + return true; + default: + // This is an error. + addFreeform(); + return false; + } +} + +/** + * Parse JSON if valid, otherwise return null + * + * Note that JSON coming from the block comment + * delimiters is constrained to be an object + * and cannot be things like `true` or `null` + * + * @param {string} input JSON input string to parse + * @return {Object|null} parsed JSON if valid + */ +function parseJSON(input) { + try { + return JSON.parse(input); + } catch (e) { + return null; + } +} + +/** + * Finds the next token in the document. + * + * @return {Token} The next matched token. + */ +function nextToken() { + // Aye the magic + // we're using a single RegExp to tokenize the block comment delimiters + // we're also using a trick here because the only difference between a + // block opener and a block closer is the leading `/` before `wp:` (and + // a closer has no attributes). we can trap them both and process the + // match back in JavaScript to see which one it was. + const matches = tokenizer.exec(document); + + // We have no more tokens. + if (null === matches) { + return ['no-more-tokens', '', null, 0, 0]; + } + const startedAt = matches.index; + const [match, closerMatch, namespaceMatch, nameMatch, attrsMatch /* Internal/unused. */,, voidMatch] = matches; + const length = match.length; + const isCloser = !!closerMatch; + const isVoid = !!voidMatch; + const namespace = namespaceMatch || 'core/'; + const name = namespace + nameMatch; + const hasAttrs = !!attrsMatch; + const attrs = hasAttrs ? parseJSON(attrsMatch) : {}; + + // This state isn't allowed + // This is an error. + if (isCloser && (isVoid || hasAttrs)) { + // We can ignore them since they don't hurt anything + // we may warn against this at some point or reject it. + } + if (isVoid) { + return ['void-block', name, attrs, startedAt, length]; + } + if (isCloser) { + return ['block-closer', name, null, startedAt, length]; + } + return ['block-opener', name, attrs, startedAt, length]; +} + +/** + * Adds a freeform block to the output. + * + * @param {number} [rawLength] + */ +function addFreeform(rawLength) { + const length = rawLength ? rawLength : document.length - offset; + if (0 === length) { + return; + } + output.push(Freeform(document.substr(offset, length))); +} + +/** + * Adds inner block to the parent block. + * + * @param {ParsedBlock} block + * @param {number} tokenStart + * @param {number} tokenLength + * @param {number} [lastOffset] + */ +function addInnerBlock(block, tokenStart, tokenLength, lastOffset) { + const parent = stack[stack.length - 1]; + parent.block.innerBlocks.push(block); + const html = document.substr(parent.prevOffset, tokenStart - parent.prevOffset); + if (html) { + parent.block.innerHTML += html; + parent.block.innerContent.push(html); + } + parent.block.innerContent.push(null); + parent.prevOffset = lastOffset ? lastOffset : tokenStart + tokenLength; +} + +/** + * Adds block from the stack to the output. + * + * @param {number} [endOffset] + */ +function addBlockFromStack(endOffset) { + const { + block, + leadingHtmlStart, + prevOffset, + tokenStart + } = /** @type {ParsedFrame} */stack.pop(); + const html = endOffset ? document.substr(prevOffset, endOffset - prevOffset) : document.substr(prevOffset); + if (html) { + block.innerHTML += html; + block.innerContent.push(html); + } + if (null !== leadingHtmlStart) { + output.push(Freeform(document.substr(leadingHtmlStart, tokenStart - leadingHtmlStart))); + } + output.push(block); +} + +(window.wp = window.wp || {}).blockSerializationDefaultParser = __webpack_exports__; +/******/ })() +;
\ No newline at end of file |