PHP and Javascript implementations of a simple markdown parser
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

markdown.js 24KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126
  1. // Blocks
  2. // - Paragraph
  3. // - Header 1-6 # ## ### #### ##### ###### or === ---
  4. // - Blockquote (nestable) >
  5. // - Unordered list (nestable) *_
  6. // - Ordered list (nestable) 1._
  7. // - Code block ```\ncode\n``` or 4 spaces/tab indent
  8. // - Horizontal rule --- - - - * * * etc
  9. // - Table -|-
  10. // - Definition list term\n: definition\n: alternate definition
  11. // - Footnote (bottom) citation[^1]
  12. // - Abbreviation (definition) *[ABC]: Abbrev Blah Cat
  13. // Inline
  14. // - Link [text](https://url)
  15. // - Emphasis *emphasized*
  16. // - Strong **bold**
  17. // - Inline code `code`
  18. // - Strikethrough ~strike~
  19. // - Image ![alt text](https://image){.cssclass}
  20. // - Footnote (inline) [^1]: footnote text
  21. // - Abbreviation (inline)
  22. class _MDHAlign {
  23. static Left = new _MDHAlign('Left');
  24. static Center = new _MDHAlign('Center');
  25. static Right = new _MDHAlign('Right');
  26. constructor(name) {
  27. this.name = name;
  28. }
  29. toString() {
  30. return `_MDHAlign.${this.name}`;
  31. }
  32. static toHTMLAttribute(align) {
  33. switch (align) {
  34. case _MDHAlign.Left: return ' align="left"';
  35. case _MDHAlign.Center: return ' align="center"';
  36. case _MDHAlign.Right: return ' align="right"';
  37. }
  38. return '';
  39. }
  40. }
  41. class _MDSpan {
  42. toHTML(config) {
  43. throw new Error(self.constructor.name + ".toHTML not implemented");
  44. }
  45. static toHTML(spans, config) {
  46. return spans.map((span) => span.toHTML(config)).join("");
  47. }
  48. }
  49. class _MDMultiSpan extends _MDSpan {
  50. /** @var {_MDSpan[]} */
  51. content;
  52. /**
  53. * @param {_MDSpan[]} content
  54. */
  55. constructor(content) {
  56. super();
  57. this.content = content;
  58. }
  59. toHTML() {
  60. return _MDSpan.toHTML(this.content);
  61. }
  62. }
  63. class _MDTextSpan extends _MDSpan {
  64. /** @param {String} text */
  65. text;
  66. /**
  67. * @param {String} text
  68. */
  69. constructor(text) {
  70. super();
  71. this.text = text;
  72. }
  73. toHTML(config) {
  74. return this.text.replace('<', '&lt;');
  75. }
  76. }
  77. class _MDHTMLSpan extends _MDSpan {
  78. /** @param {String} html */
  79. html;
  80. /**
  81. * @param {String} html
  82. */
  83. constructor(html) {
  84. super();
  85. this.html = html;
  86. }
  87. toHTML(config) {
  88. return this.html;
  89. }
  90. }
  91. class _MDLink extends _MDSpan {
  92. /** @var {String} */
  93. link;
  94. /** @var {String|null} */
  95. target = null;
  96. /** @var {_MDSpan} */
  97. content;
  98. /**
  99. * @param {String} link
  100. * @param {_MDSpan} content
  101. */
  102. constructor(link, content) {
  103. super();
  104. this.link = link;
  105. this.content = content;
  106. }
  107. toHTML(config) {
  108. let escapedLink = this.link.replace('"', '&quot;');
  109. var html = `<a href="${escapedLink}"`;
  110. if (target) {
  111. let escapedTarget = this.target.replace('"', '&quot;');
  112. html += ` target="${escapedTarget}"`;
  113. }
  114. html += '>' + this.content.toHTML(config) + '</a>';
  115. return html;
  116. }
  117. }
  118. class _MDReferencedLink extends _MDLink {
  119. /** @var {String} id */
  120. id;
  121. constructor(id, content) {
  122. super(null, content);
  123. this.id = id;
  124. }
  125. toHTML(config) {
  126. if (this.link) {
  127. return super.toHTML(config);
  128. } else {
  129. let contentHTML = this.content.toHTML(config);
  130. return `[${contentHTML}][${this.id}]`;
  131. }
  132. }
  133. }
  134. class _MDEmphasis extends _MDSpan {
  135. /** @var {_MDSpan} content */
  136. #content;
  137. /**
  138. * @param {_MDSpan} content
  139. */
  140. constructor(content) {
  141. super();
  142. this.#content = content;
  143. }
  144. toHTML(config) {
  145. let contentHTML = this.#content.toHTML(config);
  146. return `<em>${contentHTML}</em>`;
  147. }
  148. }
  149. class _MDStrong extends _MDSpan {
  150. /** @var {_MDSpan} content */
  151. #content;
  152. /**
  153. * @param {_MDSpan} content
  154. */
  155. constructor(content) {
  156. super();
  157. this.#content = content;
  158. }
  159. toHTML(config) {
  160. let contentHTML = this.#content.toHTML(config);
  161. return `<strong>${contentHTML}</strong>`;
  162. }
  163. }
  164. class _MDStrikethrough extends _MDSpan {
  165. /** @var {_MDSpan} content */
  166. #content;
  167. /**
  168. * @param {_MDSpan} content
  169. */
  170. constructor(content) {
  171. super();
  172. this.#content = content;
  173. }
  174. toHTML(config) {
  175. let contentHTML = this.#content.toHTML(config);
  176. return `<strike>${contentHTML}</strike>`;
  177. }
  178. }
  179. class _MDInlineCode extends _MDSpan {
  180. /** @var {_MDSpan} content */
  181. #content;
  182. /**
  183. * @param {_MDSpan} content
  184. */
  185. constructor(content) {
  186. super();
  187. this.#content = content;
  188. }
  189. toHTML(config) {
  190. let contentHTML = this.#content.toHTML(config);
  191. return `<code>${contentHTML}</code>`;
  192. }
  193. }
  194. class _MDImage extends _MDSpan {
  195. /** @var {String} */
  196. source;
  197. /** @var {String|null} */
  198. alt;
  199. /**
  200. * @param {String} source
  201. */
  202. constructor(source, alt) {
  203. super();
  204. this.source = source;
  205. this.alt = alt;
  206. }
  207. toHTML(config) {
  208. let escapedSource = this.source.replace('"', '&quot;');
  209. let html = `<img src="${escapedSource}"`;
  210. if (this.alt) {
  211. let altEscaped = this.alt.replace('"', '&quot');
  212. html += ` alt="${altEscaped}"`;
  213. }
  214. html += '>';
  215. return html;
  216. }
  217. }
  218. class _MDReferencedImage extends _MDImage {
  219. /** @var {String} */
  220. id;
  221. /**
  222. * @param {String} id
  223. */
  224. constructor(id, alt) {
  225. super(null, alt);
  226. this.id = id;
  227. }
  228. toHTML(config) {
  229. if (this.source) {
  230. return super.toHTML(config);
  231. } else {
  232. let altEscaped = this.alt.replace('"', '&quot;');
  233. let idEscaped = this.id.replace('"', '&quot;');
  234. return `![${altEscaped}][${idEscaped}]`;
  235. }
  236. }
  237. }
  238. class _MDFootnoteReference extends _MDSpan {
  239. /** @var {String} */
  240. symbol;
  241. /** @var {Number} */
  242. differentiator = 0;
  243. /**
  244. * @param {String} symbol
  245. */
  246. constructor(symbol) {
  247. super();
  248. this.symbol = symbol;
  249. }
  250. toHTML(config) {
  251. return `<sup id="fnref-${this.symbol}-${this.differentiator}"><a href="#fndef-${this.symbol}">${this.symbol}</a></sup>`;
  252. }
  253. }
  254. class _MDAbbreviationReference extends _MDSpan {
  255. /** @var {_MDSpan} content */
  256. #content;
  257. /** @var {String} definition */
  258. #definition;
  259. /**
  260. * @param {_MDSpan} content
  261. */
  262. constructor(content, definition) {
  263. super();
  264. this.#content = content;
  265. this.#definition = definition;
  266. }
  267. toHTML(config) {
  268. let contentHTML = this.#content.toHTML(config);
  269. let definitionEscaped = this.#definition.replace('"', '&quot;');
  270. return `<abbr title="${definitionEscaped}">${contentHTML}</em>`;
  271. }
  272. }
  273. class _MDBlock {
  274. toHTML(config) {
  275. throw new Error(self.constructor.name + ".toHTML not implemented");
  276. }
  277. /**
  278. * @param {_MDBlock[]} blocks
  279. * @returns {String}
  280. */
  281. static toHTML(blocks, config) {
  282. return blocks.map((block) => block.toHTML(config)).join("\n");
  283. }
  284. }
  285. class _MDMultiBlock extends _MDBlock {
  286. /** @var {_MDBlock[]} */
  287. #blocks;
  288. /**
  289. * @param {_MDBlock[]} blocks
  290. */
  291. constructor(blocks) {
  292. super();
  293. this.#blocks = blocks;
  294. }
  295. toHTML(config) {
  296. return _MDBlock.toHTML(this.#blocks, config);
  297. }
  298. }
  299. class _MDParagraph extends _MDBlock {
  300. /** @var {_MDBlock} */
  301. content;
  302. /**
  303. * @param {_MDBlock} content
  304. */
  305. constructor(content) {
  306. super();
  307. this.content = content;
  308. }
  309. toHTML(config) {
  310. let contentHTML = this.content.toHTML(config);
  311. return `<p>${contentHTML}</p>\n`;
  312. }
  313. }
  314. class _MDHeader extends _MDBlock {
  315. /** @var {number} */
  316. level;
  317. /** @var {_MDBlock} */
  318. content;
  319. /**
  320. * @param {number} level
  321. * @param {_MDBlock} content
  322. */
  323. constructor(level, content) {
  324. super();
  325. this.level = level;
  326. this.content = content;
  327. }
  328. toHTML(config) {
  329. let contentHTML = this.content.toHTML(config);
  330. return `<h${this.level}>${contentHTML}</h${this.level}>\n`;
  331. }
  332. }
  333. class _MDBlockquote extends _MDBlock {
  334. /** @var {_MDBlock[]} */
  335. content;
  336. /**
  337. * @param {_MDBlock[]} content
  338. */
  339. constructor(content) {
  340. super();
  341. this.content = content;
  342. }
  343. toHTML(config) {
  344. let contentHTML = _MDBlock.toHTML(this.content, config);
  345. return `<blockquote>\n${contentHTML}\n</blockquote>`;
  346. }
  347. }
  348. class _MDUnorderedList extends _MDBlock {
  349. /** @var {_MDListItem[]} */
  350. items;
  351. /**
  352. * @param {_MDListItem[]} items
  353. */
  354. constructor(items) {
  355. super();
  356. this.items = items;
  357. }
  358. toHTML(config) {
  359. let contentHTML = _MDBlock.toHTML(this.items);
  360. return `<ul>\n${contentHTML}\n</ul>`;
  361. }
  362. }
  363. class _MDOrderedList extends _MDBlock {
  364. /** @var {_MDListItem[]} */
  365. items;
  366. /**
  367. * @param {_MDListItem[]} items
  368. */
  369. constructor(items) {
  370. super();
  371. this.items = items;
  372. }
  373. toHTML(config) {
  374. let contentHTML = _MDBlock.toHTML(this.items);
  375. return `<ol>\n${contentHTML}\n</ol>`;
  376. }
  377. }
  378. class _MDListItem extends _MDBlock {
  379. /** @var {_MDBlock} */
  380. content;
  381. /**
  382. * @param {_MDBlock} content
  383. */
  384. constructor(content) {
  385. super();
  386. this.content = content;
  387. }
  388. toHTML(config) {
  389. let contentHTML = this.content.toHTML(config);
  390. return `<li>${contentHTML}</li>`;
  391. }
  392. }
  393. class _MDCodeBlock extends _MDBlock {
  394. /** @var {String} */
  395. #code;
  396. /**
  397. * @param {String} code
  398. */
  399. constructor(code) {
  400. super();
  401. this.#code = code;
  402. }
  403. toHTML(config) {
  404. return `<pre><code>${this.#code}</code></pre>`;
  405. }
  406. }
  407. class _MDHorizontalRule extends _MDBlock {
  408. toHTML(config) {
  409. return "<hr>\n";
  410. }
  411. }
  412. class _MDTableCell extends _MDBlock {
  413. /** @var {_MDBlock} */
  414. #content;
  415. /** @var {_MDHAlign|null} */
  416. align = null;
  417. /**
  418. * @param {_MDBlock} content
  419. */
  420. constructor(content) {
  421. super();
  422. this.#content = content;
  423. }
  424. toHTML(config) {
  425. let contentHTML = this.#content.toHTML(config);
  426. let alignAttribute = _MDHAlign.toHTMLAttribute(this.align);
  427. return `<td${alignAttribute}>${contentHTML}</td>`;
  428. }
  429. }
  430. class _MDTableHeaderCell extends _MDTableCell {
  431. toHTML(config) {
  432. let html = super.toHTML(config);
  433. let groups = /^<td(.*)td>$/.exec(html);
  434. return `<th${groups[1]}th>`;
  435. }
  436. }
  437. class _MDTableRow extends _MDBlock {
  438. /** @var {_MDTableCell[]|_MDTableHeaderCell[]} */
  439. #cells;
  440. /**
  441. * @param {_MDTableCell[]|_MDTableHeaderCell[]} cells
  442. */
  443. constructor(cells) {
  444. super();
  445. this.#cells = cells;
  446. }
  447. /**
  448. * @param {_MDHAlign[]} alignments
  449. */
  450. applyAlignments(alignments) {
  451. for (var i = 0; i < this.#cells.length; i++) {
  452. let cell = this.#cells[i];
  453. let align = i < alignments.length ? alignments[i] : null;
  454. cell.align = align;
  455. }
  456. }
  457. toHTML(config) {
  458. let cellsHTML = _MDBlock.toHTML(this.#cells, config);
  459. return `<tr>\n${cellsHTML}\n</tr>`;
  460. }
  461. }
  462. class _MDTable extends _MDBlock {
  463. /** @var {_MDTableRow} */
  464. #headerRow;
  465. /** @var {_MDTableRow[]} */
  466. #bodyRows;
  467. /**
  468. * @param {_MDTableRow} headerRow
  469. * @param {_MDTableRow[]} bodyRows
  470. */
  471. constructor(headerRow, bodyRows) {
  472. super();
  473. this.#headerRow = headerRow;
  474. this.#bodyRows = bodyRows;
  475. }
  476. toHTML(config) {
  477. let headerRowHTML = this.#headerRow.toHTML(config);
  478. let bodyRowsHTML = _MDBlock.toHTML(this.#bodyRows);
  479. return `<table>\n<thead>\n${headerRowHTML}\n</thead>\n<tbody>\n${bodyRowsHTML}\n</tbody>\n</table>`;
  480. }
  481. }
  482. class _MDDefinitionList extends _MDBlock {
  483. /** @var {_MDBlock[]} */
  484. #content;
  485. /**
  486. * @param {_MDBlock[]} content
  487. */
  488. constructor(content) {
  489. super();
  490. this.#content = content;
  491. }
  492. toHTML(config) {
  493. let contentHTML = _MDBlock.toHTML(this.#content);
  494. return `<dl>\n${contentHTML}\n</dl>`;
  495. }
  496. }
  497. class _MDDefinitionTerm extends _MDBlock {
  498. /** @var {_MDBlock} */
  499. #content;
  500. /**
  501. * @param {_MDBlock} content
  502. */
  503. constructor(content) {
  504. super();
  505. this.#content = content;
  506. }
  507. toHTML(config) {
  508. let contentHTML = this.#content.toHTML(config);
  509. return `<dt>${contentHTML}</dt>`;
  510. }
  511. }
  512. class _MDDefinitionDefinition extends _MDBlock {
  513. /** @var {_MDBlock} */
  514. #content;
  515. /**
  516. * @param {_MDBlock} content
  517. */
  518. constructor(content) {
  519. super();
  520. this.#content = content;
  521. }
  522. toHTML(config) {
  523. let contentHTML = this.#content.toHTML(config);
  524. return `<dd>${contentHTML}</dd>`;
  525. }
  526. }
  527. class _MDFootnoteContent extends _MDBlock {
  528. /** @var {String} */
  529. #id;
  530. /** @var {_MDBlock} */
  531. #content;
  532. /**
  533. * @param {String} id
  534. * @param {_MDBlock} content
  535. */
  536. constructor(id, content) {
  537. super();
  538. this.#id = id;
  539. this.#content = content;
  540. }
  541. toHTML(config) {
  542. // TODO: Forward and back links
  543. // TODO: Deferring footnotes to end of document
  544. //<ol>
  545. //<li id="fn:1" role="doc-endnote">
  546. //<p>Footnote&nbsp;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
  547. //</li>
  548. //</ol>
  549. return '';
  550. }
  551. }
  552. class _MDAbbreviationOccurrence extends _MDBlock {
  553. /** @var {String} */
  554. #label;
  555. /** @var {String} */
  556. #definition;
  557. /**
  558. * @param {String} label
  559. * @param {String} definition
  560. */
  561. constructor(label, definition) {
  562. super();
  563. this.#label = label;
  564. this.#definition = definition;
  565. }
  566. toHTML(config) {
  567. return `<abbr title="${this.#definition.replace('"', '&quot;')}">${this.#label}</abbr>`;
  568. }
  569. }
  570. class _MDInline extends _MDBlock {
  571. /** @var {String} */
  572. #raw;
  573. /**
  574. * @param {String} raw
  575. */
  576. constructor(raw) {
  577. super();
  578. this.#raw = raw;
  579. }
  580. toHTML(config) {
  581. return this.#raw;
  582. }
  583. }
  584. // Blocks that immediately start a new block
  585. // - Headers
  586. // - Blockquote
  587. // - Code block ```\ncode\n```
  588. // Blocks that need blank line first
  589. // - HR --- - - - *** * * * * * *
  590. // - Lists
  591. // - Table
  592. // - Code block [4+spaces]code
  593. // - Definition list term\n: definition\n: alternate def
  594. // Unknown blocks
  595. // - Footnotes some text[^1] [^1]: first footnote content
  596. // - Abbreviations *[HTML]: Hyper Text
  597. // Inline styles
  598. // - Links
  599. // - Italic
  600. // - Bold
  601. // - `code`
  602. // - Strikethrough
  603. // - Images ![alt text](url){.cssclass}
  604. // - Literals \*
  605. class _MDState {
  606. /** @var {String[]} */
  607. lines = [];
  608. /** @var {object} */
  609. abbreviations = {};
  610. /** @var {object} */
  611. footnotes = {};
  612. /** @var {number} */
  613. p = 0;
  614. copy() {
  615. let cp = new _MDState();
  616. cp.abbreviations = this.abbreviations;
  617. cp.footnotes = this.footnotes;
  618. cp.p = this.p;
  619. return cp;
  620. }
  621. /** @param {_MDState} other */
  622. apply(other) {
  623. this.abbreviations = other.abbreviations;
  624. this.footnotes = other.footnotes;
  625. this.p = other.p;
  626. }
  627. hasLines(minCount, p=-1) {
  628. let relativeTo = (p < 0) ? this.p : p;
  629. return relativeTo + minCount <= this.lines.length;
  630. }
  631. }
  632. class MDConfig {
  633. }
  634. class Markdown {
  635. /**
  636. * @param {String} line
  637. */
  638. static #stripIndent(line, count=1) {
  639. let regex = new RegExp(`^(: {1,4}|\\t){${count}}`);
  640. return line.replace(regex, '');
  641. }
  642. /**
  643. * @param {String} line
  644. * @param {Boolean} fullIndentsOnly
  645. * @returns {Number} indent count
  646. */
  647. static #countIndents(line, fullIndentsOnly=false) {
  648. var count = 0;
  649. var lastLine = line;
  650. while (line.length > 0) {
  651. line = (fullIndentsOnly)
  652. ? line.replace(/^(?: {4}|\t)/, '')
  653. : line.replace(/^(?: {1,4}|\t)/, '');
  654. if (line != lastLine) {
  655. count++;
  656. } else {
  657. break;
  658. }
  659. lastLine = line;
  660. }
  661. return count;
  662. }
  663. /**
  664. * @param {_MDState} state
  665. * @returns {_MDBlock[]}
  666. */
  667. static #readBlocks(state) {
  668. var blocks = [];
  669. while (state.hasLines(1)) {
  670. let block = this.#readNextBlock(state);
  671. if (block) {
  672. blocks.push(block);
  673. } else {
  674. break;
  675. }
  676. }
  677. return blocks;
  678. }
  679. /**
  680. * @param {_MDState} state
  681. * @returns {_MDBlock}
  682. */
  683. static #readNextBlock(state) {
  684. while (state.hasLines(1) && state.lines[state.p].trim().length == 0) {
  685. console.info("Skipping blank line " + state.p);
  686. state.p++;
  687. }
  688. var block;
  689. block = this.#readUnderlineHeader(state); if (block) return block;
  690. block = this.#readHashHeader(state); if (block) return block;
  691. block = this.#readBlockQuote(state); if (block) return block;
  692. block = this.#readUnorderedList(state); if (block) return block;
  693. block = this.#readOrderedList(state); if (block) return block;
  694. block = this.#readFencedCodeBlock(state); if (block) return block;
  695. block = this.#readIndentedCodeBlock(state); if (block) return block;
  696. block = this.#readHorizontalRule(state); if (block) return block;
  697. block = this.#readTable(state); if (block) return block;
  698. block = this.#readDefinitionList(state); if (block) return block;
  699. block = this.#readFootnoteDef(state); if (block) return block;
  700. block = this.#readAbbreviationDef(state); if (block) return block;
  701. block = this.#readParagraph(state); if (block) return block;
  702. return null;
  703. }
  704. /**
  705. * @param {_MDState} state
  706. * @returns {_MDBlock|null}
  707. */
  708. static #readInline(state, line) {
  709. return new _MDInline(line);
  710. }
  711. /**
  712. * Reads the contents of something like a list item
  713. * @param {_MDState} state
  714. * @param {number} firstLineStartPos
  715. * @param {RegExp} stopRegex
  716. * @returns {_MDBlock}
  717. */
  718. static #readInteriorContent(state, firstLineStartPos, stopRegex) {
  719. // FIXME: When reading <li> content need to detect nested list without
  720. // a blank line
  721. var p = state.p;
  722. var seenBlankLine = false;
  723. var needsBlocks = false;
  724. var lines = [];
  725. while (p < state.lines.length) {
  726. let line = state.lines[p++];
  727. if (p == state.p + 1) {
  728. line = line.substring(firstLineStartPos);
  729. }
  730. let isBlank = line.trim().length == 0;
  731. let isIndented = /^\s+/.exec(line) !== null;
  732. if (isBlank) {
  733. seenBlankLine = true;
  734. lines.push(line.trim());
  735. } else if (stopRegex && stopRegex.exec(line)) {
  736. p--;
  737. break;
  738. } else if (isIndented) {
  739. if (seenBlankLine) {
  740. needsBlocks = true;
  741. }
  742. lines.push(this.#stripIndent(line));
  743. } else {
  744. if (seenBlankLine) {
  745. p--;
  746. break;
  747. }
  748. lines.push(this.#stripIndent(line));
  749. }
  750. }
  751. while (lines.length > 0 && lines[lines.length - 1].trim().length == 0) {
  752. lines.pop();
  753. }
  754. if (needsBlocks) {
  755. let substate = new _MDState();
  756. substate.lines = lines;
  757. substate.abbreviations = state.abbreviations;
  758. substate.footnotes = state.footnotes;
  759. let blocks = this.#readBlocks(substate);
  760. state.p = p;
  761. return new _MDMultiBlock(blocks);
  762. } else {
  763. state.p = p;
  764. return this.#readInline(state, lines.join("\n"));
  765. }
  766. }
  767. /**
  768. * @param {_MDState} state
  769. * @returns {_MDBlock|null}
  770. */
  771. static #readUnderlineHeader(state) {
  772. var p = state.p;
  773. if (!state.hasLines(2)) return null;
  774. let contentLine = state.lines[p++].trim();
  775. let underLine = state.lines[p++].trim();
  776. if (contentLine == '') return null;
  777. if (/^=+$/.exec(underLine)) {
  778. state.p = p;
  779. return new _MDHeader(1, this.#readInline(state, contentLine));
  780. }
  781. if (/^\-+$/.exec(underLine)) {
  782. state.p = p;
  783. return new _MDHeader(2, this.#readInline(state, contentLine));
  784. }
  785. return null;
  786. }
  787. /**
  788. * @param {_MDState} state
  789. * @returns {_MDBlock|null}
  790. */
  791. static #readHashHeader(state) {
  792. var p = state.p;
  793. var groups = /^(#{1,6})\s*([^#].*)\s*$/.exec(state.lines[p++]);
  794. if (groups === null) return null;
  795. state.p = p;
  796. return new _MDHeader(groups[1].length, this.#readInline(state, groups[2]));
  797. }
  798. /**
  799. * @param {_MDState} state
  800. * @returns {_MDBlock|null}
  801. */
  802. static #readBlockQuote(state) {
  803. var blockquoteLines = [];
  804. var p = state.p;
  805. while (p < state.lines.length) {
  806. let line = state.lines[p++];
  807. if (line.startsWith(">")) {
  808. blockquoteLines.push(line);
  809. } else {
  810. break;
  811. }
  812. }
  813. if (blockquoteLines.length > 0) {
  814. let contentLines = blockquoteLines.map(function(line) {
  815. return line.substring(1).replace(/^ {0,3}\t?/, '');
  816. });
  817. let substate = new _MDState();
  818. substate.lines = contentLines;
  819. substate.abbreviations = state.abbreviations;
  820. substate.footnotes = state.footnotes;
  821. let quotedBlocks = this.#readBlocks(substate);
  822. state.p = p;
  823. return new _MDBlockquote(quotedBlocks);
  824. }
  825. return null;
  826. }
  827. /**
  828. * @param {_MDState} state
  829. * @returns {_MDListItem|null}
  830. */
  831. static #readUnorderedListItem(state) {
  832. var p = state.p;
  833. let line = state.lines[p];
  834. let groups = /^([\*\+\-]\s+)(.*)$/.exec(line);
  835. if (groups === null) return null;
  836. return new _MDListItem(this.#readInteriorContent(state, groups[1].length, /^[\*\+\-]\s+/));
  837. }
  838. /**
  839. * @param {_MDState} state
  840. * @returns {_MDBlock|null}
  841. */
  842. static #readUnorderedList(state) {
  843. var items = [];
  844. var item = null;
  845. do {
  846. item = this.#readUnorderedListItem(state);
  847. if (item) items.push(item);
  848. } while (item);
  849. if (items.length == 0) return null;
  850. return new _MDUnorderedList(items);
  851. }
  852. /**
  853. * @param {_MDState} state
  854. * @returns {_MDListItem|null}
  855. */
  856. static #readOrderedListItem(state) {
  857. var p = state.p;
  858. let line = state.lines[p];
  859. let groups = /^(\d+\.\s+)(.*)$/.exec(line);
  860. if (groups === null) return null;
  861. return new _MDListItem(this.#readInteriorContent(state, groups[1].length, /^\d+\.\s+/));
  862. }
  863. /**
  864. * @param {_MDState} state
  865. * @returns {_MDBlock|null}
  866. */
  867. static #readOrderedList(state) {
  868. var items = [];
  869. var item = null;
  870. do {
  871. item = this.#readOrderedListItem(state);
  872. if (item) items.push(item);
  873. } while (item);
  874. if (items.length == 0) return null;
  875. return new _MDOrderedList(items);
  876. }
  877. /**
  878. * @param {_MDState} state
  879. * @returns {_MDBlock|null}
  880. */
  881. static #readFencedCodeBlock(state) {
  882. var p = state.p;
  883. if (state.lines[p++].trim() != '```') return null;
  884. var codeLines = [];
  885. while (state.hasLines(1, p)) {
  886. let line = state.lines[p++];
  887. if (line.trim() == '```') {
  888. state.p = p;
  889. return new _MDCodeBlock(codeLines.join("\n"));
  890. }
  891. codeLines.push(line);
  892. }
  893. return null;
  894. }
  895. /**
  896. * @param {_MDState} state
  897. * @returns {_MDBlock|null}
  898. */
  899. static #readIndentedCodeBlock(state) {
  900. var p = state.p;
  901. var codeLines = [];
  902. while (state.hasLines(1, p)) {
  903. let line = state.lines[p++];
  904. if (this.#countIndents(line, true) < 1) {
  905. p--;
  906. break;
  907. }
  908. codeLines.push(this.#stripIndent(line));
  909. }
  910. if (codeLines.length == 0) return null;
  911. state.p = p;
  912. return new _MDCodeBlock(codeLines.join("\n"));
  913. }
  914. /**
  915. * @param {_MDState} state
  916. * @returns {_MDBlock|null}
  917. */
  918. static #readHorizontalRule(state) {
  919. var p = state.p;
  920. let line = state.lines[p++];
  921. if (/^\s*(?:\-(?:\s*\-){2,}|\*(?:\s*\*){2,})\s*$/.exec(line)) {
  922. state.p = p;
  923. return new _MDHorizontalRule();
  924. }
  925. return null;
  926. }
  927. /**
  928. * @param {_MDState} state
  929. * @param {Boolean} isHeader
  930. * @return {_MDTableRow|null}
  931. */
  932. static #readTableRow(state, isHeader) {
  933. if (!state.hasLines(1)) return null;
  934. var p = state.p;
  935. let line = state.lines[p++].trim();
  936. if (/.*\|.*/.exec(line) === null) return null;
  937. if (line.startsWith('|')) line = line.substring(1);
  938. if (line.endsWith('|')) line = line.substring(0, line.length - 1);
  939. let cellTokens = line.split('|');
  940. let cells = cellTokens.map(function(token) {
  941. let content = Markdown.#readInline(state, token);
  942. return isHeader ? new _MDTableHeaderCell(content) : new _MDTableCell(content);
  943. });
  944. state.p = p;
  945. return new _MDTableRow(cells);
  946. }
  947. /**
  948. * @param {String} line
  949. * @returns {_MDHAlign[]}
  950. */
  951. static #parseColumnAlignments(line) {
  952. line = line.trim();
  953. if (line.startsWith('|')) line = line.substring(1);
  954. if (line.endsWith('|')) line = line.substring(0, line.length - 1);
  955. return line.split('|').map(function(token) {
  956. token = token.trim();
  957. if (token.startsWith(':')) {
  958. if (token.endsWith(':')) {
  959. return _MDHAlign.Center;
  960. }
  961. return _MDHAlign.Left;
  962. } else if (token.endsWith(':')) {
  963. return _MDHAlign.Right;
  964. }
  965. return null;
  966. });
  967. }
  968. /**
  969. * @param {_MDState} state
  970. * @returns {_MDBlock|null}
  971. */
  972. static #readTable(state) {
  973. if (!state.hasLines(2)) return null;
  974. let startP = state.p;
  975. let headerRow = this.#readTableRow(state, true);
  976. if (headerRow === null) {
  977. state.p = startP;
  978. return null;
  979. }
  980. let dividerLine = state.lines[state.p++];
  981. let dividerGroups = /^\s*[|]?(?:\s*[:]?-+[:]?\s*\|)(?:\s*[:]?-+[:]?\s*)[|]?\s*$/.exec(dividerLine);
  982. if (dividerGroups === null) {
  983. state.p = startP;
  984. return null;
  985. }
  986. let columnAlignments = this.#parseColumnAlignments(dividerLine);
  987. headerRow.applyAlignments(columnAlignments);
  988. var bodyRows = [];
  989. while (state.hasLines(1)) {
  990. let row = this.#readTableRow(state, false);
  991. if (row === null) break;
  992. row.applyAlignments(columnAlignments);
  993. bodyRows.push(row);
  994. }
  995. return new _MDTable(headerRow, bodyRows);
  996. }
  997. /**
  998. * @param {_MDState} state
  999. * @returns {_MDBlock|null}
  1000. */
  1001. static #readDefinitionList(state) {
  1002. // TODO
  1003. return null;
  1004. }
  1005. /**
  1006. * @param {_MDState} state
  1007. * @returns {_MDBlock|null}
  1008. */
  1009. static #readFootnoteDef(state) {
  1010. // TODO
  1011. return null;
  1012. }
  1013. /**
  1014. * @param {_MDState} state
  1015. * @returns {_MDBlock|null}
  1016. */
  1017. static #readAbbreviationDef(state) {
  1018. // TODO
  1019. return null;
  1020. }
  1021. /**
  1022. * @param {_MDState} state
  1023. * @returns {_MDBlock|null}
  1024. */
  1025. static #readParagraph(state) {
  1026. if (!state.hasLines(1)) return null;
  1027. var paragraphLines = [];
  1028. var p = state.p;
  1029. while (p < state.lines.length) {
  1030. let line = state.lines[p++];
  1031. if (line.trim().length == 0) {
  1032. break;
  1033. }
  1034. paragraphLines.push(line);
  1035. }
  1036. if (paragraphLines.length > 0) {
  1037. state.p = p;
  1038. let content = paragraphLines.join("\n");
  1039. return new _MDParagraph(this.#readInline(state, content));
  1040. }
  1041. return null;
  1042. }
  1043. /**
  1044. * @param {String} markdown
  1045. * @returns {String} HTML
  1046. */
  1047. static toHTML(markdown, config=new MDConfig()) {
  1048. var state = new _MDState();
  1049. let lines = markdown.replace("\r", "").split("\n");
  1050. state.lines = lines;
  1051. let blocks = this.#readBlocks(state);
  1052. let html = _MDBlock.toHTML(blocks);
  1053. return html;
  1054. }
  1055. }