| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 { 2 "version": 3, 3 "sources": ["../../../packages/block-serialization-default-parser/src/index.ts"], 4 "sourcesContent": ["let document: string;\nlet offset: number;\nlet output: ParsedBlock[];\nlet stack: ParsedFrame[];\n\ntype Attributes = Record< string, any > | null;\n\ntype ParsedBlock = {\n\tblockName: string | null;\n\tattrs: Attributes;\n\tinnerBlocks: ParsedBlock[];\n\tinnerHTML: string;\n\tinnerContent: Array< string | null >;\n};\n\ntype ParsedFrame = {\n\tblock: ParsedBlock;\n\ttokenStart: number;\n\ttokenLength: number;\n\tprevOffset: number;\n\tleadingHtmlStart: number | null;\n};\n\ntype TokenType =\n\t| 'no-more-tokens'\n\t| 'void-block'\n\t| 'block-opener'\n\t| 'block-closer';\n\ntype Token = [ TokenType, string, Attributes, number, number ];\n\n/**\n * Matches block comment delimiters\n *\n * While most of this pattern is straightforward the attribute parsing\n * incorporates a tricks to make sure we don't choke on specific input\n *\n * - since JavaScript has no possessive quantifier or atomic grouping\n * we are emulating it with a trick\n *\n * we want a possessive quantifier or atomic group to prevent backtracking\n * on the `}`s should we fail to match the remainder of the pattern\n *\n * we can emulate this with a positive lookahead and back reference\n * (a++)*c === ((?=(a+))\\1)*c\n *\n * let's examine an example:\n * - /(a+)*c/.test('aaaaaaaaaaaaad') fails after over 49,000 steps\n * - /(a++)*c/.test('aaaaaaaaaaaaad') fails after 85 steps\n * - /(?>a+)*c/.test('aaaaaaaaaaaaad') fails after 126 steps\n *\n * this is because the possessive `++` and the atomic group `(?>)`\n * tell the engine that all those `a`s belong together as a single group\n * and so it won't split it up when stepping backwards to try and match\n *\n * if we use /((?=(a+))\\1)*c/ then we get the same behavior as the atomic group\n * or possessive and prevent the backtracking because the `a+` is matched but\n * not captured. thus, we find the long string of `a`s and remember it, then\n * reference it as a whole unit inside our pattern\n *\n * @see http://instanceof.me/post/52245507631/regex-emulate-atomic-grouping-with-lookahead\n * @see http://blog.stevenlevithan.com/archives/mimic-atomic-groups\n * @see https://javascript.info/regexp-infinite-backtracking-problem\n *\n * once browsers reliably support atomic grouping or possessive\n * quantifiers natively we should remove this trick and simplify\n *\n * @since 3.8.0\n * @since 4.6.1 added optimization to prevent backtracking on attribute parsing\n */\nconst tokenizer =\n\t/<!--\\s+(\\/)?wp:([a-z][a-z0-9_-]*\\/)?([a-z][a-z0-9_-]*)\\s+({(?:(?=([^}]+|}+(?=})|(?!}\\s+\\/?-->)[^])*)\\5|[^]*?)}\\s+)?(\\/)?-->/g;\n\n/**\n * Constructs a block object.\n *\n * @param blockName Either the abbreviated core types, e.g. \"paragraph\", or the fully-qualified\n * block type with namespace and type, e.g. \"core/paragraph\" or \"my-plugin/csv-table\".\n * @param attrs The attributes for the block, or null if there are no attributes.\n * @param innerBlocks An array of inner blocks.\n * @param innerHTML The inner HTML of the block.\n * @param innerContent An array of inner content strings.\n * @return The block object.\n */\nfunction Block(\n\tblockName: string | null,\n\tattrs: Attributes,\n\tinnerBlocks: ParsedBlock[],\n\tinnerHTML: string,\n\tinnerContent: string[]\n): ParsedBlock {\n\treturn {\n\t\tblockName,\n\t\tattrs,\n\t\tinnerBlocks,\n\t\tinnerHTML,\n\t\tinnerContent,\n\t};\n}\n\n/**\n * Constructs a freeform block object.\n *\n * @param innerHTML The inner HTML of the block.\n * @return The freeform block object.\n */\nfunction Freeform( innerHTML: string ): ParsedBlock {\n\treturn Block( null, {}, [], innerHTML, [ innerHTML ] );\n}\n\n/**\n * Constructs a frame object.\n *\n * @param block The block object.\n * @param tokenStart The start offset of the token in the document.\n * @param tokenLength The length of the token in the document.\n * @param prevOffset The offset of the previous token in the document.\n * @param leadingHtmlStart The start offset of leading HTML before the block.\n * @return The frame object.\n */\nfunction Frame(\n\tblock: ParsedBlock,\n\ttokenStart: number,\n\ttokenLength: number,\n\tprevOffset: number | null,\n\tleadingHtmlStart: number | null\n): ParsedFrame {\n\treturn {\n\t\tblock,\n\t\ttokenStart,\n\t\ttokenLength,\n\t\tprevOffset: prevOffset || tokenStart + tokenLength,\n\t\tleadingHtmlStart,\n\t};\n}\n\n/**\n * Parser function, that converts input HTML into a block based structure.\n *\n * @param doc The HTML document to parse.\n *\n * @example\n * Input post:\n * ```html\n * <!-- wp:columns {\"columns\":3} -->\n * <div class=\"wp-block-columns has-3-columns\"><!-- wp:column -->\n * <div class=\"wp-block-column\"><!-- wp:paragraph -->\n * <p>Left</p>\n * <!-- /wp:paragraph --></div>\n * <!-- /wp:column -->\n *\n * <!-- wp:column -->\n * <div class=\"wp-block-column\"><!-- wp:paragraph -->\n * <p><strong>Middle</strong></p>\n * <!-- /wp:paragraph --></div>\n * <!-- /wp:column -->\n *\n * <!-- wp:column -->\n * <div class=\"wp-block-column\"></div>\n * <!-- /wp:column --></div>\n * <!-- /wp:columns -->\n * ```\n *\n * Parsing code:\n * ```js\n * import { parse } from '@wordpress/block-serialization-default-parser';\n *\n * parse( post ) === [\n * {\n * blockName: \"core/columns\",\n * attrs: {\n * columns: 3\n * },\n * innerBlocks: [\n * {\n * blockName: \"core/column\",\n * attrs: null,\n * innerBlocks: [\n * {\n * blockName: \"core/paragraph\",\n * attrs: null,\n * innerBlocks: [],\n * innerHTML: \"\\n<p>Left</p>\\n\"\n * }\n * ],\n * innerHTML: '\\n<div class=\"wp-block-column\"></div>\\n'\n * },\n * {\n * blockName: \"core/column\",\n * attrs: null,\n * innerBlocks: [\n * {\n * blockName: \"core/paragraph\",\n * attrs: null,\n * innerBlocks: [],\n * innerHTML: \"\\n<p><strong>Middle</strong></p>\\n\"\n * }\n * ],\n * innerHTML: '\\n<div class=\"wp-block-column\"></div>\\n'\n * },\n * {\n * blockName: \"core/column\",\n * attrs: null,\n * innerBlocks: [],\n * innerHTML: '\\n<div class=\"wp-block-column\"></div>\\n'\n * }\n * ],\n * innerHTML: '\\n<div class=\"wp-block-columns has-3-columns\">\\n\\n\\n\\n</div>\\n'\n * }\n * ];\n * ```\n * @return A block-based representation of the input HTML.\n */\nexport const parse = ( doc: string ): ParsedBlock[] => {\n\tdocument = doc;\n\toffset = 0;\n\toutput = [];\n\tstack = [];\n\ttokenizer.lastIndex = 0;\n\n\tdo {\n\t\t// twiddle our thumbs\n\t} while ( proceed() );\n\n\treturn output;\n};\n\n/**\n * Parses the next token in the input document.\n *\n * @return Returns true when there is more tokens to parse.\n */\nfunction proceed(): boolean {\n\tconst stackDepth = stack.length;\n\tconst next = nextToken();\n\tconst [ tokenType, blockName, attrs, startOffset, tokenLength ] = next;\n\n\t// We may have some HTML soup before the next block.\n\tconst leadingHtmlStart = startOffset > offset ? offset : null;\n\n\tswitch ( tokenType ) {\n\t\tcase 'no-more-tokens':\n\t\t\t// If not in a block then flush output.\n\t\t\tif ( 0 === stackDepth ) {\n\t\t\t\taddFreeform();\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Otherwise we have a problem\n\t\t\t// This is an error\n\t\t\t// we have options\n\t\t\t// - treat it all as freeform text\n\t\t\t// - assume an implicit closer (easiest when not nesting)\n\n\t\t\t// For the easy case we'll assume an implicit closer.\n\t\t\tif ( 1 === stackDepth ) {\n\t\t\t\taddBlockFromStack();\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// For the nested case where it's more difficult we'll\n\t\t\t// have to assume that multiple closers are missing\n\t\t\t// and so we'll collapse the whole stack piecewise.\n\t\t\twhile ( 0 < stack.length ) {\n\t\t\t\taddBlockFromStack();\n\t\t\t}\n\t\t\treturn false;\n\t\tcase 'void-block':\n\t\t\t// easy case is if we stumbled upon a void block\n\t\t\t// in the top-level of the document.\n\t\t\tif ( 0 === stackDepth ) {\n\t\t\t\tif ( null !== leadingHtmlStart ) {\n\t\t\t\t\toutput.push(\n\t\t\t\t\t\tFreeform(\n\t\t\t\t\t\t\tdocument.substr(\n\t\t\t\t\t\t\t\tleadingHtmlStart,\n\t\t\t\t\t\t\t\tstartOffset - leadingHtmlStart\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\toutput.push( Block( blockName, attrs, [], '', [] ) );\n\t\t\t\toffset = startOffset + tokenLength;\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\t// Otherwise we found an inner block.\n\t\t\taddInnerBlock(\n\t\t\t\tBlock( blockName, attrs, [], '', [] ),\n\t\t\t\tstartOffset,\n\t\t\t\ttokenLength\n\t\t\t);\n\t\t\toffset = startOffset + tokenLength;\n\t\t\treturn true;\n\n\t\tcase 'block-opener':\n\t\t\t// Track all newly-opened blocks on the stack.\n\t\t\tstack.push(\n\t\t\t\tFrame(\n\t\t\t\t\tBlock( blockName, attrs, [], '', [] ),\n\t\t\t\t\tstartOffset,\n\t\t\t\t\ttokenLength,\n\t\t\t\t\tstartOffset + tokenLength,\n\t\t\t\t\tleadingHtmlStart\n\t\t\t\t)\n\t\t\t);\n\t\t\toffset = startOffset + tokenLength;\n\t\t\treturn true;\n\n\t\tcase 'block-closer':\n\t\t\t// If we're missing an opener we're in trouble\n\t\t\t// This is an error.\n\t\t\tif ( 0 === stackDepth ) {\n\t\t\t\t// We have options\n\t\t\t\t// - assume an implicit opener\n\t\t\t\t// - assume _this_ is the opener\n\t\t\t\t// - give up and close out the document.\n\t\t\t\taddFreeform();\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// If we're not nesting then this is easy - close the block.\n\t\t\tif ( 1 === stackDepth ) {\n\t\t\t\taddBlockFromStack( startOffset );\n\t\t\t\toffset = startOffset + tokenLength;\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\t// Otherwise we're nested and we have to close out the current\n\t\t\t// block and add it as a innerBlock to the parent.\n\t\t\tconst stackTop = stack.pop() as ParsedFrame;\n\t\t\tconst html = document.substr(\n\t\t\t\tstackTop.prevOffset,\n\t\t\t\tstartOffset - stackTop.prevOffset\n\t\t\t);\n\t\t\tstackTop.block.innerHTML += html;\n\t\t\tstackTop.block.innerContent.push( html );\n\t\t\tstackTop.prevOffset = startOffset + tokenLength;\n\n\t\t\taddInnerBlock(\n\t\t\t\tstackTop.block,\n\t\t\t\tstackTop.tokenStart,\n\t\t\t\tstackTop.tokenLength,\n\t\t\t\tstartOffset + tokenLength\n\t\t\t);\n\t\t\toffset = startOffset + tokenLength;\n\t\t\treturn true;\n\n\t\tdefault:\n\t\t\t// This is an error.\n\t\t\taddFreeform();\n\t\t\treturn false;\n\t}\n}\n\n/**\n * Parse JSON if valid, otherwise return null\n *\n * Note that JSON coming from the block comment\n * delimiters is constrained to be an object\n * and cannot be things like `true` or `null`\n *\n * @param input JSON input string to parse\n * @return parsed JSON if valid or null if invalid\n */\nfunction parseJSON( input: string ): Object | null {\n\ttry {\n\t\treturn JSON.parse( input );\n\t} catch ( e ) {\n\t\treturn null;\n\t}\n}\n\n/**\n * Finds the next token in the document.\n *\n * @return The next matched token.\n */\nfunction nextToken(): Token {\n\t// Aye the magic\n\t// we're using a single RegExp to tokenize the block comment delimiters\n\t// we're also using a trick here because the only difference between a\n\t// block opener and a block closer is the leading `/` before `wp:` (and\n\t// a closer has no attributes). we can trap them both and process the\n\t// match back in JavaScript to see which one it was.\n\tconst matches = tokenizer.exec( document );\n\n\t// We have no more tokens.\n\tif ( null === matches ) {\n\t\treturn [ 'no-more-tokens', '', null, 0, 0 ];\n\t}\n\n\tconst startedAt = matches.index;\n\tconst [\n\t\tmatch,\n\t\tcloserMatch,\n\t\tnamespaceMatch,\n\t\tnameMatch,\n\t\tattrsMatch /* Internal/unused. */,\n\t\t,\n\t\tvoidMatch,\n\t] = matches;\n\n\tconst length = match.length;\n\tconst isCloser = !! closerMatch;\n\tconst isVoid = !! voidMatch;\n\tconst namespace = namespaceMatch || 'core/';\n\tconst name = namespace + nameMatch;\n\tconst hasAttrs = !! attrsMatch;\n\tconst attrs = hasAttrs ? parseJSON( attrsMatch ) : {};\n\n\t// This state isn't allowed\n\t// This is an error.\n\tif ( isCloser && ( isVoid || hasAttrs ) ) {\n\t\t// We can ignore them since they don't hurt anything\n\t\t// we may warn against this at some point or reject it.\n\t}\n\n\tif ( isVoid ) {\n\t\treturn [ 'void-block', name, attrs, startedAt, length ];\n\t}\n\n\tif ( isCloser ) {\n\t\treturn [ 'block-closer', name, null, startedAt, length ];\n\t}\n\n\treturn [ 'block-opener', name, attrs, startedAt, length ];\n}\n\n/**\n * Adds a freeform block to the output.\n *\n * @param rawLength Optional length of the raw HTML to include as freeform content.\n */\nfunction addFreeform( rawLength?: number ) {\n\tconst length = rawLength ? rawLength : document.length - offset;\n\n\tif ( 0 === length ) {\n\t\treturn;\n\t}\n\n\toutput.push( Freeform( document.substr( offset, length ) ) );\n}\n\n/**\n * Adds inner block to the parent block.\n *\n * @param block The inner block to be added to the parent.\n * @param tokenStart The start offset of the block token in the document.\n * @param tokenLength The total length of the block token.\n * @param lastOffset Optional offset marking the end of the current block,\n * used to update the parent's HTML content boundaries.\n */\nfunction addInnerBlock(\n\tblock: ParsedBlock,\n\ttokenStart: number,\n\ttokenLength: number,\n\tlastOffset?: number\n) {\n\tconst parent = stack[ stack.length - 1 ];\n\tparent.block.innerBlocks.push( block );\n\tconst html = document.substr(\n\t\tparent.prevOffset,\n\t\ttokenStart - parent.prevOffset\n\t);\n\n\tif ( html ) {\n\t\tparent.block.innerHTML += html;\n\t\tparent.block.innerContent.push( html );\n\t}\n\n\tparent.block.innerContent.push( null );\n\tparent.prevOffset = lastOffset ? lastOffset : tokenStart + tokenLength;\n}\n\n/**\n * Adds block from the stack to the output.\n *\n * @param endOffset Optional offset marking the end of the block's HTML content.\n */\nfunction addBlockFromStack( endOffset?: number ) {\n\tconst { block, leadingHtmlStart, prevOffset, tokenStart } =\n\t\tstack.pop() as ParsedFrame;\n\n\tconst html = endOffset\n\t\t? document.substr( prevOffset, endOffset - prevOffset )\n\t\t: document.substr( prevOffset );\n\n\tif ( html ) {\n\t\tblock.innerHTML += html;\n\t\tblock.innerContent.push( html );\n\t}\n\n\tif ( null !== leadingHtmlStart ) {\n\t\toutput.push(\n\t\t\tFreeform(\n\t\t\t\tdocument.substr(\n\t\t\t\t\tleadingHtmlStart,\n\t\t\t\t\ttokenStart - leadingHtmlStart\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\t}\n\n\toutput.push( block );\n}\n"], 5 "mappings": "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", 6 "names": [] 7 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Wed Apr 15 08:20:10 2026 | Cross-referenced by PHPXref |