PluginProbe
Gutenberg / 18.9.0
Gutenberg v18.9.0
24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 All 403 releases
gutenberg / build / block-serialization-default-parser / index.js

index.js in Gutenberg 18.9.0, at build/block-serialization-default-parser/index.js

485 lines 14.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /******/ (() => { // webpackBootstrap
2 /******/ "use strict";
3 /******/ // The require scope
4 /******/ var __webpack_require__ = {};
5 /******/
6 /************************************************************************/
7 /******/ /* webpack/runtime/define property getters */
8 /******/ (() => {
9 /******/ // define getter functions for harmony exports
10 /******/ __webpack_require__.d = (exports, definition) => {
11 /******/ for(var key in definition) {
12 /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
13 /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
14 /******/ }
15 /******/ }
16 /******/ };
17 /******/ })();
18 /******/
19 /******/ /* webpack/runtime/hasOwnProperty shorthand */
20 /******/ (() => {
21 /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
22 /******/ })();
23 /******/
24 /******/ /* webpack/runtime/make namespace object */
25 /******/ (() => {
26 /******/ // define __esModule on exports
27 /******/ __webpack_require__.r = (exports) => {
28 /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
29 /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
30 /******/ }
31 /******/ Object.defineProperty(exports, '__esModule', { value: true });
32 /******/ };
33 /******/ })();
34 /******/
35 /************************************************************************/
36 var __webpack_exports__ = {};
37 __webpack_require__.r(__webpack_exports__);
38 /* harmony export */ __webpack_require__.d(__webpack_exports__, {
39 /* harmony export */ parse: () => (/* binding */ parse)
40 /* harmony export */ });
41 /**
42 * @type {string}
43 */
44 let document;
45 /**
46 * @type {number}
47 */
48 let offset;
49 /**
50 * @type {ParsedBlock[]}
51 */
52 let output;
53 /**
54 * @type {ParsedFrame[]}
55 */
56 let stack;
57
58 /**
59 * @typedef {Object|null} Attributes
60 */
61
62 /**
63 * @typedef {Object} ParsedBlock
64 * @property {string|null} blockName Block name.
65 * @property {Attributes} attrs Block attributes.
66 * @property {ParsedBlock[]} innerBlocks Inner blocks.
67 * @property {string} innerHTML Inner HTML.
68 * @property {Array<string|null>} innerContent Inner content.
69 */
70
71 /**
72 * @typedef {Object} ParsedFrame
73 * @property {ParsedBlock} block Block.
74 * @property {number} tokenStart Token start.
75 * @property {number} tokenLength Token length.
76 * @property {number} prevOffset Previous offset.
77 * @property {number|null} leadingHtmlStart Leading HTML start.
78 */
79
80 /**
81 * @typedef {'no-more-tokens'|'void-block'|'block-opener'|'block-closer'} TokenType
82 */
83
84 /**
85 * @typedef {[TokenType, string, Attributes, number, number]} Token
86 */
87
88 /**
89 * Matches block comment delimiters
90 *
91 * While most of this pattern is straightforward the attribute parsing
92 * incorporates a tricks to make sure we don't choke on specific input
93 *
94 * - since JavaScript has no possessive quantifier or atomic grouping
95 * we are emulating it with a trick
96 *
97 * we want a possessive quantifier or atomic group to prevent backtracking
98 * on the `}`s should we fail to match the remainder of the pattern
99 *
100 * we can emulate this with a positive lookahead and back reference
101 * (a++)*c === ((?=(a+))\1)*c
102 *
103 * let's examine an example:
104 * - /(a+)*c/.test('aaaaaaaaaaaaad') fails after over 49,000 steps
105 * - /(a++)*c/.test('aaaaaaaaaaaaad') fails after 85 steps
106 * - /(?>a+)*c/.test('aaaaaaaaaaaaad') fails after 126 steps
107 *
108 * this is because the possessive `++` and the atomic group `(?>)`
109 * tell the engine that all those `a`s belong together as a single group
110 * and so it won't split it up when stepping backwards to try and match
111 *
112 * if we use /((?=(a+))\1)*c/ then we get the same behavior as the atomic group
113 * or possessive and prevent the backtracking because the `a+` is matched but
114 * not captured. thus, we find the long string of `a`s and remember it, then
115 * reference it as a whole unit inside our pattern
116 *
117 * @see http://instanceof.me/post/52245507631/regex-emulate-atomic-grouping-with-lookahead
118 * @see http://blog.stevenlevithan.com/archives/mimic-atomic-groups
119 * @see https://javascript.info/regexp-infinite-backtracking-problem
120 *
121 * once browsers reliably support atomic grouping or possessive
122 * quantifiers natively we should remove this trick and simplify
123 *
124 * @type {RegExp}
125 *
126 * @since 3.8.0
127 * @since 4.6.1 added optimization to prevent backtracking on attribute parsing
128 */
129 const tokenizer = /<!--\s+(\/)?wp:([a-z][a-z0-9_-]*\/)?([a-z][a-z0-9_-]*)\s+({(?:(?=([^}]+|}+(?=})|(?!}\s+\/?-->)[^])*)\5|[^]*?)}\s+)?(\/)?-->/g;
130
131 /**
132 * Constructs a block object.
133 *
134 * @param {string|null} blockName
135 * @param {Attributes} attrs
136 * @param {ParsedBlock[]} innerBlocks
137 * @param {string} innerHTML
138 * @param {string[]} innerContent
139 * @return {ParsedBlock} The block object.
140 */
141 function Block(blockName, attrs, innerBlocks, innerHTML, innerContent) {
142 return {
143 blockName,
144 attrs,
145 innerBlocks,
146 innerHTML,
147 innerContent
148 };
149 }
150
151 /**
152 * Constructs a freeform block object.
153 *
154 * @param {string} innerHTML
155 * @return {ParsedBlock} The freeform block object.
156 */
157 function Freeform(innerHTML) {
158 return Block(null, {}, [], innerHTML, [innerHTML]);
159 }
160
161 /**
162 * Constructs a frame object.
163 *
164 * @param {ParsedBlock} block
165 * @param {number} tokenStart
166 * @param {number} tokenLength
167 * @param {number} prevOffset
168 * @param {number|null} leadingHtmlStart
169 * @return {ParsedFrame} The frame object.
170 */
171 function Frame(block, tokenStart, tokenLength, prevOffset, leadingHtmlStart) {
172 return {
173 block,
174 tokenStart,
175 tokenLength,
176 prevOffset: prevOffset || tokenStart + tokenLength,
177 leadingHtmlStart
178 };
179 }
180
181 /**
182 * Parser function, that converts input HTML into a block based structure.
183 *
184 * @param {string} doc The HTML document to parse.
185 *
186 * @example
187 * Input post:
188 * ```html
189 * <!-- wp:columns {"columns":3} -->
190 * <div class="wp-block-columns has-3-columns"><!-- wp:column -->
191 * <div class="wp-block-column"><!-- wp:paragraph -->
192 * <p>Left</p>
193 * <!-- /wp:paragraph --></div>
194 * <!-- /wp:column -->
195 *
196 * <!-- wp:column -->
197 * <div class="wp-block-column"><!-- wp:paragraph -->
198 * <p><strong>Middle</strong></p>
199 * <!-- /wp:paragraph --></div>
200 * <!-- /wp:column -->
201 *
202 * <!-- wp:column -->
203 * <div class="wp-block-column"></div>
204 * <!-- /wp:column --></div>
205 * <!-- /wp:columns -->
206 * ```
207 *
208 * Parsing code:
209 * ```js
210 * import { parse } from '@wordpress/block-serialization-default-parser';
211 *
212 * parse( post ) === [
213 * {
214 * blockName: "core/columns",
215 * attrs: {
216 * columns: 3
217 * },
218 * innerBlocks: [
219 * {
220 * blockName: "core/column",
221 * attrs: null,
222 * innerBlocks: [
223 * {
224 * blockName: "core/paragraph",
225 * attrs: null,
226 * innerBlocks: [],
227 * innerHTML: "\n<p>Left</p>\n"
228 * }
229 * ],
230 * innerHTML: '\n<div class="wp-block-column"></div>\n'
231 * },
232 * {
233 * blockName: "core/column",
234 * attrs: null,
235 * innerBlocks: [
236 * {
237 * blockName: "core/paragraph",
238 * attrs: null,
239 * innerBlocks: [],
240 * innerHTML: "\n<p><strong>Middle</strong></p>\n"
241 * }
242 * ],
243 * innerHTML: '\n<div class="wp-block-column"></div>\n'
244 * },
245 * {
246 * blockName: "core/column",
247 * attrs: null,
248 * innerBlocks: [],
249 * innerHTML: '\n<div class="wp-block-column"></div>\n'
250 * }
251 * ],
252 * innerHTML: '\n<div class="wp-block-columns has-3-columns">\n\n\n\n</div>\n'
253 * }
254 * ];
255 * ```
256 * @return {ParsedBlock[]} A block-based representation of the input HTML.
257 */
258 const parse = doc => {
259 document = doc;
260 offset = 0;
261 output = [];
262 stack = [];
263 tokenizer.lastIndex = 0;
264 do {
265 // twiddle our thumbs
266 } while (proceed());
267 return output;
268 };
269
270 /**
271 * Parses the next token in the input document.
272 *
273 * @return {boolean} Returns true when there is more tokens to parse.
274 */
275 function proceed() {
276 const stackDepth = stack.length;
277 const next = nextToken();
278 const [tokenType, blockName, attrs, startOffset, tokenLength] = next;
279
280 // We may have some HTML soup before the next block.
281 const leadingHtmlStart = startOffset > offset ? offset : null;
282 switch (tokenType) {
283 case 'no-more-tokens':
284 // If not in a block then flush output.
285 if (0 === stackDepth) {
286 addFreeform();
287 return false;
288 }
289
290 // Otherwise we have a problem
291 // This is an error
292 // we have options
293 // - treat it all as freeform text
294 // - assume an implicit closer (easiest when not nesting)
295
296 // For the easy case we'll assume an implicit closer.
297 if (1 === stackDepth) {
298 addBlockFromStack();
299 return false;
300 }
301
302 // For the nested case where it's more difficult we'll
303 // have to assume that multiple closers are missing
304 // and so we'll collapse the whole stack piecewise.
305 while (0 < stack.length) {
306 addBlockFromStack();
307 }
308 return false;
309 case 'void-block':
310 // easy case is if we stumbled upon a void block
311 // in the top-level of the document.
312 if (0 === stackDepth) {
313 if (null !== leadingHtmlStart) {
314 output.push(Freeform(document.substr(leadingHtmlStart, startOffset - leadingHtmlStart)));
315 }
316 output.push(Block(blockName, attrs, [], '', []));
317 offset = startOffset + tokenLength;
318 return true;
319 }
320
321 // Otherwise we found an inner block.
322 addInnerBlock(Block(blockName, attrs, [], '', []), startOffset, tokenLength);
323 offset = startOffset + tokenLength;
324 return true;
325 case 'block-opener':
326 // Track all newly-opened blocks on the stack.
327 stack.push(Frame(Block(blockName, attrs, [], '', []), startOffset, tokenLength, startOffset + tokenLength, leadingHtmlStart));
328 offset = startOffset + tokenLength;
329 return true;
330 case 'block-closer':
331 // If we're missing an opener we're in trouble
332 // This is an error.
333 if (0 === stackDepth) {
334 // We have options
335 // - assume an implicit opener
336 // - assume _this_ is the opener
337 // - give up and close out the document.
338 addFreeform();
339 return false;
340 }
341
342 // If we're not nesting then this is easy - close the block.
343 if (1 === stackDepth) {
344 addBlockFromStack(startOffset);
345 offset = startOffset + tokenLength;
346 return true;
347 }
348
349 // Otherwise we're nested and we have to close out the current
350 // block and add it as a innerBlock to the parent.
351 const stackTop = /** @type {ParsedFrame} */stack.pop();
352 const html = document.substr(stackTop.prevOffset, startOffset - stackTop.prevOffset);
353 stackTop.block.innerHTML += html;
354 stackTop.block.innerContent.push(html);
355 stackTop.prevOffset = startOffset + tokenLength;
356 addInnerBlock(stackTop.block, stackTop.tokenStart, stackTop.tokenLength, startOffset + tokenLength);
357 offset = startOffset + tokenLength;
358 return true;
359 default:
360 // This is an error.
361 addFreeform();
362 return false;
363 }
364 }
365
366 /**
367 * Parse JSON if valid, otherwise return null
368 *
369 * Note that JSON coming from the block comment
370 * delimiters is constrained to be an object
371 * and cannot be things like `true` or `null`
372 *
373 * @param {string} input JSON input string to parse
374 * @return {Object|null} parsed JSON if valid
375 */
376 function parseJSON(input) {
377 try {
378 return JSON.parse(input);
379 } catch (e) {
380 return null;
381 }
382 }
383
384 /**
385 * Finds the next token in the document.
386 *
387 * @return {Token} The next matched token.
388 */
389 function nextToken() {
390 // Aye the magic
391 // we're using a single RegExp to tokenize the block comment delimiters
392 // we're also using a trick here because the only difference between a
393 // block opener and a block closer is the leading `/` before `wp:` (and
394 // a closer has no attributes). we can trap them both and process the
395 // match back in JavaScript to see which one it was.
396 const matches = tokenizer.exec(document);
397
398 // We have no more tokens.
399 if (null === matches) {
400 return ['no-more-tokens', '', null, 0, 0];
401 }
402 const startedAt = matches.index;
403 const [match, closerMatch, namespaceMatch, nameMatch, attrsMatch /* Internal/unused. */,, voidMatch] = matches;
404 const length = match.length;
405 const isCloser = !!closerMatch;
406 const isVoid = !!voidMatch;
407 const namespace = namespaceMatch || 'core/';
408 const name = namespace + nameMatch;
409 const hasAttrs = !!attrsMatch;
410 const attrs = hasAttrs ? parseJSON(attrsMatch) : {};
411
412 // This state isn't allowed
413 // This is an error.
414 if (isCloser && (isVoid || hasAttrs)) {
415 // We can ignore them since they don't hurt anything
416 // we may warn against this at some point or reject it.
417 }
418 if (isVoid) {
419 return ['void-block', name, attrs, startedAt, length];
420 }
421 if (isCloser) {
422 return ['block-closer', name, null, startedAt, length];
423 }
424 return ['block-opener', name, attrs, startedAt, length];
425 }
426
427 /**
428 * Adds a freeform block to the output.
429 *
430 * @param {number} [rawLength]
431 */
432 function addFreeform(rawLength) {
433 const length = rawLength ? rawLength : document.length - offset;
434 if (0 === length) {
435 return;
436 }
437 output.push(Freeform(document.substr(offset, length)));
438 }
439
440 /**
441 * Adds inner block to the parent block.
442 *
443 * @param {ParsedBlock} block
444 * @param {number} tokenStart
445 * @param {number} tokenLength
446 * @param {number} [lastOffset]
447 */
448 function addInnerBlock(block, tokenStart, tokenLength, lastOffset) {
449 const parent = stack[stack.length - 1];
450 parent.block.innerBlocks.push(block);
451 const html = document.substr(parent.prevOffset, tokenStart - parent.prevOffset);
452 if (html) {
453 parent.block.innerHTML += html;
454 parent.block.innerContent.push(html);
455 }
456 parent.block.innerContent.push(null);
457 parent.prevOffset = lastOffset ? lastOffset : tokenStart + tokenLength;
458 }
459
460 /**
461 * Adds block from the stack to the output.
462 *
463 * @param {number} [endOffset]
464 */
465 function addBlockFromStack(endOffset) {
466 const {
467 block,
468 leadingHtmlStart,
469 prevOffset,
470 tokenStart
471 } = /** @type {ParsedFrame} */stack.pop();
472 const html = endOffset ? document.substr(prevOffset, endOffset - prevOffset) : document.substr(prevOffset);
473 if (html) {
474 block.innerHTML += html;
475 block.innerContent.push(html);
476 }
477 if (null !== leadingHtmlStart) {
478 output.push(Freeform(document.substr(leadingHtmlStart, tokenStart - leadingHtmlStart)));
479 }
480 output.push(block);
481 }
482
483 (window.wp = window.wp || {}).blockSerializationDefaultParser = __webpack_exports__;
484 /******/ })()
485 ;