block-serialization-default-parser.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. /******/ (function() { // webpackBootstrap
  2. /******/ "use strict";
  3. /******/ // The require scope
  4. /******/ var __webpack_require__ = {};
  5. /******/
  6. /************************************************************************/
  7. /******/ /* webpack/runtime/define property getters */
  8. /******/ !function() {
  9. /******/ // define getter functions for harmony exports
  10. /******/ __webpack_require__.d = function(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. /******/ !function() {
  21. /******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
  22. /******/ }();
  23. /******/
  24. /******/ /* webpack/runtime/make namespace object */
  25. /******/ !function() {
  26. /******/ // define __esModule on exports
  27. /******/ __webpack_require__.r = function(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": function() { return /* binding */ parse; }
  40. /* harmony export */ });
  41. let document;
  42. let offset;
  43. let output;
  44. let stack;
  45. /**
  46. * Matches block comment delimiters
  47. *
  48. * While most of this pattern is straightforward the attribute parsing
  49. * incorporates a tricks to make sure we don't choke on specific input
  50. *
  51. * - since JavaScript has no possessive quantifier or atomic grouping
  52. * we are emulating it with a trick
  53. *
  54. * we want a possessive quantifier or atomic group to prevent backtracking
  55. * on the `}`s should we fail to match the remainder of the pattern
  56. *
  57. * we can emulate this with a positive lookahead and back reference
  58. * (a++)*c === ((?=(a+))\1)*c
  59. *
  60. * let's examine an example:
  61. * - /(a+)*c/.test('aaaaaaaaaaaaad') fails after over 49,000 steps
  62. * - /(a++)*c/.test('aaaaaaaaaaaaad') fails after 85 steps
  63. * - /(?>a+)*c/.test('aaaaaaaaaaaaad') fails after 126 steps
  64. *
  65. * this is because the possessive `++` and the atomic group `(?>)`
  66. * tell the engine that all those `a`s belong together as a single group
  67. * and so it won't split it up when stepping backwards to try and match
  68. *
  69. * if we use /((?=(a+))\1)*c/ then we get the same behavior as the atomic group
  70. * or possessive and prevent the backtracking because the `a+` is matched but
  71. * not captured. thus, we find the long string of `a`s and remember it, then
  72. * reference it as a whole unit inside our pattern
  73. *
  74. * @see http://instanceof.me/post/52245507631/regex-emulate-atomic-grouping-with-lookahead
  75. * @see http://blog.stevenlevithan.com/archives/mimic-atomic-groups
  76. * @see https://javascript.info/regexp-infinite-backtracking-problem
  77. *
  78. * once browsers reliably support atomic grouping or possessive
  79. * quantifiers natively we should remove this trick and simplify
  80. *
  81. * @type {RegExp}
  82. *
  83. * @since 3.8.0
  84. * @since 4.6.1 added optimization to prevent backtracking on attribute parsing
  85. */
  86. const tokenizer = /<!--\s+(\/)?wp:([a-z][a-z0-9_-]*\/)?([a-z][a-z0-9_-]*)\s+({(?:(?=([^}]+|}+(?=})|(?!}\s+\/?-->)[^])*)\5|[^]*?)}\s+)?(\/)?-->/g;
  87. function Block(blockName, attrs, innerBlocks, innerHTML, innerContent) {
  88. return {
  89. blockName,
  90. attrs,
  91. innerBlocks,
  92. innerHTML,
  93. innerContent
  94. };
  95. }
  96. function Freeform(innerHTML) {
  97. return Block(null, {}, [], innerHTML, [innerHTML]);
  98. }
  99. function Frame(block, tokenStart, tokenLength, prevOffset, leadingHtmlStart) {
  100. return {
  101. block,
  102. tokenStart,
  103. tokenLength,
  104. prevOffset: prevOffset || tokenStart + tokenLength,
  105. leadingHtmlStart
  106. };
  107. }
  108. /**
  109. * Parser function, that converts input HTML into a block based structure.
  110. *
  111. * @param {string} doc The HTML document to parse.
  112. *
  113. * @example
  114. * Input post:
  115. * ```html
  116. * <!-- wp:columns {"columns":3} -->
  117. * <div class="wp-block-columns has-3-columns"><!-- wp:column -->
  118. * <div class="wp-block-column"><!-- wp:paragraph -->
  119. * <p>Left</p>
  120. * <!-- /wp:paragraph --></div>
  121. * <!-- /wp:column -->
  122. *
  123. * <!-- wp:column -->
  124. * <div class="wp-block-column"><!-- wp:paragraph -->
  125. * <p><strong>Middle</strong></p>
  126. * <!-- /wp:paragraph --></div>
  127. * <!-- /wp:column -->
  128. *
  129. * <!-- wp:column -->
  130. * <div class="wp-block-column"></div>
  131. * <!-- /wp:column --></div>
  132. * <!-- /wp:columns -->
  133. * ```
  134. *
  135. * Parsing code:
  136. * ```js
  137. * import { parse } from '@wordpress/block-serialization-default-parser';
  138. *
  139. * parse( post ) === [
  140. * {
  141. * blockName: "core/columns",
  142. * attrs: {
  143. * columns: 3
  144. * },
  145. * innerBlocks: [
  146. * {
  147. * blockName: "core/column",
  148. * attrs: null,
  149. * innerBlocks: [
  150. * {
  151. * blockName: "core/paragraph",
  152. * attrs: null,
  153. * innerBlocks: [],
  154. * innerHTML: "\n<p>Left</p>\n"
  155. * }
  156. * ],
  157. * innerHTML: '\n<div class="wp-block-column"></div>\n'
  158. * },
  159. * {
  160. * blockName: "core/column",
  161. * attrs: null,
  162. * innerBlocks: [
  163. * {
  164. * blockName: "core/paragraph",
  165. * attrs: null,
  166. * innerBlocks: [],
  167. * innerHTML: "\n<p><strong>Middle</strong></p>\n"
  168. * }
  169. * ],
  170. * innerHTML: '\n<div class="wp-block-column"></div>\n'
  171. * },
  172. * {
  173. * blockName: "core/column",
  174. * attrs: null,
  175. * innerBlocks: [],
  176. * innerHTML: '\n<div class="wp-block-column"></div>\n'
  177. * }
  178. * ],
  179. * innerHTML: '\n<div class="wp-block-columns has-3-columns">\n\n\n\n</div>\n'
  180. * }
  181. * ];
  182. * ```
  183. * @return {Array} A block-based representation of the input HTML.
  184. */
  185. const parse = doc => {
  186. document = doc;
  187. offset = 0;
  188. output = [];
  189. stack = [];
  190. tokenizer.lastIndex = 0;
  191. do {// twiddle our thumbs
  192. } while (proceed());
  193. return output;
  194. };
  195. function proceed() {
  196. const next = nextToken();
  197. const [tokenType, blockName, attrs, startOffset, tokenLength] = next;
  198. const stackDepth = stack.length; // We may have some HTML soup before the next block.
  199. const leadingHtmlStart = startOffset > offset ? offset : null;
  200. switch (tokenType) {
  201. case 'no-more-tokens':
  202. // If not in a block then flush output.
  203. if (0 === stackDepth) {
  204. addFreeform();
  205. return false;
  206. } // Otherwise we have a problem
  207. // This is an error
  208. // we have options
  209. // - treat it all as freeform text
  210. // - assume an implicit closer (easiest when not nesting)
  211. // For the easy case we'll assume an implicit closer.
  212. if (1 === stackDepth) {
  213. addBlockFromStack();
  214. return false;
  215. } // For the nested case where it's more difficult we'll
  216. // have to assume that multiple closers are missing
  217. // and so we'll collapse the whole stack piecewise.
  218. while (0 < stack.length) {
  219. addBlockFromStack();
  220. }
  221. return false;
  222. case 'void-block':
  223. // easy case is if we stumbled upon a void block
  224. // in the top-level of the document.
  225. if (0 === stackDepth) {
  226. if (null !== leadingHtmlStart) {
  227. output.push(Freeform(document.substr(leadingHtmlStart, startOffset - leadingHtmlStart)));
  228. }
  229. output.push(Block(blockName, attrs, [], '', []));
  230. offset = startOffset + tokenLength;
  231. return true;
  232. } // Otherwise we found an inner block.
  233. addInnerBlock(Block(blockName, attrs, [], '', []), startOffset, tokenLength);
  234. offset = startOffset + tokenLength;
  235. return true;
  236. case 'block-opener':
  237. // Track all newly-opened blocks on the stack.
  238. stack.push(Frame(Block(blockName, attrs, [], '', []), startOffset, tokenLength, startOffset + tokenLength, leadingHtmlStart));
  239. offset = startOffset + tokenLength;
  240. return true;
  241. case 'block-closer':
  242. // If we're missing an opener we're in trouble
  243. // This is an error.
  244. if (0 === stackDepth) {
  245. // We have options
  246. // - assume an implicit opener
  247. // - assume _this_ is the opener
  248. // - give up and close out the document.
  249. addFreeform();
  250. return false;
  251. } // If we're not nesting then this is easy - close the block.
  252. if (1 === stackDepth) {
  253. addBlockFromStack(startOffset);
  254. offset = startOffset + tokenLength;
  255. return true;
  256. } // Otherwise we're nested and we have to close out the current
  257. // block and add it as a innerBlock to the parent.
  258. const stackTop = stack.pop();
  259. const html = document.substr(stackTop.prevOffset, startOffset - stackTop.prevOffset);
  260. stackTop.block.innerHTML += html;
  261. stackTop.block.innerContent.push(html);
  262. stackTop.prevOffset = startOffset + tokenLength;
  263. addInnerBlock(stackTop.block, stackTop.tokenStart, stackTop.tokenLength, startOffset + tokenLength);
  264. offset = startOffset + tokenLength;
  265. return true;
  266. default:
  267. // This is an error.
  268. addFreeform();
  269. return false;
  270. }
  271. }
  272. /**
  273. * Parse JSON if valid, otherwise return null
  274. *
  275. * Note that JSON coming from the block comment
  276. * delimiters is constrained to be an object
  277. * and cannot be things like `true` or `null`
  278. *
  279. * @param {string} input JSON input string to parse
  280. * @return {Object|null} parsed JSON if valid
  281. */
  282. function parseJSON(input) {
  283. try {
  284. return JSON.parse(input);
  285. } catch (e) {
  286. return null;
  287. }
  288. }
  289. function nextToken() {
  290. // Aye the magic
  291. // we're using a single RegExp to tokenize the block comment delimiters
  292. // we're also using a trick here because the only difference between a
  293. // block opener and a block closer is the leading `/` before `wp:` (and
  294. // a closer has no attributes). we can trap them both and process the
  295. // match back in JavaScript to see which one it was.
  296. const matches = tokenizer.exec(document); // We have no more tokens.
  297. if (null === matches) {
  298. return ['no-more-tokens'];
  299. }
  300. const startedAt = matches.index;
  301. const [match, closerMatch, namespaceMatch, nameMatch, attrsMatch
  302. /* Internal/unused. */
  303. ,, voidMatch] = matches;
  304. const length = match.length;
  305. const isCloser = !!closerMatch;
  306. const isVoid = !!voidMatch;
  307. const namespace = namespaceMatch || 'core/';
  308. const name = namespace + nameMatch;
  309. const hasAttrs = !!attrsMatch;
  310. const attrs = hasAttrs ? parseJSON(attrsMatch) : {}; // This state isn't allowed
  311. // This is an error.
  312. if (isCloser && (isVoid || hasAttrs)) {// We can ignore them since they don't hurt anything
  313. // we may warn against this at some point or reject it.
  314. }
  315. if (isVoid) {
  316. return ['void-block', name, attrs, startedAt, length];
  317. }
  318. if (isCloser) {
  319. return ['block-closer', name, null, startedAt, length];
  320. }
  321. return ['block-opener', name, attrs, startedAt, length];
  322. }
  323. function addFreeform(rawLength) {
  324. const length = rawLength ? rawLength : document.length - offset;
  325. if (0 === length) {
  326. return;
  327. }
  328. output.push(Freeform(document.substr(offset, length)));
  329. }
  330. function addInnerBlock(block, tokenStart, tokenLength, lastOffset) {
  331. const parent = stack[stack.length - 1];
  332. parent.block.innerBlocks.push(block);
  333. const html = document.substr(parent.prevOffset, tokenStart - parent.prevOffset);
  334. if (html) {
  335. parent.block.innerHTML += html;
  336. parent.block.innerContent.push(html);
  337. }
  338. parent.block.innerContent.push(null);
  339. parent.prevOffset = lastOffset ? lastOffset : tokenStart + tokenLength;
  340. }
  341. function addBlockFromStack(endOffset) {
  342. const {
  343. block,
  344. leadingHtmlStart,
  345. prevOffset,
  346. tokenStart
  347. } = stack.pop();
  348. const html = endOffset ? document.substr(prevOffset, endOffset - prevOffset) : document.substr(prevOffset);
  349. if (html) {
  350. block.innerHTML += html;
  351. block.innerContent.push(html);
  352. }
  353. if (null !== leadingHtmlStart) {
  354. output.push(Freeform(document.substr(leadingHtmlStart, tokenStart - leadingHtmlStart)));
  355. }
  356. output.push(block);
  357. }
  358. (window.wp = window.wp || {}).blockSerializationDefaultParser = __webpack_exports__;
  359. /******/ })()
  360. ;