PHP and Javascript implementations of a simple markdown parser
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

markdown.js 48KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963
  1. // TODO: HTML tags probably need better handling. Consider whether interior of matched tags should be interpreted as markdown.
  2. // TODO: {.class #cssid lang=fr}
  3. // # Header {.class}
  4. // Header {.class}
  5. // ---
  6. // [link](url){.class}
  7. // ``` {.class}
  8. // FIXME: Nested lists not working right
  9. // FIXME: Nested blockquotes require blank line
  10. // FIXME: Ordered list should start with first number
  11. class _MDHAlign {
  12. static Left = new _MDHAlign('Left');
  13. static Center = new _MDHAlign('Center');
  14. static Right = new _MDHAlign('Right');
  15. /** @var {String} */
  16. name;
  17. constructor(name) {
  18. this.name = name;
  19. }
  20. toString() {
  21. return `${this.constructor.name}.${this.name}`;
  22. }
  23. static toHTMLAttribute(align) {
  24. switch (align) {
  25. case _MDHAlign.Left: return ' align="left"';
  26. case _MDHAlign.Center: return ' align="center"';
  27. case _MDHAlign.Right: return ' align="right"';
  28. }
  29. return '';
  30. }
  31. }
  32. class _MDTokenType {
  33. static Text = new _MDTokenType('Text');
  34. static Whitespace = new _MDTokenType('Whitespace');
  35. static Underscore = new _MDTokenType('Underscore');
  36. static Asterisk = new _MDTokenType('Asterisk');
  37. static Slash = new _MDTokenType('Slash');
  38. static Tilde = new _MDTokenType('Tilde');
  39. static Bang = new _MDTokenType('Bang');
  40. static Backtick = new _MDTokenType('Backtick');
  41. static Label = new _MDTokenType('Label'); // content=label
  42. static URL = new _MDTokenType('URL'); // content=URL, extra=title
  43. static Email = new _MDTokenType('Email'); // content=email address, extra=title
  44. static SimpleLink = new _MDTokenType('SimpleLink'); // content=URL
  45. static SimpleEmail = new _MDTokenType('SimpleEmail'); // content=email address
  46. static Footnote = new _MDTokenType('Footnote'); // content=symbol
  47. static HTMLTag = new _MDTokenType('HTMLTag'); // content=tag string, tag=_MDHTMLTag
  48. static META_AnyNonWhitespace = new _MDTokenType('METAAnyNonWhitespace');
  49. /** @var {String} */
  50. name;
  51. constructor(name) {
  52. this.name = name;
  53. }
  54. toString() {
  55. return `${this.constructor.name}.${this.name}`;
  56. }
  57. }
  58. class _MDToken {
  59. /**
  60. * The original token string.
  61. * @var {String}
  62. */
  63. original;
  64. /** @var {_MDTokenType} */
  65. type;
  66. /** @var {String|null} */
  67. content;
  68. /** @var {String|null} */
  69. extra;
  70. /** @var {_MDHTMLTag|null} */
  71. tag;
  72. /**
  73. * @param {String} original
  74. * @param {_MDTokenType} type
  75. * @param {String|null} content
  76. * @param {String|null} extra
  77. * @param {_MDHTMLTag|null} tag
  78. */
  79. constructor(original, type, content=null, extra=null, tag=null) {
  80. this.original = original;
  81. this.type = type;
  82. this.content = content;
  83. this.extra = extra;
  84. this.tag = tag;
  85. }
  86. }
  87. // -- Spans -----------------------------------------------------------------
  88. class _MDSpan {
  89. /** @var {String[]} */
  90. cssClasses = [];
  91. /** @var {String|null} */
  92. id = null;
  93. /** @var {Object} */
  94. attributes = {};
  95. /**
  96. * @param {_MDState} state
  97. * @returns {String} HTML
  98. */
  99. toHTML(state) {
  100. throw new Error(self.constructor.name + ".toHTML not implemented");
  101. }
  102. htmlAttributes() {
  103. var html = '';
  104. if (this.cssClasses.length > 0) {
  105. html += ` class="${this.cssClasses.join(' ')}"`;
  106. }
  107. if (this.id !== null) {
  108. html += ` id="${this.id}"`;
  109. }
  110. for (const name in this.attributes) {
  111. let value = this.attributes[name];
  112. html += ` ${name}="${value.replace('"', '"')}"`;
  113. }
  114. return html;
  115. }
  116. /**
  117. * @param {_MDSpan[]} spans
  118. * @param {_MDState} state
  119. */
  120. static toHTML(spans, state) {
  121. return spans.map((span) => span.toHTML(state)).join("");
  122. }
  123. }
  124. class _MDMultiSpan extends _MDSpan {
  125. /** @var {_MDSpan[]} */
  126. content;
  127. /**
  128. * @param {_MDSpan[]} content
  129. */
  130. constructor(content) {
  131. super();
  132. this.content = content;
  133. }
  134. toHTML(state) {
  135. return _MDSpan.toHTML(this.content, state);
  136. }
  137. }
  138. class _MDTextSpan extends _MDSpan {
  139. /** @param {String} text */
  140. text;
  141. /**
  142. * @param {String} text
  143. */
  144. constructor(text) {
  145. super();
  146. this.text = text;
  147. }
  148. toHTML(state) {
  149. let html = this.text.replace('<', '&lt;');
  150. let abbrevs = state.abbreviations;
  151. let regexes = state.abbreviationRegexes;
  152. for (const abbrev in abbrevs) {
  153. let def = abbrevs[abbrev];
  154. let regex = regexes[abbrev];
  155. let escapedDef = def.replace('"', '&quot;');
  156. html = html.replace(regex, `<abbr title="${escapedDef}">$1</abbr>`);
  157. }
  158. return html;
  159. }
  160. }
  161. class _MDHTMLSpan extends _MDSpan {
  162. /** @param {String} html */
  163. html;
  164. /**
  165. * @param {String} html
  166. */
  167. constructor(html) {
  168. super();
  169. this.html = html;
  170. }
  171. toHTML(state) {
  172. return this.html;
  173. }
  174. }
  175. class _MDLinkSpan extends _MDSpan {
  176. /** @var {String} */
  177. link;
  178. /** @var {String|null} */
  179. target = null;
  180. /** @var {_MDSpan} */
  181. content;
  182. /** @var {String|null} */
  183. title = null;
  184. /**
  185. * @param {String} link
  186. * @param {_MDSpan} content
  187. */
  188. constructor(link, content, title=null) {
  189. super();
  190. this.link = link;
  191. this.content = content;
  192. this.title = title;
  193. }
  194. toHTML(state) {
  195. let escapedLink = this.link.replace('"', '&quot;');
  196. var html = `<a href="${escapedLink}"`;
  197. if (this.target) {
  198. let escapedTarget = this.target.replace('"', '&quot;');
  199. html += ` target="${escapedTarget}"`;
  200. }
  201. if (this.title) {
  202. html += ` title="${this.title.replace('"', '&quot;')}"`;
  203. }
  204. html += this.htmlAttributes();
  205. html += '>' + this.content.toHTML(state) + '</a>';
  206. return html;
  207. }
  208. }
  209. class _MDReferencedLinkSpan extends _MDLinkSpan {
  210. /** @var {String} id */
  211. id;
  212. constructor(id, content) {
  213. super(null, content);
  214. this.id = id;
  215. }
  216. /**
  217. * @param {_MDState} state
  218. */
  219. toHTML(state) {
  220. if (!this.link) {
  221. let url = state.urls[this.id.toLowerCase()];
  222. let title = state.urlTitles[this.id.toLowerCase()];
  223. this.link = url;
  224. this.title = title || this.title;
  225. }
  226. if (this.link) {
  227. return super.toHTML(state);
  228. } else {
  229. let contentHTML = this.content.toHTML(state);
  230. return `[${contentHTML}][${this.id}]`;
  231. }
  232. }
  233. }
  234. class _MDEmphasisSpan extends _MDSpan {
  235. /** @var {_MDSpan} */
  236. #content;
  237. /**
  238. * @param {_MDSpan} content
  239. */
  240. constructor(content) {
  241. super();
  242. this.#content = content;
  243. }
  244. toHTML(state) {
  245. let contentHTML = this.#content.toHTML(state);
  246. return `<em${this.htmlAttributes()}>${contentHTML}</em>`;
  247. }
  248. }
  249. class _MDStrongSpan extends _MDSpan {
  250. /** @var {_MDSpan} content */
  251. #content;
  252. /**
  253. * @param {_MDSpan} content
  254. */
  255. constructor(content) {
  256. super();
  257. this.#content = content;
  258. }
  259. toHTML(state) {
  260. let contentHTML = this.#content.toHTML(state);
  261. return `<strong${this.htmlAttributes()}>${contentHTML}</strong>`;
  262. }
  263. }
  264. class _MDStrikethroughSpan extends _MDSpan {
  265. /** @var {_MDSpan} content */
  266. #content;
  267. /**
  268. * @param {_MDSpan} content
  269. */
  270. constructor(content) {
  271. super();
  272. this.#content = content;
  273. }
  274. toHTML(state) {
  275. let contentHTML = this.#content.toHTML(state);
  276. return `<strike${this.htmlAttributes()}>${contentHTML}</strike>`;
  277. }
  278. }
  279. class _MDCodeSpan extends _MDSpan {
  280. /** @var {_MDSpan} content */
  281. #content;
  282. /**
  283. * @param {_MDSpan} content
  284. */
  285. constructor(content) {
  286. super();
  287. this.#content = content;
  288. }
  289. toHTML(state) {
  290. let contentHTML = this.#content.toHTML(state);
  291. return `<code${this.htmlAttributes()}>${contentHTML}</code>`;
  292. }
  293. }
  294. class _MDImageSpan extends _MDSpan {
  295. /** @var {String} */
  296. source;
  297. /** @var {String|null} */
  298. alt;
  299. /** @var {String|null} */
  300. title;
  301. /**
  302. * @param {String} source
  303. */
  304. constructor(source, alt, title=null) {
  305. super();
  306. this.source = source;
  307. this.alt = alt;
  308. this.title = title;
  309. }
  310. toHTML(state) {
  311. let escapedSource = this.source.replace('"', '&quot;');
  312. let html = `<img src="${escapedSource}"`;
  313. if (this.alt) {
  314. let altEscaped = this.alt.replace('"', '&quot');
  315. html += ` alt="${altEscaped}"`;
  316. }
  317. if (this.title) {
  318. let titleEscaped = this.title.replace('"', '&quot;');
  319. html += ` title="${titleEscaped}"`;
  320. }
  321. html += this.htmlAttributes();
  322. html += '>';
  323. return html;
  324. }
  325. }
  326. class _MDReferencedImageSpan extends _MDImageSpan {
  327. /** @var {String} */
  328. id;
  329. /**
  330. * @param {String} id
  331. */
  332. constructor(id, alt) {
  333. super(null, alt);
  334. this.id = id;
  335. }
  336. toHTML(state) {
  337. if (!this.source) {
  338. let url = state.urls[this.id.toLowerCase()];
  339. let title = state.urlTitles[this.id.toLowerCase()];
  340. this.source = url;
  341. this.title = title || this.title;
  342. }
  343. if (this.source) {
  344. return super.toHTML(state);
  345. } else {
  346. let altEscaped = this.alt.replace('"', '&quot;');
  347. let idEscaped = this.id.replace('"', '&quot;');
  348. return `![${altEscaped}][${idEscaped}]`;
  349. }
  350. }
  351. }
  352. class _MDFootnoteReferenceSpan extends _MDSpan {
  353. /** @var {String} */
  354. symbol;
  355. /**
  356. * @param {String} symbol
  357. */
  358. constructor(symbol) {
  359. super();
  360. this.symbol = symbol;
  361. }
  362. toHTML(state) {
  363. return `<!--FNREF:{${this.symbol}}-->`;
  364. }
  365. }
  366. // -- Blocks ----------------------------------------------------------------
  367. class _MDBlock {
  368. /** @var {String[]} */
  369. cssClasses = [];
  370. /** @var {String|null} */
  371. id = null;
  372. /** @var {Object} */
  373. attributes = {};
  374. /**
  375. * @param {_MDState} state
  376. */
  377. toHTML(state) {
  378. throw new Error(self.constructor.name + ".toHTML not implemented");
  379. }
  380. htmlAttributes() {
  381. var html = '';
  382. if (this.cssClasses.length > 0) {
  383. html += ` class="${this.cssClasses.join(' ')}"`;
  384. }
  385. if (this.id !== null) {
  386. html += ` id="${this.id}"`;
  387. }
  388. for (const name in this.attributes) {
  389. let value = this.attributes[name];
  390. html += ` ${name}="${value.replace('"', '&quot;')}"`;
  391. }
  392. return html;
  393. }
  394. /**
  395. * @param {_MDBlock[]} blocks
  396. * @param {_MDState} state
  397. * @returns {String}
  398. */
  399. static toHTML(blocks, state) {
  400. return blocks.map((block) => block.toHTML(state)).join("\n");
  401. }
  402. }
  403. class _MDMultiBlock extends _MDBlock {
  404. /** @var {_MDBlock[]} */
  405. #blocks;
  406. /**
  407. * @param {_MDBlock[]} blocks
  408. */
  409. constructor(blocks) {
  410. super();
  411. this.#blocks = blocks;
  412. }
  413. toHTML(state) {
  414. return _MDBlock.toHTML(this.#blocks, state);
  415. }
  416. }
  417. class _MDParagraphBlock extends _MDBlock {
  418. /** @var {_MDBlock} */
  419. content;
  420. /**
  421. * @param {_MDBlock} content
  422. */
  423. constructor(content) {
  424. super();
  425. this.content = content;
  426. }
  427. toHTML(state) {
  428. let contentHTML = this.content.toHTML(state);
  429. return `<p${this.htmlAttributes()}>${contentHTML}</p>\n`;
  430. }
  431. }
  432. class _MDHeaderBlock extends _MDBlock {
  433. /** @var {number} */
  434. level;
  435. /** @var {_MDBlock} */
  436. content;
  437. /**
  438. * @param {number} level
  439. * @param {_MDBlock} content
  440. */
  441. constructor(level, content) {
  442. super();
  443. this.level = level;
  444. this.content = content;
  445. }
  446. toHTML(state) {
  447. let contentHTML = this.content.toHTML(state);
  448. return `<h${this.level}${this.htmlAttributes()}>${contentHTML}</h${this.level}>\n`;
  449. }
  450. }
  451. class _MDBlockquoteBlock extends _MDBlock {
  452. /** @var {_MDBlock[]} */
  453. content;
  454. /**
  455. * @param {_MDBlock[]} content
  456. */
  457. constructor(content) {
  458. super();
  459. this.content = content;
  460. }
  461. toHTML(state) {
  462. let contentHTML = _MDBlock.toHTML(this.content, state);
  463. return `<blockquote${this.htmlAttributes()}>\n${contentHTML}\n</blockquote>`;
  464. }
  465. }
  466. class _MDUnorderedListBlock extends _MDBlock {
  467. /** @var {_MDListItemBlock[]} */
  468. items;
  469. /**
  470. * @param {_MDListItemBlock[]} items
  471. */
  472. constructor(items) {
  473. super();
  474. this.items = items;
  475. }
  476. toHTML(state) {
  477. let contentHTML = _MDBlock.toHTML(this.items, state);
  478. return `<ul${this.htmlAttributes()}>\n${contentHTML}\n</ul>`;
  479. }
  480. }
  481. class _MDOrderedListBlock extends _MDBlock {
  482. /** @var {_MDListItemBlock[]} */
  483. items;
  484. /**
  485. * @param {_MDListItemBlock[]} items
  486. */
  487. constructor(items) {
  488. super();
  489. this.items = items;
  490. }
  491. toHTML(state) {
  492. let contentHTML = _MDBlock.toHTML(this.items, state);
  493. return `<ol${this.htmlAttributes()}>\n${contentHTML}\n</ol>`;
  494. }
  495. }
  496. class _MDListItemBlock extends _MDBlock {
  497. /** @var {_MDBlock} */
  498. content;
  499. /**
  500. * @param {_MDBlock} content
  501. */
  502. constructor(content) {
  503. super();
  504. this.content = content;
  505. }
  506. toHTML(state) {
  507. let contentHTML = this.content.toHTML(state);
  508. return `<li${this.htmlAttributes()}>${contentHTML}</li>`;
  509. }
  510. }
  511. class _MDCodeBlock extends _MDBlock {
  512. /** @var {String} */
  513. #code;
  514. /**
  515. * @param {String} code
  516. */
  517. constructor(code) {
  518. super();
  519. this.#code = code;
  520. }
  521. toHTML(state) {
  522. return `<pre${this.htmlAttributes()}><code>${this.#code}</code></pre>`;
  523. }
  524. }
  525. class _MDHorizontalRuleBlock extends _MDBlock {
  526. toHTML(state) {
  527. return `<hr${this.htmlAttributes()}>\n`;
  528. }
  529. }
  530. class _MDTableCellBlock extends _MDBlock {
  531. /** @var {_MDBlock} */
  532. #content;
  533. /** @var {_MDHAlign|null} */
  534. align = null;
  535. /**
  536. * @param {_MDBlock} content
  537. */
  538. constructor(content) {
  539. super();
  540. this.#content = content;
  541. }
  542. htmlAttributes() {
  543. var html = super.htmlAttributes();
  544. html += _MDHAlign.toHTMLAttribute(this.align);
  545. return html;
  546. }
  547. toHTML(state) {
  548. let contentHTML = this.#content.toHTML(state);
  549. return `<td${this.htmlAttributes()}>${contentHTML}</td>`;
  550. }
  551. }
  552. class _MDTableHeaderCellBlock extends _MDTableCellBlock {
  553. toHTML(state) {
  554. let html = super.toHTML(state);
  555. let groups = /^<td(.*)td>$/.exec(html);
  556. return `<th${groups[1]}th>`;
  557. }
  558. }
  559. class _MDTableRowBlock extends _MDBlock {
  560. /** @var {_MDTableCellBlock[]|_MDTableHeaderCellBlock[]} */
  561. #cells;
  562. /**
  563. * @param {_MDTableCellBlock[]|_MDTableHeaderCellBlock[]} cells
  564. */
  565. constructor(cells) {
  566. super();
  567. this.#cells = cells;
  568. }
  569. /**
  570. * @param {_MDHAlign[]} alignments
  571. */
  572. applyAlignments(alignments) {
  573. for (var i = 0; i < this.#cells.length; i++) {
  574. let cell = this.#cells[i];
  575. let align = i < alignments.length ? alignments[i] : null;
  576. cell.align = align;
  577. }
  578. }
  579. toHTML(state) {
  580. let cellsHTML = _MDBlock.toHTML(this.#cells, state);
  581. return `<tr${this.htmlAttributes()}>\n${cellsHTML}\n</tr>`;
  582. }
  583. }
  584. class _MDTableBlock extends _MDBlock {
  585. /** @var {_MDTableRowBlock} */
  586. #headerRow;
  587. /** @var {_MDTableRowBlock[]} */
  588. #bodyRows;
  589. /**
  590. * @param {_MDTableRowBlock} headerRow
  591. * @param {_MDTableRowBlock[]} bodyRows
  592. */
  593. constructor(headerRow, bodyRows) {
  594. super();
  595. this.#headerRow = headerRow;
  596. this.#bodyRows = bodyRows;
  597. }
  598. toHTML(state) {
  599. let headerRowHTML = this.#headerRow.toHTML(state);
  600. let bodyRowsHTML = _MDBlock.toHTML(this.#bodyRows, state);
  601. return `<table${this.htmlAttributes()}>\n<thead>\n${headerRowHTML}\n</thead>\n<tbody>\n${bodyRowsHTML}\n</tbody>\n</table>`;
  602. }
  603. }
  604. class _MDDefinitionListBlock extends _MDBlock {
  605. /** @var {_MDBlock[]} */
  606. #content;
  607. /**
  608. * @param {_MDBlock[]} content
  609. */
  610. constructor(content) {
  611. super();
  612. this.#content = content;
  613. }
  614. toHTML(state) {
  615. let contentHTML = _MDBlock.toHTML(this.#content, state);
  616. return `<dl${this.htmlAttributes()}>\n${contentHTML}\n</dl>`;
  617. }
  618. }
  619. class _MDDefinitionTermBlock extends _MDBlock {
  620. /** @var {_MDBlock} */
  621. #content;
  622. /**
  623. * @param {_MDBlock} content
  624. */
  625. constructor(content) {
  626. super();
  627. this.#content = content;
  628. }
  629. toHTML(state) {
  630. let contentHTML = this.#content.toHTML(state);
  631. return `<dt${this.htmlAttributes()}>${contentHTML}</dt>`;
  632. }
  633. }
  634. class _MDDefinitionDefinitionBlock extends _MDBlock {
  635. /** @var {_MDBlock} */
  636. #content;
  637. /**
  638. * @param {_MDBlock} content
  639. */
  640. constructor(content) {
  641. super();
  642. this.#content = content;
  643. }
  644. toHTML(state) {
  645. let contentHTML = this.#content.toHTML(state);
  646. return `<dd${this.htmlAttributes()}>${contentHTML}</dd>`;
  647. }
  648. }
  649. class _MDInlineBlock extends _MDBlock {
  650. /** @var {_MDSpan[]} */
  651. #content;
  652. /**
  653. * @param {_MDSpan[]} content
  654. */
  655. constructor(content) {
  656. super();
  657. this.#content = content;
  658. }
  659. toHTML(state) {
  660. return _MDSpan.toHTML(this.#content, state);
  661. }
  662. }
  663. class _MDHTMLTag {
  664. /** @var {String} */
  665. fullTag;
  666. /** @var {String} */
  667. tagName;
  668. /** @var {Boolean} */
  669. isCloser;
  670. /** @var {Object} */
  671. attributes;
  672. /**
  673. * @param {String} fullTag
  674. * @param {String} tagName
  675. * @param {Boolean} isCloser
  676. * @param {Object} attributes
  677. */
  678. constructor(fullTag, tagName, isCloser, attributes) {
  679. this.fullTag = fullTag;
  680. this.tagName = tagName;
  681. this.isCloser = isCloser;
  682. this.attributes = attributes;
  683. }
  684. }
  685. class _MDState {
  686. /** @var {String[]} */
  687. lines = [];
  688. /** @var {Object} */
  689. #abbreviations = {};
  690. /** @var {Object} */
  691. #abbreviationRegexes = {};
  692. /** @var {Object} */
  693. #footnotes = {};
  694. /** @var {Object} */
  695. #urlDefinitions = {};
  696. /** @var {Object} */
  697. #urlTitles = {};
  698. /** @var {number} */
  699. p = 0;
  700. /** @var {_MDState|null} */
  701. #parent = null;
  702. /** @var {Object} */
  703. get abbreviations() {
  704. return (this.#parent) ? this.#parent.abbreviations : this.#abbreviations;
  705. }
  706. /** @var {Object} */
  707. get abbreviationRegexes() {
  708. return (this.#parent) ? this.#parent.abbreviationRegexes : this.#abbreviationRegexes;
  709. }
  710. /** @var {Object} */
  711. get footnotes() {
  712. return (this.#parent) ? this.#parent.footnotes : this.#footnotes;
  713. }
  714. /** @var {Object} */
  715. get urls() {
  716. return (this.#parent) ? this.#parent.urls : this.#urlDefinitions;
  717. }
  718. /** @var {Object} */
  719. get urlTitles() {
  720. return (this.#parent) ? this.#parent.urlTitles : this.#urlTitles;
  721. }
  722. /**
  723. * @param {String[]} lines
  724. */
  725. copy(lines) {
  726. let cp = new _MDState();
  727. cp.#parent = this;
  728. cp.lines = lines;
  729. cp.p = 0;
  730. return cp;
  731. }
  732. /**
  733. * @param {String} abbreviation
  734. * @param {String} definition
  735. */
  736. defineAbbreviation(abbreviation, definition) {
  737. if (this.#parent) {
  738. this.#parent.defineAbbreviation(abbreviation, definition);
  739. return;
  740. }
  741. this.#abbreviations[abbreviation] = definition;
  742. let regex = new RegExp("\\b(" + abbreviation + ")\\b", "ig");
  743. this.#abbreviationRegexes[abbreviation] = regex;
  744. }
  745. /**
  746. * @param {String} symbol
  747. * @param {_MDBlock} footnote
  748. */
  749. defineFootnote(symbol, footnote) {
  750. if (this.#parent) {
  751. this.#parent.defineFootnote(symbol, footnote);
  752. } else {
  753. this.#footnotes[symbol] = footnote;
  754. }
  755. }
  756. defineURL(symbol, url, title=null) {
  757. if (this.#parent) {
  758. this.#parent.defineURL(symbol, url, title);
  759. } else {
  760. this.#urlDefinitions[symbol.toLowerCase()] = url;
  761. if (title !== null) {
  762. this.#urlTitles[symbol.toLowerCase()] = title;
  763. }
  764. }
  765. }
  766. hasLines(minCount, p=-1) {
  767. let relativeTo = (p < 0) ? this.p : p;
  768. return relativeTo + minCount <= this.lines.length;
  769. }
  770. }
  771. class Markdown {
  772. /**
  773. * @param {String} line
  774. */
  775. static #stripIndent(line, count=1) {
  776. let regex = new RegExp(`^(: {1,4}|\\t){${count}}`);
  777. return line.replace(regex, '');
  778. }
  779. /**
  780. * @param {String} line
  781. * @param {Boolean} fullIndentsOnly
  782. * @returns {Number} indent count
  783. */
  784. static #countIndents(line, fullIndentsOnly=false) {
  785. var count = 0;
  786. var lastLine = line;
  787. while (line.length > 0) {
  788. line = (fullIndentsOnly)
  789. ? line.replace(/^(?: {4}|\t)/, '')
  790. : line.replace(/^(?: {1,4}|\t)/, '');
  791. if (line != lastLine) {
  792. count++;
  793. } else {
  794. break;
  795. }
  796. lastLine = line;
  797. }
  798. return count;
  799. }
  800. /**
  801. * @param {_MDState} state
  802. * @returns {_MDBlock[]}
  803. */
  804. static #readBlocks(state) {
  805. var blocks = [];
  806. while (state.hasLines(1)) {
  807. let block = this.#readNextBlock(state);
  808. if (block) {
  809. blocks.push(block);
  810. } else {
  811. break;
  812. }
  813. }
  814. return blocks;
  815. }
  816. /**
  817. * @param {_MDState} state
  818. * @returns {_MDBlock}
  819. */
  820. static #readNextBlock(state) {
  821. while (state.hasLines(1) && state.lines[state.p].trim().length == 0) {
  822. state.p++;
  823. }
  824. var block;
  825. block = this.#readUnderlineHeader(state); if (block) return block;
  826. block = this.#readHashHeader(state); if (block) return block;
  827. block = this.#readBlockQuote(state); if (block) return block;
  828. block = this.#readUnorderedList(state); if (block) return block;
  829. block = this.#readOrderedList(state); if (block) return block;
  830. block = this.#readFencedCodeBlock(state); if (block) return block;
  831. block = this.#readIndentedCodeBlock(state); if (block) return block;
  832. block = this.#readHorizontalRule(state); if (block) return block;
  833. block = this.#readTable(state); if (block) return block;
  834. block = this.#readFootnoteDef(state); if (block) return block;
  835. block = this.#readAbbreviationDef(state); if (block) return block;
  836. block = this.#readURLDef(state); if (block) return block;
  837. block = this.#readDefinitionList(state); if (block) return block;
  838. block = this.#readParagraph(state); if (block) return block;
  839. return null;
  840. }
  841. static #htmlTagNameFirstRegex = /[a-z]/i;
  842. static #htmlTagNameMedialRegex = /[a-z0-9]/i;
  843. static #htmlAttributeNameFirstRegex = /[a-z]/i;
  844. static #htmlAttributeNameMedialRegex = /[a-z0-9-]/i;
  845. static #whitespaceCharRegex = /\s/;
  846. /**
  847. * @param {String} line
  848. * @returns {_MDHTMLTag|null} HTML tag if possible
  849. */
  850. static #htmlTag(line) {
  851. let expectOpenBracket = 0;
  852. let expectCloserOrName = 1;
  853. let expectName = 2;
  854. let expectAttributeNameOrEnd = 3;
  855. let expectEqualsOrAttributeOrEnd = 4;
  856. let expectAttributeValue = 5;
  857. let expectCloseBracket = 6;
  858. var isCloser = false;
  859. var tagName = '';
  860. var attributeName = '';
  861. var attributeValue = '';
  862. var attributeQuote = null;
  863. var attributes = {};
  864. var fullTag = null;
  865. let endAttribute = function() {
  866. if (attributeName.length > 0) {
  867. if (attributeValue.length > 0 || attributeQuote) {
  868. attributes[attributeName] = attributeValue;
  869. } else {
  870. attributes[attributeName] = true;
  871. }
  872. }
  873. attributeName = '';
  874. attributeValue = '';
  875. attributeQuote = null;
  876. };
  877. var expect = expectOpenBracket;
  878. for (var p = 0; p < line.length && fullTag === null; p++) {
  879. let ch = line.substring(p, p + 1);
  880. let isWhitespace = this.#whitespaceCharRegex.exec(ch) !== null;
  881. switch (expect) {
  882. case expectOpenBracket:
  883. if (ch != '<') return null;
  884. expect = expectCloserOrName;
  885. break;
  886. case expectCloserOrName:
  887. if (ch == '/') {
  888. isCloser = true;
  889. } else {
  890. p--;
  891. }
  892. expect = expectName;
  893. break;
  894. case expectName:
  895. if (tagName.length == 0) {
  896. if (this.#htmlTagNameFirstRegex.exec(ch) === null) return null;
  897. tagName += ch;
  898. } else {
  899. if (this.#htmlTagNameMedialRegex.exec(ch)) {
  900. tagName += ch;
  901. } else {
  902. p--;
  903. expect = (isCloser) ? expectCloseBracket : expectAttributeNameOrEnd;
  904. }
  905. }
  906. break;
  907. case expectAttributeNameOrEnd:
  908. if (attributeName.length == 0) {
  909. if (isWhitespace) {
  910. // skip whitespace
  911. } else if (ch == '/') {
  912. expect = expectCloseBracket;
  913. } else if (ch == '>') {
  914. fullTag = line.substring(0, p + 1);
  915. break;
  916. } else if (this.#htmlAttributeNameFirstRegex.exec(ch)) {
  917. attributeName += ch;
  918. } else {
  919. return null;
  920. }
  921. } else if (isWhitespace) {
  922. expect = expectEqualsOrAttributeOrEnd;
  923. } else if (ch == '/') {
  924. endAttribute();
  925. expect = expectCloseBracket;
  926. } else if (ch == '>') {
  927. endAttribute();
  928. fullTag = line.substring(0, p + 1);
  929. break;
  930. } else if (ch == '=') {
  931. expect = expectAttributeValue;
  932. } else if (this.#htmlAttributeNameMedialRegex.exec(ch)) {
  933. attributeName += ch;
  934. } else {
  935. return null;
  936. }
  937. break;
  938. case expectEqualsOrAttributeOrEnd:
  939. if (ch == '=') {
  940. expect = expectAttributeValue;
  941. } else if (isWhitespace) {
  942. // skip whitespace
  943. } else if (ch == '/') {
  944. expect = expectCloseBracket;
  945. } else if (ch == '>') {
  946. fullTag = line.substring(0, p + 1);
  947. break;
  948. } else if (this.#htmlAttributeNameFirstRegex.exec(ch)) {
  949. endAttribute();
  950. expect = expectAttributeNameOrEnd;
  951. p--;
  952. }
  953. break;
  954. case expectAttributeValue:
  955. if (attributeValue.length == 0) {
  956. if (attributeQuote === null) {
  957. if (isWhitespace) {
  958. // skip whitespace
  959. } else if (ch == '"' || ch == "'") {
  960. attributeQuote = ch;
  961. } else {
  962. attributeQuote = ''; // explicitly unquoted
  963. p--;
  964. }
  965. } else {
  966. if (ch === attributeQuote) {
  967. // Empty string
  968. endAttribute();
  969. expect = expectAttributeNameOrEnd;
  970. } else if (attributeQuote === '' && (ch == '/' || ch == '>')) {
  971. return null;
  972. } else {
  973. attributeValue += ch;
  974. }
  975. }
  976. } else {
  977. if (ch === attributeQuote) {
  978. endAttribute();
  979. expect = expectAttributeNameOrEnd;
  980. } else if (attributeQuote === '' && isWhitespace) {
  981. endAttribute();
  982. expect = expectAttributeNameOrEnd;
  983. } else {
  984. attributeValue += ch;
  985. }
  986. }
  987. break;
  988. case expectCloseBracket:
  989. if (isWhitespace) {
  990. // ignore whitespace
  991. } else if (ch == '>') {
  992. fullTag = line.substring(0, p + 1);
  993. break;
  994. }
  995. break;
  996. }
  997. }
  998. if (fullTag === null) return null;
  999. endAttribute();
  1000. return new _MDHTMLTag(fullTag, tagName, isCloser, attributes);
  1001. }
  1002. static #textWhitespaceRegex = /^(\s*)(?:(\S|\S.*\S)(\s*?))?$/; // 1=leading WS, 2=text, 3=trailing WS
  1003. // Modified from https://urlregex.com/ to remove capture groups. Matches fully qualified URLs only.
  1004. static #baseURLRegex = /(?:(?:(?:[a-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[a-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[a-z0-9\.\-]+)(?:(?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/i;
  1005. // Modified from https://emailregex.com/ to remove capture groups.
  1006. static #baseEmailRegex = /(?:(?:[^<>()\[\]\\.,;:\s@"]+(?:\.[^<>()\[\]\\.,;:\s@"]+)*)|(?:".+"))@(?:(?:\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(?:(?:[a-z\-0-9]+\.)+[a-z]{2,}))/i;
  1007. static #footnoteWithTitleRegex = /^\[\^(\d+?)\s+"(.*?)"\]/; // 1=symbol, 2=title
  1008. static #footnoteRegex = /^\[\^(\d+?)\]/; // 1=symbol
  1009. // Note: label contents have to have matching pairs of [] and (). Handles images inside links.
  1010. static #labelRegex = /^\[((?:[^\[\]]*\[[^\[\]]*\][^\[\]]*|[^\(\)]*\([^\(\)]*?\)[^\(\)]*|[^\[\]\(\)]*?)*)\]/; // 1=content
  1011. static #urlWithTitleRegex = /^\((\S+?)\s+"(.*?)"\)/i; // 1=URL, 2=title
  1012. static #urlRegex = /^\((\S+?)\)/i; // 1=URL
  1013. static #emailWithTitleRegex = new RegExp("^\\(\\s*(" + this.#baseEmailRegex.source + ")\\s+\"(.*?)\"\\s*\\)", "i"); // 1=email, 2=title
  1014. static #emailRegex = new RegExp("^\\(\\s*(" + this.#baseEmailRegex.source + ")\\s*\\)", "i"); // 1=email
  1015. static #simpleURLRegex = new RegExp("^<" + this.#baseURLRegex.source + ">", "i"); // 1=URL
  1016. static #simpleEmailRegex = new RegExp("^<" + this.#baseEmailRegex.source + ">", "i"); // 1=email
  1017. /**
  1018. * @param {String} line
  1019. * @returns {_MDToken[]} tokens
  1020. */
  1021. static #tokenize(line) {
  1022. var tokens = [];
  1023. var text = '';
  1024. var expectLiteral = false;
  1025. var groups = null;
  1026. var tag = null;
  1027. const endText = function() {
  1028. if (text.length == 0) return;
  1029. let textGroups = Markdown.#textWhitespaceRegex.exec(text);
  1030. if (textGroups !== null) {
  1031. if (textGroups[1].length > 0) {
  1032. tokens.push(new _MDToken(textGroups[1], _MDTokenType.Whitespace, textGroups[1]));
  1033. }
  1034. if (textGroups[2] !== undefined && textGroups[2].length > 0) {
  1035. tokens.push(new _MDToken(textGroups[2], _MDTokenType.Text, textGroups[2]));
  1036. }
  1037. if (textGroups[3] !== undefined && textGroups[3].length > 0) {
  1038. tokens.push(new _MDToken(textGroups[3], _MDTokenType.Whitespace, textGroups[3]));
  1039. }
  1040. } else {
  1041. tokens.push(new _MDToken(text, _MDTokenType.Text, text));
  1042. }
  1043. text = '';
  1044. }
  1045. for (var p = 0; p < line.length; p++) {
  1046. let ch = line.substring(p, p + 1);
  1047. let remainder = line.substring(p);
  1048. if (expectLiteral) {
  1049. text += ch;
  1050. expectLiteral = false;
  1051. continue;
  1052. }
  1053. if (ch == '\\') {
  1054. expectLiteral = true;
  1055. } else if (ch == '*') {
  1056. endText();
  1057. tokens.push(new _MDToken(ch, _MDTokenType.Asterisk));
  1058. } else if (ch == '_') {
  1059. endText();
  1060. tokens.push(new _MDToken(ch, _MDTokenType.Underscore));
  1061. } else if (ch == '`') {
  1062. endText();
  1063. tokens.push(new _MDToken(ch, _MDTokenType.Backtick));
  1064. } else if (ch == '~') {
  1065. endText();
  1066. tokens.push(new _MDToken(ch, _MDTokenType.Tilde));
  1067. } else if (ch == '!') {
  1068. endText();
  1069. tokens.push(new _MDToken(ch, _MDTokenType.Bang));
  1070. } else if (groups = this.#footnoteWithTitleRegex.exec(remainder)) {
  1071. // Footnote with title [^1 "Foo"]
  1072. endText();
  1073. tokens.push(new _MDToken(groups[0], _MDTokenType.Footnote, groups[1], groups[2]));
  1074. p += groups[0].length - 1;
  1075. } else if (groups = this.#footnoteRegex.exec(remainder)) {
  1076. // Footnote without title [^1]
  1077. endText();
  1078. tokens.push(new _MDToken(groups[0], _MDTokenType.Footnote, groups[1]));
  1079. p += groups[0].length - 1;
  1080. } else if (groups = this.#labelRegex.exec(remainder)) {
  1081. // Label/ref for link/image [Foo]
  1082. endText();
  1083. tokens.push(new _MDToken(groups[0], _MDTokenType.Label, groups[1]));
  1084. p += groups[0].length - 1;
  1085. } else if (groups = this.#urlWithTitleRegex.exec(remainder)) {
  1086. // URL with title (https://foo "Bar")
  1087. endText();
  1088. tokens.push(new _MDToken(groups[0], _MDTokenType.URL, groups[1], groups[2]));
  1089. p += groups[0].length - 1;
  1090. } else if (groups = this.#emailWithTitleRegex.exec(remainder)) {
  1091. // Email address with title (user@example.com "Foo")
  1092. endText();
  1093. tokens.push(new _MDToken(groups[0], _MDTokenType.Email, groups[1], groups[2]));
  1094. p += groups[0].length - 1;
  1095. } else if (groups = this.#urlRegex.exec(remainder)) {
  1096. // URL (https://example.com)
  1097. endText();
  1098. tokens.push(new _MDToken(groups[0], _MDTokenType.URL, groups[1]));
  1099. p += groups[0].length - 1;
  1100. } else if (groups = this.#emailRegex.exec(remainder)) {
  1101. // Email (user@example.com)
  1102. endText();
  1103. tokens.push(new _MDToken(groups[0], _MDTokenType.Email, groups[1]));
  1104. p += groups[0].length - 1;
  1105. } else if (groups = this.#simpleURLRegex.exec(remainder)) {
  1106. // Simple URL <https://example.com>
  1107. endText();
  1108. tokens.push(new _MDToken(groups[0], _MDTokenType.SimpleLink, groups[1]));
  1109. p += groups[0].length - 1;
  1110. } else if (groups = this.#simpleEmailRegex.exec(remainder)) {
  1111. // Simple email <user@example.com>
  1112. endText();
  1113. tokens.push(new _MDToken(groups[0], _MDTokenType.SimpleEmail, groups[1]));
  1114. p += groups[0].length - 1;
  1115. } else if (tag = this.#htmlTag(remainder)) {
  1116. endText();
  1117. tokens.push(new _MDToken(tag.fullTag, _MDTokenType.HTMLTag, tag.fullTag, null, tag));
  1118. p += tag.fullTag.length - 1;
  1119. } else {
  1120. text += ch;
  1121. }
  1122. }
  1123. endText();
  1124. return tokens;
  1125. }
  1126. static #firstTokenIndex(tokens, pattern, startIndex=0) {
  1127. for (var t = startIndex; t < tokens.length; t++) {
  1128. var matchedAll = true;
  1129. for (var p = 0; p < pattern.length; p++) {
  1130. var t0 = t + p;
  1131. if (t0 >= tokens.length) return null;
  1132. let token = tokens[t0];
  1133. let elem = pattern[p];
  1134. if (elem == _MDTokenType.META_AnyNonWhitespace) {
  1135. if (token instanceof _MDToken && token.type == _MDTokenType.Whitespace) {
  1136. matchedAll = false;
  1137. break;
  1138. }
  1139. } else {
  1140. if (!(token instanceof _MDToken) || token.type != elem) {
  1141. matchedAll = false;
  1142. break;
  1143. }
  1144. }
  1145. }
  1146. if (matchedAll) {
  1147. return t;
  1148. }
  1149. }
  1150. return null;
  1151. }
  1152. /**
  1153. * @param {_MDState} state
  1154. * @param {String} line
  1155. * @returns {_MDBlock|null}
  1156. */
  1157. static #readInline(state, line) {
  1158. var tokens = this.#tokenize(line);
  1159. return new _MDInlineBlock(this.#tokensToSpans(tokens, state));
  1160. }
  1161. /**
  1162. * @param {Array} tokens
  1163. * @returns {_MDSpan[]} spans
  1164. */
  1165. static #tokensToSpans(tokens, state) {
  1166. var spans = tokens.slice(0, tokens.length);
  1167. var anyChanges = false;
  1168. var index, index0;
  1169. // First pass - contiguous constructs
  1170. do {
  1171. anyChanges = false;
  1172. // ![alt](image.jpg)
  1173. if ((index = this.#firstTokenIndex(spans, [
  1174. _MDTokenType.Bang,
  1175. _MDTokenType.Label,
  1176. _MDTokenType.URL,
  1177. ])) !== null) {
  1178. let alt = spans[index + 1].content;
  1179. let url = spans[index + 2].content;
  1180. let title = spans[index + 2].extra;
  1181. spans.splice(index, 3, new _MDImageSpan(url, alt, title));
  1182. anyChanges = true;
  1183. }
  1184. // ![alt][ref]
  1185. else if ((index = this.#firstTokenIndex(spans, [
  1186. _MDTokenType.Bang,
  1187. _MDTokenType.Label,
  1188. _MDTokenType.Label,
  1189. ])) !== null) {
  1190. let alt = spans[index + 1].content;
  1191. let ref = spans[index + 2].content;
  1192. spans.splice(index, 3, new _MDReferencedImageSpan(ref, alt));
  1193. anyChanges = true;
  1194. }
  1195. // [text](link.html)
  1196. else if ((index = this.#firstTokenIndex(spans, [
  1197. _MDTokenType.Label,
  1198. _MDTokenType.URL,
  1199. ])) !== null) {
  1200. let text = spans[index + 0].content;
  1201. let url = spans[index + 1].content;
  1202. spans.splice(index, 2, new _MDLinkSpan(url, this.#readInline(state, text)));
  1203. anyChanges = true;
  1204. }
  1205. // [text][ref]
  1206. else if ((index = this.#firstTokenIndex(spans, [
  1207. _MDTokenType.Label,
  1208. _MDTokenType.Label,
  1209. ])) !== null) {
  1210. let text = spans[index + 0].content;
  1211. let ref = spans[index + 1].content;
  1212. spans.splice(index, 2, new _MDReferencedLinkSpan(ref, this.#readInline(state, text)));
  1213. anyChanges = true;
  1214. }
  1215. // [^1]
  1216. else if ((index = this.#firstTokenIndex(spans, [
  1217. _MDTokenType.Footnote,
  1218. ])) !== null) {
  1219. let symbol = spans[index].content;
  1220. spans.splice(index, 1, new _MDFootnoteReferenceSpan(symbol));
  1221. anyChanges = true;
  1222. }
  1223. } while (anyChanges);
  1224. /**
  1225. * @param {_MDTokenType[]} delimiter
  1226. * @param {Set<_MDTokenType>} disallowedInnerTokens
  1227. */
  1228. const matchPair = function(delimiter, disallowedInnerTokens=new Set()) {
  1229. var searchStart = 0;
  1230. var hasNewStart = false;
  1231. do {
  1232. hasNewStart = false;
  1233. let startIndex = Markdown.#firstTokenIndex(spans, delimiter.concat(_MDTokenType.META_AnyNonWhitespace), searchStart);
  1234. if (startIndex === null) return null;
  1235. let endIndex = Markdown.#firstTokenIndex(spans, [_MDTokenType.META_AnyNonWhitespace].concat(delimiter), startIndex + delimiter.length);
  1236. if (endIndex === null) return null;
  1237. let contentTokens = spans.slice(startIndex + delimiter.length, endIndex + 1);
  1238. if (disallowedInnerTokens.size > 0) {
  1239. for (const token of contentTokens) {
  1240. if (token instanceof _MDToken && disallowedInnerTokens.has(token.type)) {
  1241. searchStart = startIndex + 1;
  1242. hasNewStart = true;
  1243. break;
  1244. }
  1245. }
  1246. if (hasNewStart) continue;
  1247. }
  1248. let contentSpans = Markdown.#tokensToSpans(contentTokens, state);
  1249. return {
  1250. startIndex: startIndex,
  1251. toDelete: endIndex - startIndex + delimiter.length + 1,
  1252. content: new _MDMultiSpan(contentSpans),
  1253. };
  1254. } while (hasNewStart);
  1255. return null;
  1256. };
  1257. var spanMatch = null;
  1258. // Second pass - paired constructs. Prioritize pairs with no other paired tokens inside.
  1259. const delimiterTokens = new Set([
  1260. _MDTokenType.Backtick,
  1261. _MDTokenType.Tilde,
  1262. _MDTokenType.Asterisk,
  1263. _MDTokenType.Underscore
  1264. ]);
  1265. for (let disallowed of [ delimiterTokens, new Set() ]) {
  1266. do {
  1267. anyChanges = false;
  1268. // ``code``
  1269. if (spanMatch = matchPair([ _MDTokenType.Backtick, _MDTokenType.Backtick ], disallowed)) {
  1270. spans.splice(spanMatch.startIndex, spanMatch.toDelete, new _MDCodeSpan(spanMatch.content));
  1271. anyChanges = true;
  1272. }
  1273. // ~~strike~~
  1274. else if (spanMatch = matchPair([ _MDTokenType.Tilde, _MDTokenType.Tilde ], disallowed)) {
  1275. spans.splice(spanMatch.startIndex, spanMatch.toDelete, new _MDStrikethroughSpan(spanMatch.content));
  1276. anyChanges = true;
  1277. }
  1278. // **strong** __strong__
  1279. else if (spanMatch = (matchPair([ _MDTokenType.Asterisk, _MDTokenType.Asterisk ], disallowed) ||
  1280. matchPair([ _MDTokenType.Underscore, _MDTokenType.Underscore ], disallowed))) {
  1281. spans.splice(spanMatch.startIndex, spanMatch.toDelete, new _MDStrongSpan(spanMatch.content));
  1282. anyChanges = true;
  1283. }
  1284. // `code`
  1285. if (spanMatch = matchPair([ _MDTokenType.Backtick ], disallowed)) {
  1286. spans.splice(spanMatch.startIndex, spanMatch.toDelete, new _MDCodeSpan(spanMatch.content));
  1287. anyChanges = true;
  1288. }
  1289. // ~strike~
  1290. else if (spanMatch = matchPair([ _MDTokenType.Tilde ], disallowed)) {
  1291. spans.splice(spanMatch.startIndex, spanMatch.toDelete, new _MDStrikethroughSpan(spanMatch.content));
  1292. anyChanges = true;
  1293. }
  1294. // *strong* _strong_
  1295. else if (spanMatch = (matchPair([ _MDTokenType.Asterisk ], disallowed) ||
  1296. matchPair([ _MDTokenType.Underscore ], disallowed))) {
  1297. spans.splice(spanMatch.startIndex, spanMatch.toDelete, new _MDEmphasisSpan(spanMatch.content));
  1298. anyChanges = true;
  1299. }
  1300. } while (anyChanges);
  1301. }
  1302. spans = spans.map(function(span) {
  1303. if (span instanceof _MDToken) {
  1304. return new _MDTextSpan(span.original);
  1305. } else if (span instanceof _MDSpan) {
  1306. return span;
  1307. } else {
  1308. throw new Error(`Unexpected span type ${span.constructor.name}`);
  1309. }
  1310. });
  1311. return spans;
  1312. }
  1313. /**
  1314. * Reads the contents of something like a list item
  1315. * @param {_MDState} state
  1316. * @param {number} firstLineStartPos
  1317. * @param {RegExp} stopRegex
  1318. * @param {Boolean} inList
  1319. * @returns {_MDBlock}
  1320. */
  1321. static #readInteriorContent(state, firstLineStartPos, stopRegex, inList=false) {
  1322. var p = state.p;
  1323. var seenBlankLine = false;
  1324. var needsBlocks = false;
  1325. var lines = [];
  1326. while (p < state.lines.length) {
  1327. let line = state.lines[p++];
  1328. if (p == state.p + 1) {
  1329. line = line.substring(firstLineStartPos);
  1330. }
  1331. let isBlank = line.trim().length == 0;
  1332. let isIndented = /^\s+/.exec(line) !== null;
  1333. if (isBlank) {
  1334. seenBlankLine = true;
  1335. lines.push(line.trim());
  1336. } else if (stopRegex && stopRegex.exec(line)) {
  1337. p--;
  1338. break;
  1339. } else if (isIndented) {
  1340. if (seenBlankLine) {
  1341. needsBlocks = true;
  1342. }
  1343. lines.push(this.#stripIndent(line));
  1344. } else {
  1345. if (seenBlankLine) {
  1346. p--;
  1347. break;
  1348. }
  1349. lines.push(this.#stripIndent(line));
  1350. }
  1351. }
  1352. while (lines.length > 0 && lines[lines.length - 1].trim().length == 0) {
  1353. lines.pop();
  1354. }
  1355. if (needsBlocks) {
  1356. let substate = state.copy(lines);
  1357. let blocks = this.#readBlocks(substate);
  1358. state.p = p;
  1359. return new _MDMultiBlock(blocks);
  1360. } else {
  1361. state.p = p;
  1362. return this.#readInline(state, lines.join("\n"));
  1363. }
  1364. }
  1365. /**
  1366. * @param {_MDState} state
  1367. * @returns {_MDBlock|null}
  1368. */
  1369. static #readUnderlineHeader(state) {
  1370. var p = state.p;
  1371. if (!state.hasLines(2)) return null;
  1372. let contentLine = state.lines[p++].trim();
  1373. let underLine = state.lines[p++].trim();
  1374. if (contentLine == '') return null;
  1375. if (/^=+$/.exec(underLine)) {
  1376. state.p = p;
  1377. return new _MDHeaderBlock(1, this.#readInline(state, contentLine));
  1378. }
  1379. if (/^\-+$/.exec(underLine)) {
  1380. state.p = p;
  1381. return new _MDHeaderBlock(2, this.#readInline(state, contentLine));
  1382. }
  1383. return null;
  1384. }
  1385. static #hashHeaderRegex = /^(#{1,6})\s*([^#].*?)\s*\#*\s*$/; // 1=hashes, 2=content
  1386. /**
  1387. * @param {_MDState} state
  1388. * @returns {_MDBlock|null}
  1389. */
  1390. static #readHashHeader(state) {
  1391. var p = state.p;
  1392. var groups = this.#hashHeaderRegex.exec(state.lines[p++]);
  1393. if (groups === null) return null;
  1394. state.p = p;
  1395. return new _MDHeaderBlock(groups[1].length, this.#readInline(state, groups[2]));
  1396. }
  1397. /**
  1398. * @param {_MDState} state
  1399. * @returns {_MDBlock|null}
  1400. */
  1401. static #readBlockQuote(state) {
  1402. var blockquoteLines = [];
  1403. var p = state.p;
  1404. while (p < state.lines.length) {
  1405. let line = state.lines[p++];
  1406. if (line.startsWith(">")) {
  1407. blockquoteLines.push(line);
  1408. } else {
  1409. break;
  1410. }
  1411. }
  1412. if (blockquoteLines.length > 0) {
  1413. let contentLines = blockquoteLines.map(function(line) {
  1414. return line.substring(1).replace(/^ {0,3}\t?/, '');
  1415. });
  1416. let substate = state.copy(contentLines);
  1417. let quotedBlocks = this.#readBlocks(substate);
  1418. state.p = p;
  1419. return new _MDBlockquoteBlock(quotedBlocks);
  1420. }
  1421. return null;
  1422. }
  1423. static #unorderedListRegex = /^([\*\+\-]\s+)(.*)$/; // 1=bullet, 2=content
  1424. static #unorderedListItemRegex = /^[\*\+\-]\s+/;
  1425. /**
  1426. * @param {_MDState} state
  1427. * @returns {_MDListItemBlock|null}
  1428. */
  1429. static #readUnorderedListItem(state) {
  1430. var p = state.p;
  1431. let line = state.lines[p];
  1432. let groups = this.#unorderedListRegex.exec(line);
  1433. if (groups === null) return null;
  1434. return new _MDListItemBlock(this.#readInteriorContent(state, groups[1].length, this.#unorderedListItemRegex, true));
  1435. }
  1436. /**
  1437. * @param {_MDState} state
  1438. * @returns {_MDBlock|null}
  1439. */
  1440. static #readUnorderedList(state) {
  1441. var items = [];
  1442. var item = null;
  1443. do {
  1444. item = this.#readUnorderedListItem(state);
  1445. if (item) items.push(item);
  1446. } while (item);
  1447. if (items.length == 0) return null;
  1448. return new _MDUnorderedListBlock(items);
  1449. }
  1450. static #orderedListRegex = /^(\d+)(\.\s+)(.*)$/; // 1=number, 2=dot, 3=content
  1451. static #orderedListItemRegex = /^\d+\.\s+/;
  1452. /**
  1453. * @param {_MDState} state
  1454. * @returns {_MDListItemBlock|null}
  1455. */
  1456. static #readOrderedListItem(state) {
  1457. var p = state.p;
  1458. let line = state.lines[p];
  1459. let groups = this.#orderedListRegex.exec(line);
  1460. if (groups === null) return null;
  1461. return new _MDListItemBlock(this.#readInteriorContent(state, groups[1].length + groups[2].length, this.#orderedListItemRegex, true));
  1462. }
  1463. /**
  1464. * @param {_MDState} state
  1465. * @returns {_MDBlock|null}
  1466. */
  1467. static #readOrderedList(state) {
  1468. var items = [];
  1469. var item = null;
  1470. do {
  1471. item = this.#readOrderedListItem(state);
  1472. if (item) items.push(item);
  1473. } while (item);
  1474. if (items.length == 0) return null;
  1475. return new _MDOrderedListBlock(items);
  1476. }
  1477. /**
  1478. * @param {_MDState} state
  1479. * @returns {_MDBlock|null}
  1480. */
  1481. static #readFencedCodeBlock(state) {
  1482. if (!state.hasLines(2)) return null;
  1483. var p = state.p;
  1484. if (state.lines[p++].trim() != '```') return null;
  1485. var codeLines = [];
  1486. while (state.hasLines(1, p)) {
  1487. let line = state.lines[p++];
  1488. if (line.trim() == '```') {
  1489. state.p = p;
  1490. return new _MDCodeBlock(codeLines.join("\n"));
  1491. }
  1492. codeLines.push(line);
  1493. }
  1494. return null;
  1495. }
  1496. /**
  1497. * @param {_MDState} state
  1498. * @returns {_MDBlock|null}
  1499. */
  1500. static #readIndentedCodeBlock(state) {
  1501. var p = state.p;
  1502. var codeLines = [];
  1503. while (state.hasLines(1, p)) {
  1504. let line = state.lines[p++];
  1505. if (this.#countIndents(line, true) < 1) {
  1506. p--;
  1507. break;
  1508. }
  1509. codeLines.push(this.#stripIndent(line));
  1510. }
  1511. if (codeLines.length == 0) return null;
  1512. state.p = p;
  1513. return new _MDCodeBlock(codeLines.join("\n"));
  1514. }
  1515. static #horizontalRuleRegex = /^\s*(?:\-(?:\s*\-){2,}|\*(?:\s*\*){2,})\s*$/;
  1516. /**
  1517. * @param {_MDState} state
  1518. * @returns {_MDBlock|null}
  1519. */
  1520. static #readHorizontalRule(state) {
  1521. var p = state.p;
  1522. let line = state.lines[p++];
  1523. if (this.#horizontalRuleRegex.exec(line)) {
  1524. state.p = p;
  1525. return new _MDHorizontalRuleBlock();
  1526. }
  1527. return null;
  1528. }
  1529. /**
  1530. * @param {_MDState} state
  1531. * @param {Boolean} isHeader
  1532. * @return {_MDTableRowBlock|null}
  1533. */
  1534. static #readTableRow(state, isHeader) {
  1535. if (!state.hasLines(1)) return null;
  1536. var p = state.p;
  1537. let line = state.lines[p++].trim();
  1538. if (/.*\|.*/.exec(line) === null) return null;
  1539. if (line.startsWith('|')) line = line.substring(1);
  1540. if (line.endsWith('|')) line = line.substring(0, line.length - 1);
  1541. let cellTokens = line.split('|');
  1542. let cells = cellTokens.map(function(token) {
  1543. let content = Markdown.#readInline(state, token);
  1544. return isHeader ? new _MDTableHeaderCellBlock(content) : new _MDTableCellBlock(content);
  1545. });
  1546. state.p = p;
  1547. return new _MDTableRowBlock(cells);
  1548. }
  1549. /**
  1550. * @param {String} line
  1551. * @returns {_MDHAlign[]}
  1552. */
  1553. static #parseColumnAlignments(line) {
  1554. line = line.trim();
  1555. if (line.startsWith('|')) line = line.substring(1);
  1556. if (line.endsWith('|')) line = line.substring(0, line.length - 1);
  1557. return line.split('|').map(function(token) {
  1558. token = token.trim();
  1559. if (token.startsWith(':')) {
  1560. if (token.endsWith(':')) {
  1561. return _MDHAlign.Center;
  1562. }
  1563. return _MDHAlign.Left;
  1564. } else if (token.endsWith(':')) {
  1565. return _MDHAlign.Right;
  1566. }
  1567. return null;
  1568. });
  1569. }
  1570. static #tableDividerRegex = /^\s*[|]?(?:\s*[:]?-+[:]?\s*\|)(?:\s*[:]?-+[:]?\s*)[|]?\s*$/;
  1571. /**
  1572. * @param {_MDState} state
  1573. * @returns {_MDBlock|null}
  1574. */
  1575. static #readTable(state) {
  1576. if (!state.hasLines(2)) return null;
  1577. let startP = state.p;
  1578. let headerRow = this.#readTableRow(state, true);
  1579. if (headerRow === null) {
  1580. state.p = startP;
  1581. return null;
  1582. }
  1583. let dividerLine = state.lines[state.p++];
  1584. let dividerGroups = this.#tableDividerRegex.exec(dividerLine);
  1585. if (dividerGroups === null) {
  1586. state.p = startP;
  1587. return null;
  1588. }
  1589. let columnAlignments = this.#parseColumnAlignments(dividerLine);
  1590. headerRow.applyAlignments(columnAlignments);
  1591. var bodyRows = [];
  1592. while (state.hasLines(1)) {
  1593. let row = this.#readTableRow(state, false);
  1594. if (row === null) break;
  1595. row.applyAlignments(columnAlignments);
  1596. bodyRows.push(row);
  1597. }
  1598. return new _MDTableBlock(headerRow, bodyRows);
  1599. }
  1600. /**
  1601. * @param {_MDState} state
  1602. * @returns {_MDBlock|null}
  1603. */
  1604. static #readDefinitionList(state) {
  1605. var p = state.p;
  1606. var groups;
  1607. var termCount = 0;
  1608. var definitionCount = 0;
  1609. var defLines = [];
  1610. while (state.hasLines(1, p)) {
  1611. let line = state.lines[p++];
  1612. if (line.trim().length == 0) {
  1613. p--;
  1614. break;
  1615. }
  1616. if (/^\s+/.exec(line)) {
  1617. if (defLines.length == 0) return null;
  1618. defLines[defLines.length - 1] += "\n" + line;
  1619. } else if (/^:\s+/.exec(line)) {
  1620. defLines.push(line);
  1621. definitionCount++;
  1622. } else {
  1623. defLines.push(line);
  1624. termCount++;
  1625. }
  1626. }
  1627. if (termCount == 0 || definitionCount == 0) return null;
  1628. let blocks = defLines.map(function(line) {
  1629. if (groups = /^:\s+(.*)$/.exec(line)) {
  1630. return new _MDDefinitionDefinitionBlock(Markdown.#readInline(state, groups[1]));
  1631. } else {
  1632. return new _MDDefinitionTermBlock(Markdown.#readInline(state, line));
  1633. }
  1634. });
  1635. state.p = p;
  1636. return new _MDDefinitionListBlock(blocks);
  1637. }
  1638. /**
  1639. * @param {_MDState} state
  1640. * @returns {_MDBlock|null}
  1641. */
  1642. static #readFootnoteDef(state) {
  1643. var p = state.p;
  1644. let groups = /^\s*\[\^\s*([^\]]+)\s*\]:\s+(.*)\s*$/.exec(state.lines[p++]);
  1645. if (groups === null) return null;
  1646. let symbol = groups[1];
  1647. let def = groups[2];
  1648. while (state.hasLines(1, p)) {
  1649. let line = state.lines[p++];
  1650. if (/^\s+/.exec(line)) {
  1651. def += "\n" + line;
  1652. } else {
  1653. p--;
  1654. break;
  1655. }
  1656. }
  1657. state.p = p;
  1658. let content = this.#readInline(state, def);
  1659. state.defineFootnote(symbol, content);
  1660. state.p = p;
  1661. return new _MDMultiBlock([]);
  1662. }
  1663. /**
  1664. * @param {_MDState} state
  1665. * @returns {_MDBlock|null}
  1666. */
  1667. static #readAbbreviationDef(state) {
  1668. var p = state.p;
  1669. let line = state.lines[p++];
  1670. let groups = /^\s*\*\[([^\]]+?)\]:\s+(.*?)\s*$/.exec(line);
  1671. if (groups === null) return null;
  1672. let abbrev = groups[1];
  1673. let def = groups[2];
  1674. state.defineAbbreviation(abbrev, def);
  1675. state.p = p;
  1676. return new _MDMultiBlock([]);
  1677. }
  1678. /**
  1679. * @param {_MDState} state
  1680. * @returns {_MDBlock|null}
  1681. */
  1682. static #readURLDef(state) {
  1683. var p = state.p;
  1684. let line = state.lines[p++];
  1685. var symbol;
  1686. var url;
  1687. var title = null;
  1688. let groups = /^\s*\[(.+?)]:\s*(\S+)\s+"(.*?)"\s*$/.exec(line);
  1689. if (groups) {
  1690. symbol = groups[1];
  1691. url = groups[2];
  1692. title = groups[3];
  1693. } else {
  1694. groups = /^\s*\[(.+?)]:\s*(\S+)\s*$/.exec(line);
  1695. if (groups) {
  1696. symbol = groups[1];
  1697. url = groups[2];
  1698. } else {
  1699. return null;
  1700. }
  1701. }
  1702. state.defineURL(symbol, url, title);
  1703. state.p = p;
  1704. return new _MDInlineBlock([]);
  1705. }
  1706. /**
  1707. * @param {_MDState} state
  1708. * @returns {_MDBlock|null}
  1709. */
  1710. static #readParagraph(state) {
  1711. var paragraphLines = [];
  1712. var p = state.p;
  1713. while (p < state.lines.length) {
  1714. let line = state.lines[p++];
  1715. if (line.trim().length == 0) {
  1716. break;
  1717. }
  1718. paragraphLines.push(line);
  1719. }
  1720. if (paragraphLines.length > 0) {
  1721. state.p = p;
  1722. let content = paragraphLines.join("\n");
  1723. return new _MDParagraphBlock(this.#readInline(state, content));
  1724. }
  1725. return null;
  1726. }
  1727. /**
  1728. * @param {String} html
  1729. * @param {_MDState} state
  1730. * @returns {String}
  1731. */
  1732. static #postProcessFootnotes(html, state) {
  1733. let footnotes = state.footnotes;
  1734. if (Object.keys(footnotes).length == 0) return html;
  1735. var symbolOrder = [];
  1736. var footnoteOccurrences = {};
  1737. var footnoteIndex = 0;
  1738. html = html.replace(/<!--FNREF:{(\d+)}-->/g, function(match, symbol) {
  1739. footnoteIndex++;
  1740. symbol = symbol.toLowerCase();
  1741. if (!symbolOrder.includes(symbol)) {
  1742. symbolOrder.push(symbol);
  1743. }
  1744. var occurrences = footnoteOccurrences[symbol] || [];
  1745. occurrences.push(footnoteIndex);
  1746. footnoteOccurrences[symbol] = occurrences;
  1747. return `<sup id="footnoteref_${footnoteIndex}"><a href="#footnote_${symbol}">${symbol}</a></sup>`;
  1748. });
  1749. if (footnoteIndex == 0) return html;
  1750. html += '<div class="footnotes"><hr/>';
  1751. html += '<ol>';
  1752. for (const symbol of symbolOrder) {
  1753. let content = state.footnotes[symbol];
  1754. if (!content) continue;
  1755. html += `<li value="${symbol}" id="footnote_${symbol}">${content.toHTML(state)}`;
  1756. for (const ref of footnoteOccurrences[symbol]) {
  1757. html += ` <a href="#footnoteref_${ref}" class="footnote-backref" role="doc-backlink">↩︎</a>`;
  1758. }
  1759. html += `</li>\n`;
  1760. }
  1761. html += '</ol>';
  1762. html += '</div>';
  1763. return html;
  1764. }
  1765. /**
  1766. * @param {String} markdown
  1767. * @returns {String} HTML
  1768. */
  1769. static toHTML(markdown) {
  1770. var state = new _MDState();
  1771. let lines = markdown.split(/(?:\n|\r|\r\n)/);
  1772. state.lines = lines;
  1773. let blocks = this.#readBlocks(state);
  1774. let html = _MDBlock.toHTML(blocks, state);
  1775. html = this.#postProcessFootnotes(html, state);
  1776. return html;
  1777. }
  1778. }