aboutsummaryrefslogtreecommitdiff
path: root/js/lib/blocks.js
blob: 4b36ac6ec705738490db658647e6c39e20a2b80f (plain)
  1. "use strict";
  2. var Node = require('./node');
  3. var unescapeString = require('./common').unescapeString;
  4. var C_GREATERTHAN = 62;
  5. var C_NEWLINE = 10;
  6. var C_SPACE = 32;
  7. var C_OPEN_BRACKET = 91;
  8. var CODE_INDENT = 4;
  9. var InlineParser = require('./inlines');
  10. var BLOCKTAGNAME = '(?:article|header|aside|hgroup|iframe|blockquote|hr|body|li|map|button|object|canvas|ol|caption|output|col|p|colgroup|pre|dd|progress|div|section|dl|table|td|dt|tbody|embed|textarea|fieldset|tfoot|figcaption|th|figure|thead|footer|footer|tr|form|ul|h1|h2|h3|h4|h5|h6|video|script|style)';
  11. var HTMLBLOCKOPEN = "<(?:" + BLOCKTAGNAME + "[\\s/>]" + "|" +
  12. "/" + BLOCKTAGNAME + "[\\s>]" + "|" + "[?!])";
  13. var reHtmlBlockOpen = new RegExp('^' + HTMLBLOCKOPEN, 'i');
  14. var reHrule = /^(?:(?:\* *){3,}|(?:_ *){3,}|(?:- *){3,}) *$/;
  15. var reMaybeSpecial = /^[#`~*+_=<>0-9-]/;
  16. var reNonSpace = /[^ \t\n]/;
  17. var reBulletListMarker = /^[*+-]( +|$)/;
  18. var reOrderedListMarker = /^(\d+)([.)])( +|$)/;
  19. var reATXHeaderMarker = /^#{1,6}(?: +|$)/;
  20. var reCodeFence = /^`{3,}(?!.*`)|^~{3,}(?!.*~)/;
  21. var reClosingCodeFence = /^(?:`{3,}|~{3,})(?= *$)/;
  22. var reSetextHeaderLine = /^(?:=+|-+) *$/;
  23. var reLineEnding = /\r\n|\n|\r/;
  24. // Returns true if string contains only space characters.
  25. var isBlank = function(s) {
  26. return !(reNonSpace.test(s));
  27. };
  28. var tabSpaces = [' ', ' ', ' ', ' '];
  29. // Convert tabs to spaces on each line using a 4-space tab stop.
  30. var detabLine = function(text) {
  31. var start = 0;
  32. var offset;
  33. var lastStop = 0;
  34. while ((offset = text.indexOf('\t', start)) !== -1) {
  35. var numspaces = (offset - lastStop) % 4;
  36. var spaces = tabSpaces[numspaces];
  37. text = text.slice(0, offset) + spaces + text.slice(offset + 1);
  38. lastStop = offset + numspaces;
  39. start = lastStop;
  40. }
  41. return text;
  42. };
  43. // Attempt to match a regex in string s at offset offset.
  44. // Return index of match or -1.
  45. var matchAt = function(re, s, offset) {
  46. var res = s.slice(offset).match(re);
  47. if (res === null) {
  48. return -1;
  49. } else {
  50. return offset + res.index;
  51. }
  52. };
  53. // destructively trip final blank lines in an array of strings
  54. var stripFinalBlankLines = function(lns) {
  55. var i = lns.length - 1;
  56. while (!reNonSpace.test(lns[i])) {
  57. lns.pop();
  58. i--;
  59. }
  60. };
  61. // DOC PARSER
  62. // These are methods of a Parser object, defined below.
  63. // Returns true if parent block can contain child block.
  64. var canContain = function(parent_type, child_type) {
  65. return ( parent_type === 'Document' ||
  66. parent_type === 'BlockQuote' ||
  67. parent_type === 'Item' ||
  68. (parent_type === 'List' && child_type === 'Item') );
  69. };
  70. // Returns true if block type can accept lines of text.
  71. var acceptsLines = function(block_type) {
  72. return ( block_type === 'Paragraph' ||
  73. block_type === 'CodeBlock' );
  74. };
  75. // Returns true if block ends with a blank line, descending if needed
  76. // into lists and sublists.
  77. var endsWithBlankLine = function(block) {
  78. while (block) {
  79. if (block._lastLineBlank) {
  80. return true;
  81. }
  82. var t = block.type;
  83. if (t === 'List' || t === 'Item') {
  84. block = block._lastChild;
  85. } else {
  86. break;
  87. }
  88. }
  89. return false;
  90. };
  91. // Break out of all containing lists, resetting the tip of the
  92. // document to the parent of the highest list, and finalizing
  93. // all the lists. (This is used to implement the "two blank lines
  94. // break of of all lists" feature.)
  95. var breakOutOfLists = function(block) {
  96. var b = block;
  97. var last_list = null;
  98. do {
  99. if (b.type === 'List') {
  100. last_list = b;
  101. }
  102. b = b._parent;
  103. } while (b);
  104. if (last_list) {
  105. while (block !== last_list) {
  106. this.finalize(block, this.lineNumber);
  107. block = block._parent;
  108. }
  109. this.finalize(last_list, this.lineNumber);
  110. this.tip = last_list._parent;
  111. }
  112. };
  113. // Add a line to the block at the tip. We assume the tip
  114. // can accept lines -- that check should be done before calling this.
  115. var addLine = function(ln) {
  116. this.tip._strings.push(ln.slice(this.offset));
  117. };
  118. // Add block of type tag as a child of the tip. If the tip can't
  119. // accept children, close and finalize it and try its parent,
  120. // and so on til we find a block that can accept children.
  121. var addChild = function(tag, offset) {
  122. while (!canContain(this.tip.type, tag)) {
  123. this.finalize(this.tip, this.lineNumber - 1);
  124. }
  125. var column_number = offset + 1; // offset 0 = column 1
  126. var newBlock = new Node(tag, [[this.lineNumber, column_number], [0, 0]]);
  127. newBlock._strings = [];
  128. newBlock._string_content = null;
  129. this.tip.appendChild(newBlock);
  130. this.tip = newBlock;
  131. return newBlock;
  132. };
  133. // Parse a list marker and return data on the marker (type,
  134. // start, delimiter, bullet character, padding) or null.
  135. var parseListMarker = function(ln, offset, indent) {
  136. var rest = ln.slice(offset);
  137. var match;
  138. var spaces_after_marker;
  139. var data = { type: null,
  140. tight: true, // lists are tight by default
  141. bulletChar: null,
  142. start: null,
  143. delimiter: null,
  144. padding: null,
  145. markerOffset: indent };
  146. if (rest.match(reHrule)) {
  147. return null;
  148. }
  149. if ((match = rest.match(reBulletListMarker))) {
  150. spaces_after_marker = match[1].length;
  151. data.type = 'Bullet';
  152. data.bulletChar = match[0][0];
  153. } else if ((match = rest.match(reOrderedListMarker))) {
  154. spaces_after_marker = match[3].length;
  155. data.type = 'Ordered';
  156. data.start = parseInt(match[1]);
  157. data.delimiter = match[2];
  158. } else {
  159. return null;
  160. }
  161. var blank_item = match[0].length === rest.length;
  162. if (spaces_after_marker >= 5 ||
  163. spaces_after_marker < 1 ||
  164. blank_item) {
  165. data.padding = match[0].length - spaces_after_marker + 1;
  166. } else {
  167. data.padding = match[0].length;
  168. }
  169. return data;
  170. };
  171. // Returns true if the two list items are of the same type,
  172. // with the same delimiter and bullet character. This is used
  173. // in agglomerating list items into lists.
  174. var listsMatch = function(list_data, item_data) {
  175. return (list_data.type === item_data.type &&
  176. list_data.delimiter === item_data.delimiter &&
  177. list_data.bulletChar === item_data.bulletChar);
  178. };
  179. // Finalize and close any unmatched blocks. Returns true.
  180. var closeUnmatchedBlocks = function() {
  181. // finalize any blocks not matched
  182. while (this.oldtip !== this.lastMatchedContainer) {
  183. var parent = this.oldtip._parent;
  184. this.finalize(this.oldtip, this.lineNumber - 1);
  185. this.oldtip = parent;
  186. }
  187. return true;
  188. };
  189. // 'finalize' is run when the block is closed.
  190. // 'continue' is run to check whether the block is continuing
  191. // at a certain line and offset (e.g. whether a block quote
  192. // contains a `>`. It returns 0 for matched, 1 for not matched,
  193. // and 2 for "we've dealt with this line completely, go to next."
  194. var blocks = {
  195. Document: {
  196. continue: function(parser, container, next_nonspace) {
  197. return 0;
  198. },
  199. finalize: function(parser, block) {
  200. return;
  201. }
  202. },
  203. List: {
  204. continue: function(parser, container, next_nonspace) {
  205. return 0;
  206. },
  207. finalize: function(parser, block) {
  208. var item = block._firstChild;
  209. while (item) {
  210. // check for non-final list item ending with blank line:
  211. if (endsWithBlankLine(item) && item._next) {
  212. block._listData.tight = false;
  213. break;
  214. }
  215. // recurse into children of list item, to see if there are
  216. // spaces between any of them:
  217. var subitem = item._firstChild;
  218. while (subitem) {
  219. if (endsWithBlankLine(subitem) &&
  220. (item._next || subitem._next)) {
  221. block._listData.tight = false;
  222. break;
  223. }
  224. subitem = subitem._next;
  225. }
  226. item = item._next;
  227. }
  228. }
  229. },
  230. BlockQuote: {
  231. continue: function(parser, container, next_nonspace) {
  232. var ln = parser.currentLine;
  233. if (next_nonspace - parser.offset <= 3 &&
  234. ln.charCodeAt(next_nonspace) === C_GREATERTHAN) {
  235. parser.offset = next_nonspace + 1;
  236. if (ln.charCodeAt(parser.offset) === C_SPACE) {
  237. parser.offset++;
  238. }
  239. } else {
  240. return 1;
  241. }
  242. return 0;
  243. },
  244. finalize: function(parser, block) {
  245. return;
  246. }
  247. },
  248. Item: {
  249. continue: function(parser, container, next_nonspace) {
  250. if (next_nonspace === parser.currentLine.length) { // blank
  251. parser.offset = next_nonspace;
  252. } else if (next_nonspace - parser.offset >=
  253. container._listData.markerOffset +
  254. container._listData.padding) {
  255. parser.offset += container._listData.markerOffset +
  256. container._listData.padding;
  257. } else {
  258. return 1;
  259. }
  260. return 0;
  261. },
  262. finalize: function(parser, block) {
  263. return;
  264. }
  265. },
  266. Header: {
  267. continue: function(parser, container, next_nonspace) {
  268. // a header can never container > 1 line, so fail to match:
  269. return 1;
  270. },
  271. finalize: function(parser, block) {
  272. block._string_content = block._strings.join('\n');
  273. }
  274. },
  275. HorizontalRule: {
  276. continue: function(parser, container, next_nonspace) {
  277. // an hrule can never container > 1 line, so fail to match:
  278. return 1;
  279. },
  280. finalize: function(parser, block) {
  281. return;
  282. }
  283. },
  284. CodeBlock: {
  285. continue: function(parser, container, next_nonspace) {
  286. var ln = parser.currentLine;
  287. var indent = next_nonspace - parser.offset;
  288. if (container._isFenced) { // fenced
  289. var match = (indent <= 3 &&
  290. ln.charAt(next_nonspace) === container._fenceChar &&
  291. ln.slice(next_nonspace).match(reClosingCodeFence));
  292. if (match && match[0].length >= container._fenceLength) {
  293. // closing fence - we're at end of line, so we can return
  294. parser.finalize(container, parser.lineNumber);
  295. return 2;
  296. } else {
  297. // skip optional spaces of fence offset
  298. var i = container._fenceOffset;
  299. while (i > 0 && ln.charCodeAt(parser.offset) === C_SPACE) {
  300. parser.offset++;
  301. i--;
  302. }
  303. }
  304. } else { // indented
  305. if (indent >= CODE_INDENT) {
  306. parser.offset += CODE_INDENT;
  307. } else if (next_nonspace === ln.length) { // blank
  308. parser.offset = next_nonspace;
  309. } else {
  310. return 1;
  311. }
  312. }
  313. return 0;
  314. },
  315. finalize: function(parser, block) {
  316. if (block._isFenced) { // fenced
  317. // first line becomes info string
  318. block.info = unescapeString(block._strings[0].trim());
  319. if (block._strings.length === 1) {
  320. block._literal = '';
  321. } else {
  322. block._literal = block._strings.slice(1).join('\n') + '\n';
  323. }
  324. } else { // indented
  325. stripFinalBlankLines(block._strings);
  326. block._literal = block._strings.join('\n') + '\n';
  327. }
  328. }
  329. },
  330. HtmlBlock: {
  331. continue: function(parser, container, next_nonspace) {
  332. return (next_nonspace === parser.currentLine.length ? 1 : 0);
  333. },
  334. finalize: function(parser, block) {
  335. block._literal = block._strings.join('\n');
  336. }
  337. },
  338. Paragraph: {
  339. continue: function(parser, container, next_nonspace) {
  340. return (next_nonspace === parser.currentLine.length ? 1 : 0);
  341. },
  342. finalize: function(parser, block) {
  343. var pos;
  344. block._string_content = block._strings.join('\n');
  345. // try parsing the beginning as link reference definitions:
  346. while (block._string_content.charCodeAt(0) === C_OPEN_BRACKET &&
  347. (pos =
  348. parser.inlineParser.parseReference(block._string_content,
  349. parser.refmap))) {
  350. block._string_content = block._string_content.slice(pos);
  351. if (isBlank(block._string_content)) {
  352. block.unlink();
  353. break;
  354. }
  355. }
  356. }
  357. }
  358. };
  359. // Analyze a line of text and update the document appropriately.
  360. // We parse markdown text by calling this on each line of input,
  361. // then finalizing the document.
  362. var incorporateLine = function(ln) {
  363. var all_matched = true;
  364. var next_nonspace;
  365. var match;
  366. var data;
  367. var blank;
  368. var indent;
  369. var allClosed;
  370. var container = this.doc;
  371. this.oldtip = this.tip;
  372. this.offset = 0;
  373. this.lineNumber += 1;
  374. // replace NUL characters for security
  375. if (ln.indexOf('\u0000') !== -1) {
  376. ln = ln.replace(/\0/g, '\uFFFD');
  377. }
  378. // Convert tabs to spaces:
  379. ln = detabLine(ln);
  380. this.currentLine = ln;
  381. // For each containing block, try to parse the associated line start.
  382. // Bail out on failure: container will point to the last matching block.
  383. // Set all_matched to false if not all containers match.
  384. var lastChild;
  385. while ((lastChild = container._lastChild) && lastChild._open) {
  386. container = lastChild;
  387. match = matchAt(reNonSpace, ln, this.offset);
  388. if (match === -1) {
  389. next_nonspace = ln.length;
  390. } else {
  391. next_nonspace = match;
  392. }
  393. switch (this.blocks[container.type].continue(this, container, next_nonspace)) {
  394. case 0: // we've matched, keep going
  395. break;
  396. case 1: // we've failed to match a block
  397. all_matched = false;
  398. break;
  399. case 2: // we've hit end of line for fenced code close and can return
  400. this.lastLineLength = ln.length - 1; // -1 for newline
  401. return;
  402. default:
  403. throw 'continue returned illegal value, must be 0, 1, or 2';
  404. }
  405. if (!all_matched) {
  406. container = container._parent; // back up to last matching block
  407. break;
  408. }
  409. }
  410. blank = next_nonspace === ln.length;
  411. allClosed = (container === this.oldtip);
  412. this.lastMatchedContainer = container;
  413. // Check to see if we've hit 2nd blank line; if so break out of list:
  414. if (blank && container._lastLineBlank) {
  415. this.breakOutOfLists(container);
  416. }
  417. // Unless last matched container is a code block, try new container starts,
  418. // adding children to the last matched container:
  419. while (true) {
  420. var t = container.type;
  421. match = matchAt(reNonSpace, ln, this.offset);
  422. if (match === -1) {
  423. next_nonspace = ln.length;
  424. blank = true;
  425. break;
  426. } else {
  427. next_nonspace = match;
  428. blank = false;
  429. }
  430. indent = next_nonspace - this.offset;
  431. if (t === 'CodeBlock' || t === 'HtmlBlock') {
  432. break;
  433. }
  434. if (indent >= CODE_INDENT) {
  435. // indented code
  436. if (this.tip.type !== 'Paragraph' && !blank) {
  437. this.offset += CODE_INDENT;
  438. allClosed = allClosed ||
  439. this.closeUnmatchedBlocks();
  440. container = this.addChild('CodeBlock', this.offset);
  441. }
  442. break;
  443. }
  444. // this is a little performance optimization:
  445. if (matchAt(reMaybeSpecial, ln, next_nonspace) === -1) {
  446. break;
  447. }
  448. this.offset = next_nonspace;
  449. var cc = ln.charCodeAt(this.offset);
  450. if (cc === C_GREATERTHAN) {
  451. // blockquote
  452. this.offset += 1;
  453. // optional following space
  454. if (ln.charCodeAt(this.offset) === C_SPACE) {
  455. this.offset++;
  456. }
  457. allClosed = allClosed || this.closeUnmatchedBlocks();
  458. container = this.addChild('BlockQuote', next_nonspace);
  459. } else if ((match = ln.slice(this.offset).match(reATXHeaderMarker))) {
  460. // ATX header
  461. this.offset += match[0].length;
  462. allClosed = allClosed || this.closeUnmatchedBlocks();
  463. container = this.addChild('Header', next_nonspace);
  464. container.level = match[0].trim().length; // number of #s
  465. // remove trailing ###s:
  466. container._strings =
  467. [ln.slice(this.offset).replace(/^ *#+ *$/, '').replace(/ +#+ *$/, '')];
  468. break;
  469. } else if ((match = ln.slice(this.offset).match(reCodeFence))) {
  470. // fenced code block
  471. var fenceLength = match[0].length;
  472. allClosed = allClosed || this.closeUnmatchedBlocks();
  473. container = this.addChild('CodeBlock', next_nonspace);
  474. container._isFenced = true;
  475. container._fenceLength = fenceLength;
  476. container._fenceChar = match[0][0];
  477. container._fenceOffset = indent;
  478. this.offset += fenceLength;
  479. } else if (matchAt(reHtmlBlockOpen, ln, this.offset) !== -1) {
  480. // html block
  481. allClosed = allClosed || this.closeUnmatchedBlocks();
  482. container = this.addChild('HtmlBlock', this.offset);
  483. this.offset -= indent; // back up so spaces are part of block
  484. break;
  485. } else if (t === 'Paragraph' &&
  486. container._strings.length === 1 &&
  487. ((match = ln.slice(this.offset).match(reSetextHeaderLine)))) {
  488. // setext header line
  489. allClosed = allClosed || this.closeUnmatchedBlocks();
  490. var header = new Node('Header', container.sourcepos);
  491. header.level = match[0][0] === '=' ? 1 : 2;
  492. header._strings = container._strings;
  493. container.insertAfter(header);
  494. container.unlink();
  495. container = header;
  496. this.tip = header;
  497. this.offset = ln.length;
  498. break;
  499. } else if (matchAt(reHrule, ln, this.offset) !== -1) {
  500. // hrule
  501. allClosed = allClosed || this.closeUnmatchedBlocks();
  502. container = this.addChild('HorizontalRule', next_nonspace);
  503. this.offset = ln.length - 1;
  504. break;
  505. } else if ((data = parseListMarker(ln, this.offset, indent))) {
  506. // list item
  507. allClosed = allClosed || this.closeUnmatchedBlocks();
  508. this.offset += data.padding;
  509. // add the list if needed
  510. if (t !== 'List' ||
  511. !(listsMatch(container._listData, data))) {
  512. container = this.addChild('List', next_nonspace);
  513. container._listData = data;
  514. }
  515. // add the list item
  516. container = this.addChild('Item', next_nonspace);
  517. container._listData = data;
  518. } else {
  519. break;
  520. }
  521. }
  522. // What remains at the offset is a text line. Add the text to the
  523. // appropriate container.
  524. // First check for a lazy paragraph continuation:
  525. if (!allClosed && !blank &&
  526. this.tip.type === 'Paragraph' &&
  527. this.tip._strings.length > 0) {
  528. // lazy paragraph continuation
  529. this.addLine(ln);
  530. } else { // not a lazy continuation
  531. // finalize any blocks not matched
  532. allClosed = allClosed || this.closeUnmatchedBlocks();
  533. if (blank && container.lastChild) {
  534. container.lastChild._lastLineBlank = true;
  535. }
  536. t = container.type;
  537. // Block quote lines are never blank as they start with >
  538. // and we don't count blanks in fenced code for purposes of tight/loose
  539. // lists or breaking out of lists. We also don't set _lastLineBlank
  540. // on an empty list item, or if we just closed a fenced block.
  541. var lastLineBlank = blank &&
  542. !(t === 'BlockQuote' ||
  543. (t === 'CodeBlock' && container._isFenced) ||
  544. (t === 'Item' &&
  545. !container._firstChild &&
  546. container.sourcepos[0][0] === this.lineNumber));
  547. // propagate lastLineBlank up through parents:
  548. var cont = container;
  549. while (cont) {
  550. cont._lastLineBlank = lastLineBlank;
  551. cont = cont._parent;
  552. }
  553. switch (t) {
  554. case 'HtmlBlock':
  555. case 'CodeBlock':
  556. this.addLine(ln);
  557. break;
  558. case 'Header':
  559. case 'HorizontalRule':
  560. // nothing to do; we already added the contents.
  561. break;
  562. default:
  563. this.offset = next_nonspace;
  564. if (acceptsLines(t)) {
  565. this.addLine(ln);
  566. } else if (blank) {
  567. break;
  568. } else {
  569. // create paragraph container for line
  570. container = this.addChild('Paragraph', this.offset);
  571. this.addLine(ln);
  572. }
  573. }
  574. }
  575. this.lastLineLength = ln.length - 1; // -1 for newline
  576. };
  577. // Finalize a block. Close it and do any necessary postprocessing,
  578. // e.g. creating string_content from strings, setting the 'tight'
  579. // or 'loose' status of a list, and parsing the beginnings
  580. // of paragraphs for reference definitions. Reset the tip to the
  581. // parent of the closed block.
  582. var finalize = function(block, lineNumber) {
  583. var above = block._parent || this.top;
  584. block._open = false;
  585. block.sourcepos[1] = [lineNumber, this.lastLineLength + 1];
  586. this.blocks[block.type].finalize(this, block);
  587. this.tip = above;
  588. };
  589. // Walk through a block & children recursively, parsing string content
  590. // into inline content where appropriate. Returns new object.
  591. var processInlines = function(block) {
  592. var node, event, t;
  593. var walker = block.walker();
  594. while ((event = walker.next())) {
  595. node = event.node;
  596. t = node.type;
  597. if (!event.entering && (t === 'Paragraph' || t === 'Header')) {
  598. this.inlineParser.parse(node, this.refmap);
  599. }
  600. }
  601. };
  602. var Document = function() {
  603. var doc = new Node('Document', [[1, 1], [0, 0]]);
  604. doc._string_content = null;
  605. doc._strings = [];
  606. return doc;
  607. };
  608. // The main parsing function. Returns a parsed document AST.
  609. var parse = function(input) {
  610. this.doc = new Document();
  611. this.tip = this.doc;
  612. this.refmap = {};
  613. if (this.options.time) { console.time("preparing input"); }
  614. var lines = input.split(reLineEnding);
  615. var len = lines.length;
  616. if (input.charCodeAt(input.length - 1) === C_NEWLINE) {
  617. // ignore last blank line created by final newline
  618. len -= 1;
  619. }
  620. if (this.options.time) { console.timeEnd("preparing input"); }
  621. if (this.options.time) { console.time("block parsing"); }
  622. for (var i = 0; i < len; i++) {
  623. this.incorporateLine(lines[i]);
  624. }
  625. while (this.tip) {
  626. this.finalize(this.tip, len);
  627. }
  628. if (this.options.time) { console.timeEnd("block parsing"); }
  629. if (this.options.time) { console.time("inline parsing"); }
  630. this.processInlines(this.doc);
  631. if (this.options.time) { console.timeEnd("inline parsing"); }
  632. return this.doc;
  633. };
  634. // The Parser object.
  635. function Parser(options){
  636. return {
  637. doc: new Document(),
  638. blocks: blocks,
  639. tip: this.doc,
  640. oldtip: this.doc,
  641. currentLine: "",
  642. lineNumber: 0,
  643. offset: 0,
  644. lastMatchedContainer: this.doc,
  645. refmap: {},
  646. lastLineLength: 0,
  647. inlineParser: new InlineParser(),
  648. breakOutOfLists: breakOutOfLists,
  649. addLine: addLine,
  650. addChild: addChild,
  651. incorporateLine: incorporateLine,
  652. finalize: finalize,
  653. processInlines: processInlines,
  654. closeUnmatchedBlocks: closeUnmatchedBlocks,
  655. parse: parse,
  656. options: options || {}
  657. };
  658. }
  659. module.exports = Parser;