PHP and Javascript implementations of a simple markdown parser
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274
  1. // FIXME: Nested blockquotes require blank line
  2. // TODO: HTML tags probably need better handling. Consider whether interior of matched tags should be interpreted as markdown.
  3. // TODO: Test broken/incomplete syntax thoroughly
  4. // TODO: Sanity checks on loops/recursion?
  5. // TODO: Tolerate whitespace between tokens (e.g. [click here] [urlref])
  6. // TODO: Spreadsheet functions in tables
  7. // TODO: Support document differentiators for CSS identifiers
  8. class MDTokenType {
  9. static Text = new MDTokenType('Text');
  10. static Whitespace = new MDTokenType('Whitespace');
  11. static Underscore = new MDTokenType('Underscore');
  12. static Asterisk = new MDTokenType('Asterisk');
  13. static Slash = new MDTokenType('Slash');
  14. static Tilde = new MDTokenType('Tilde');
  15. static Bang = new MDTokenType('Bang');
  16. static Backtick = new MDTokenType('Backtick');
  17. static Label = new MDTokenType('Label'); // content=label
  18. static URL = new MDTokenType('URL'); // content=URL, extra=title
  19. static Email = new MDTokenType('Email'); // content=email address, extra=title
  20. static SimpleLink = new MDTokenType('SimpleLink'); // content=URL
  21. static SimpleEmail = new MDTokenType('SimpleEmail'); // content=email address
  22. static Footnote = new MDTokenType('Footnote'); // content=symbol
  23. static Modifier = new MDTokenType('Modifier'); // content
  24. static HTMLTag = new MDTokenType('HTMLTag'); // content=tag string, tag=MDHTMLTag
  25. static META_AnyNonWhitespace = new MDTokenType('METAAnyNonWhitespace');
  26. static META_OptionalWhitespace = new MDTokenType('METAOptionalWhitespace');
  27. /** @type {string} */
  28. name;
  29. /**
  30. * @param {string} name
  31. */
  32. constructor(name) {
  33. this.name = name;
  34. }
  35. toString() {
  36. return `${this.constructor.name}.${this.name}`;
  37. }
  38. }
  39. class MDToken {
  40. /**
  41. * The original token string.
  42. * @type {string}
  43. */
  44. original;
  45. /** @type {MDTokenType} */
  46. type;
  47. /** @type {string|null} */
  48. content;
  49. /** @type {string|null} */
  50. extra;
  51. /** @type {MDHTMLTag|null} */
  52. tag;
  53. /** @type {MDTagModifier|null} */
  54. modifier;
  55. /**
  56. * @param {string} original
  57. * @param {MDTokenType} type
  58. * @param {string|MDTagModifier|null} content
  59. * @param {string|null} extra
  60. * @param {MDHTMLTag|null} tag
  61. */
  62. constructor(original, type, content=null, extra=null, tag=null) {
  63. this.original = original;
  64. this.type = type;
  65. if (content instanceof MDTagModifier) {
  66. this.content = null;
  67. this.modifier = content;
  68. } else {
  69. this.content = content;
  70. this.modifier = null;
  71. }
  72. this.extra = extra;
  73. this.tag = tag;
  74. }
  75. /**
  76. * Searches an array of MDToken for the given pattern of MDTokenTypes.
  77. * If found, returns an object with the given keys.
  78. * - `tokens: MDToken[]` - the subarray of `tokensToSearch` that match the pattern
  79. * - `index: number` - index into `tokensToSearch` of first matching token
  80. *
  81. * @param {MDToken[]|MDSpan[]} tokensToSearch
  82. * @param {MDTokenType[]} pattern
  83. * @param {number} startIndex
  84. * @returns {object|null} match
  85. */
  86. static findFirstTokens(tokensToSearch, pattern, startIndex=0) {
  87. var matched = [];
  88. for (var t = startIndex; t < tokensToSearch.length; t++) {
  89. var matchedAll = true;
  90. matched = [];
  91. var patternOffset = 0;
  92. for (var p = 0; p < pattern.length; p++) {
  93. var t0 = t + p + patternOffset;
  94. if (t0 >= tokensToSearch.length) return null;
  95. let token = tokensToSearch[t0];
  96. let elem = pattern[p];
  97. if (elem == MDTokenType.META_OptionalWhitespace) {
  98. if (token instanceof MDToken && token.type == MDTokenType.Whitespace) {
  99. matched.push(token);
  100. } else {
  101. patternOffset--;
  102. }
  103. } else if (elem == MDTokenType.META_AnyNonWhitespace) {
  104. if (token instanceof MDToken && token.type == MDTokenType.Whitespace) {
  105. matchedAll = false;
  106. break;
  107. }
  108. matched.push(token);
  109. } else {
  110. if (!(token instanceof MDToken) || token.type != elem) {
  111. matchedAll = false;
  112. break;
  113. }
  114. matched.push(token);
  115. }
  116. }
  117. if (matchedAll) {
  118. return {
  119. 'tokens': matched,
  120. 'index': t,
  121. };
  122. }
  123. }
  124. return null;
  125. }
  126. /**
  127. * Searches an array of MDToken for a given starting pattern and ending
  128. * pattern and returns match info about both and the tokens in between.
  129. *
  130. * If `contentValidator` is specified, it will be called with the content
  131. * tokens of a potential match. If the validator returns `true`, the result
  132. * will be accepted and returned by this method. If the validator returns
  133. * `false`, this method will keep looking for another matching pair. If no
  134. * validator is given the first match will be returned regardless of content.
  135. *
  136. * If a match is found, returns an object with the given keys:
  137. * - `startTokens: MDToken[]` - tokens that matched `startPattern`
  138. * - `contentTokens: MDToken[]` - tokens between the start and end pattern. May be an empty array.
  139. * - `endTokens: MDToken[]` - tokens that matched `endPattern`
  140. * - `startIndex: number` - index into `tokensToSearch` where `startPattern` begins
  141. * - `contentIndex: number` - index into `tokensToSearch` of the first token that is between the start and end patterns
  142. * - `endIndex: number` - index into `tokensToSearch` where `endPattern` begins
  143. * - `totalLength: number` - total number of matched tokens
  144. *
  145. * @param {MDToken[]} tokensToSearch - array of `MDToken` to search in
  146. * @param {MDTokenType[]} startPattern - array of `MDTokenType` to find first
  147. * @param {MDTokenType[]} endPattern - array of `MDTokenType` to find positioned after `startPattern`
  148. * @param {function|null} contentValidator - optional validator function. If provided, will be passed an array of inner `MDToken`, and the function can return `true` to accept the contents or `false` to keep searching
  149. * @param {number} startIndex - token index where searching should begin
  150. * @returns {object|null} match object
  151. */
  152. static findPairedTokens(tokensToSearch, startPattern, endPattern, contentValidator=null, startIndex=0) {
  153. for (var s = startIndex; s < tokensToSearch.length; s++) {
  154. var startMatch = this.findFirstTokens(tokensToSearch, startPattern, s);
  155. if (startMatch === null) return null;
  156. var endStart = startMatch.index + startMatch.tokens.length;
  157. while (endStart < tokensToSearch.length) {
  158. var endMatch = this.findFirstTokens(tokensToSearch, endPattern, endStart);
  159. if (endMatch === null) break;
  160. var contents = tokensToSearch.slice(startMatch.index + startMatch.tokens.length, endMatch.index);
  161. if (contents.length > 0 && (contentValidator === null || contentValidator(contents))) {
  162. return {
  163. 'startTokens': startMatch.tokens,
  164. 'contentTokens': contents,
  165. 'endTokens': endMatch.tokens,
  166. 'startIndex': startMatch.index,
  167. 'contentIndex': startMatch.index + startMatch.tokens.length,
  168. 'endIndex': endMatch.index,
  169. 'totalLength': endMatch.index + endMatch.tokens.length - startMatch.index,
  170. };
  171. } else {
  172. // Contents rejected. Try next end match.
  173. endStart = endMatch.index + 1;
  174. }
  175. }
  176. // No end matches. Increment start match.
  177. s = startMatch.index;
  178. }
  179. return null;
  180. }
  181. }
  182. class MDUtils {
  183. // Modified from https://urlregex.com/ to remove capture groups. Matches fully qualified URLs only.
  184. static baseURLRegex = /(?:(?:(?:[a-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[a-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[a-z0-9\.\-]+)(?:(?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/i;
  185. // Modified from https://emailregex.com/ to remove capture groups.
  186. 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;
  187. /**
  188. * @param {string} str
  189. * @returns {string}
  190. */
  191. static escapeHTML(str) {
  192. return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  193. }
  194. /**
  195. * @param {string} email
  196. */
  197. static escapeObfuscated(text) {
  198. var html = '';
  199. for (var p = 0; p < text.length; p++) {
  200. const cp = text.codePointAt(p);
  201. html += `&#${cp};`;
  202. }
  203. return html;
  204. }
  205. /**
  206. * Strips one or more leading indents from a line or lines of markdown. An
  207. * indent is defined as 4 spaces or one tab. Incomplete indents (i.e. 1-3
  208. * spaces) are treated like one indent level.
  209. *
  210. * @param {string|string[]} line - string or strings to strip
  211. * @param {number} levels - how many indent levels to strip
  212. * @returns {string|string[]} stripped lines
  213. */
  214. static stripIndent(line, levels=1) {
  215. const regex = new RegExp(`^(?: {1,4}|\t){${levels}}`);
  216. return (line instanceof Array) ? line.map((l) => l.replace(regex, '')) : line.replace(regex, '');
  217. }
  218. /**
  219. * Returns a copy of an array without any whitespace-only lines at the end.
  220. *
  221. * @param {String[]} lines - text lines
  222. * @returns {String[]} - text lines without trailing blank lines
  223. */
  224. static withoutTrailingBlankLines(lines) {
  225. var stripped = lines.slice();
  226. while (stripped.length > 0 && stripped[stripped.length - 1].trim().length == 0) {
  227. stripped.pop();
  228. }
  229. return stripped;
  230. }
  231. /**
  232. * Counts the number of indent levels in a line of text. Partial indents
  233. * (1 to 3 spaces) are counted as one indent level unless `fullIndentsOnly`
  234. * is `true`.
  235. *
  236. * @param {string} line - line of markdown
  237. * @param {boolean} fullIndentsOnly - whether to only count full indent levels (4 spaces or a tab)
  238. * @returns {number} number of indent levels found
  239. */
  240. static countIndents(line, fullIndentsOnly=false) {
  241. // normalize indents to tabs
  242. return line.replace(fullIndentsOnly
  243. ? /(?: {4}|\t)/g
  244. : /(?: {1,4}|\t)/g,
  245. "\t")
  246. // remove content after indent
  247. .replace(/^(\t*)(.*?)$/, '$1')
  248. // count tabs
  249. .length;
  250. }
  251. /**
  252. * Attempts to parse a label from the beginning of `line`. A label is of the
  253. * form `[content]`. If found, returns an array with element 0 being the
  254. * entire label and element 1 being the content of the label.
  255. *
  256. * @param {string} line
  257. * @returns {string[]|null} match groups or null if not found
  258. */
  259. static tokenizeLabel(line) {
  260. if (!line.startsWith('[')) return null;
  261. var parenCount = 0;
  262. var bracketCount = 0;
  263. for (var p = 1; p < line.length; p++) {
  264. let ch = line.substring(p, p + 1);
  265. if (ch == '\\') {
  266. p++;
  267. } else if (ch == '(') {
  268. parenCount++;
  269. } else if (ch == ')') {
  270. parenCount--;
  271. if (parenCount < 0) return null;
  272. } else if (ch == '[') {
  273. bracketCount++;
  274. } else if (ch == ']') {
  275. if (bracketCount > 0) {
  276. bracketCount--;
  277. } else {
  278. return [ line.substring(0, p + 1), line.substring(1, p) ];
  279. }
  280. }
  281. }
  282. return null;
  283. }
  284. static #urlWithTitleRegex = /^\((\S+?)\s+"(.*?)"\)/i; // 1=URL, 2=title
  285. static #urlRegex = /^\((\S+?)\)/i; // 1=URL
  286. /**
  287. * Attempts to parse a URL from the beginning of `line`. A URL is of the
  288. * form `(url)` or `(url "title")`. If found, returns an array with element
  289. * 0 being the entire URL token, 1 is the URL, 2 is the optional title.
  290. *
  291. * @param {string} line
  292. * @returns {string[]} token tuple
  293. */
  294. static tokenizeURL(line) {
  295. var groups;
  296. if (groups = this.#urlWithTitleRegex.exec(line)) {
  297. if (this.tokenizeEmail(line)) return null; // make sure it's not better described as an email address
  298. return groups;
  299. }
  300. if (groups = this.#urlRegex.exec(line)) {
  301. if (this.tokenizeEmail(line)) return null;
  302. return [...groups, null];
  303. }
  304. return null;
  305. }
  306. static #emailWithTitleRegex = new RegExp("^\\(\\s*(" + MDUtils.baseEmailRegex.source + ")\\s+\"(.*?)\"\\s*\\)", "i"); // 1=email, 2=title
  307. static #emailRegex = new RegExp("^\\(\\s*(" + MDUtils.baseEmailRegex.source + ")\\s*\\)", "i"); // 1=email
  308. /**
  309. * Attempts to parse an email address from the beginning of `line`. An
  310. * email address is of the form `(user@example.com)` or `(user@example.com "link title")`.
  311. * If found, returns an array with element 0 being the entire token, 1 is the
  312. * email address, and 2 is the optional link title.
  313. *
  314. * @param {string} line
  315. * @returns {string[]} token tuple
  316. */
  317. static tokenizeEmail(line) {
  318. var groups;
  319. if (groups = this.#emailWithTitleRegex.exec(line)) {
  320. return groups;
  321. }
  322. if (groups = this.#emailRegex.exec(line)) {
  323. return [...groups, null];
  324. }
  325. return null;
  326. }
  327. /**
  328. * Describes the type of a variable for debugging.
  329. *
  330. * @param {any} value - value
  331. * @returns {String} description of type
  332. */
  333. static typename(value) {
  334. if (value === null) return 'null';
  335. if (value instanceof Object) {
  336. return value.constructor.name;
  337. }
  338. return typeof value;
  339. }
  340. }
  341. // -- Block readers ---------------------------------------------------------
  342. /**
  343. * Base class for reading from an array of markdown lines and finding a block
  344. * of a given type. Readers are checked in `priority` order and `readBlock` is
  345. * called to see the reader implementation recognizes a particular kind of block
  346. * at the given line pointer, returning that block if so or null if not.
  347. *
  348. * Inline markdown is processed in a separate stage by `MDInlineReader`.
  349. */
  350. class MDBlockReader {
  351. /** @type {number} */
  352. #priority;
  353. /**
  354. * A unitless relative priority value that determines which readers are
  355. * tried first. Lower values are tried first. In the range of 0.0 to 100.0.
  356. * @type {number} priority
  357. */
  358. get priority() { return this.#priority; }
  359. /**
  360. * Creates a block reader.
  361. *
  362. * When overriding the constructor, it is suggested to allow the caller to
  363. * specify an optional custom priority value, falling back to a reasonable
  364. * default if not specified.
  365. *
  366. * @param {number} priority
  367. */
  368. constructor(priority) {
  369. this.#priority = priority;
  370. }
  371. /**
  372. * Attempts to read a block of this type from the given read state. If
  373. * successful, the state's line pointer should be incremented to the line
  374. * just after the last line of the block and the read block should be
  375. * returned.
  376. *
  377. * @param {MDState} state - read state
  378. * @returns {MDBlock|null} the read block
  379. */
  380. readBlock(state) {
  381. throw Error(`Abstract readBlock must be overridden in ${this.constructor.name}`);
  382. }
  383. /**
  384. * Called after the full document has been generated for optional
  385. * post-processing.
  386. *
  387. * @param {MDState} state
  388. * @param {MDBlock[]} blocks - top-level document block list
  389. */
  390. postProcess(state, blocks) {
  391. // no op
  392. }
  393. }
  394. /**
  395. * Reads markdown blocks for headers denoted with the underline syntax.
  396. *
  397. * Example:
  398. *
  399. * > ```markdown
  400. * > Header 1
  401. * > ========
  402. * > ```
  403. */
  404. class MDUnderlinedHeaderBlockReader extends MDBlockReader {
  405. constructor(priority=0.0) {
  406. super(priority);
  407. }
  408. /**
  409. * @param {MDState} state
  410. */
  411. readBlock(state) {
  412. var p = state.p;
  413. if (!state.hasLines(2)) return null;
  414. var modifier;
  415. let contentLine = state.lines[p++].trim();
  416. [contentLine, modifier] = MDTagModifier.fromLine(contentLine);
  417. let underLine = state.lines[p++].trim();
  418. if (contentLine == '') return null;
  419. if (/^=+$/.exec(underLine)) {
  420. state.p = p;
  421. let block = new MDHeaderBlock(1, state.inlineMarkdownToSpan(contentLine));
  422. if (modifier) modifier.applyTo(block);
  423. return block;
  424. }
  425. if (/^\-+$/.exec(underLine)) {
  426. state.p = p;
  427. let block = new MDHeaderBlock(2, state.inlineMarkdownToSpan(contentLine));
  428. if (modifier) modifier.applyTo(block);
  429. return block;
  430. }
  431. return null;
  432. }
  433. }
  434. /**
  435. * Reads markdown blocks for headers denoted with hash marks. Header levels 1 to
  436. * 6 are supported.
  437. *
  438. * Examples:
  439. *
  440. * > ```markdown
  441. * > # Header 1
  442. * >
  443. * > ## Header 2
  444. * >
  445. * > # Enclosing Hashes Are Optional #
  446. * >
  447. * > ## Trailing Hashes Don't Have to Match in Number ####
  448. * > ```
  449. */
  450. class MDHashHeaderBlockReader extends MDBlockReader {
  451. static #hashHeaderRegex = /^(#{1,6})\s*([^#].*?)\s*\#*\s*$/; // 1=hashes, 2=content
  452. constructor(priority=5.0) {
  453. super(priority);
  454. }
  455. readBlock(state) {
  456. var p = state.p;
  457. let line = state.lines[p++];
  458. var modifier;
  459. [line, modifier] = MDTagModifier.fromLine(line);
  460. var groups = MDHashHeaderBlockReader.#hashHeaderRegex.exec(line);
  461. if (groups === null) return null;
  462. state.p = p;
  463. const level = groups[1].length;
  464. const content = groups[2];
  465. let block = new MDHeaderBlock(level, state.inlineMarkdownToSpan(content));
  466. if (modifier) modifier.applyTo(block);
  467. return block;
  468. }
  469. }
  470. /**
  471. * Reads markdown blocks for blockquoted text.
  472. *
  473. * Example:
  474. *
  475. * > ```markdown
  476. * > > Blockquoted text
  477. * > ```
  478. */
  479. class MDBlockQuoteBlockReader extends MDBlockReader {
  480. constructor(priority=10.0) {
  481. super(priority);
  482. }
  483. /**
  484. * @param {MDState} state
  485. */
  486. readBlock(state) {
  487. var blockquoteLines = [];
  488. var p = state.p;
  489. while (p < state.lines.length) {
  490. let line = state.lines[p++];
  491. if (line.startsWith(">")) {
  492. blockquoteLines.push(line);
  493. } else {
  494. break;
  495. }
  496. }
  497. if (blockquoteLines.length > 0) {
  498. let contentLines = blockquoteLines.map(function(line) {
  499. return line.substring(1).replace(/^ {0,3}\t?/, '');
  500. });
  501. let substate = state.copy(contentLines);
  502. let quotedBlocks = substate.readBlocks();
  503. state.p = p;
  504. return new MDBlockquoteBlock(quotedBlocks);
  505. }
  506. return null;
  507. }
  508. }
  509. class MDBaseListBlockReader extends MDBlockReader {
  510. constructor(priority) {
  511. super(priority);
  512. }
  513. #readItemLines(state, firstLineStartPos) {
  514. var p = state.p;
  515. var lines = [];
  516. var seenBlankLine = false;
  517. var stripTrailingBlankLines = true;
  518. while (state.hasLines(1, p)) {
  519. const isFirstLine = p == state.p;
  520. var line = state.lines[p++];
  521. if (isFirstLine) {
  522. line = line.substring(firstLineStartPos);
  523. }
  524. if (/^(?:\*|\+|\-|\d+\.)\s+/.exec(line)) {
  525. // Found next list item
  526. stripTrailingBlankLines = false; // because this signals extra spacing intended
  527. break;
  528. }
  529. const isBlankLine = line.trim().length == 0;
  530. const isIndented = /^\s+\S/.exec(line) !== null;
  531. if (isBlankLine) {
  532. seenBlankLine = true;
  533. } else if (!isIndented && seenBlankLine) {
  534. // Post-list content
  535. break;
  536. }
  537. lines.push(line);
  538. }
  539. lines = MDUtils.withoutTrailingBlankLines(lines);
  540. return MDUtils.stripIndent(lines);
  541. }
  542. /**
  543. * @param {MDState} state
  544. * @param {number} firstLineStart
  545. * @return {MDBlock}
  546. */
  547. readListItemContent(state, firstLineStartPos) {
  548. const itemLines = this.#readItemLines(state, firstLineStartPos);
  549. state.p += itemLines.length;
  550. if (itemLines.length == 1) {
  551. return new MDInlineBlock(state.inlineMarkdownToSpans(itemLines[0]));
  552. }
  553. const hasBlankLines = itemLines.filter((line) => line.trim().length == 0).length > 0;
  554. if (hasBlankLines) {
  555. const substate = state.copy(itemLines);
  556. const blocks = substate.readBlocks();
  557. return (blocks.length == 1) ? blocks[0] : new MDMultiBlock(blocks);
  558. }
  559. // Multiline content with no blank lines. Search for new block
  560. // boundaries without the benefit of a blank line to demarcate it.
  561. for (var p = 1; p < itemLines.length; p++) {
  562. const line = itemLines[p];
  563. if (/^(?:\*|\-|\+|\d+\.)\s+/.exec(line)) {
  564. // Nested list found
  565. const firstBlock = new MDInlineBlock(state.inlineMarkdownToSpans(itemLines.slice(0, p).join("\n")));
  566. const substate = state.copy(itemLines.slice(p));
  567. const blocks = substate.readBlocks();
  568. return new MDMultiBlock([ firstBlock, ...blocks ]);
  569. }
  570. }
  571. // Ok, give up and just do a standard block read
  572. {
  573. const substate = state.copy(itemLines);
  574. const blocks = substate.readBlocks();
  575. return (blocks.length == 1) ? blocks[0] : new MDMultiBlock(blocks);
  576. }
  577. }
  578. readBlock(state) {
  579. throw new Error(`Abstract readBlock must be overridden in ${this.constructor.name}`);
  580. }
  581. }
  582. /**
  583. * Block reader for unordered (bulleted) lists.
  584. *
  585. * Example:
  586. *
  587. * > ```markdown
  588. * > * First item
  589. * > * Second item
  590. * > * Third item
  591. * > ```
  592. */
  593. class MDUnorderedListBlockReader extends MDBaseListBlockReader {
  594. static #unorderedListRegex = /^([\*\+\-]\s+)(.*)$/; // 1=bullet, 2=content
  595. constructor(priority=15.0) {
  596. super(priority);
  597. }
  598. /**
  599. * @param {MDState} state
  600. * @returns {MDListItemBlock|null}
  601. */
  602. #readUnorderedListItem(state) {
  603. var p = state.p;
  604. let line = state.lines[p];
  605. let groups = MDUnorderedListBlockReader.#unorderedListRegex.exec(line);
  606. if (groups === null) return null;
  607. const firstLineOffset = groups[1].length;
  608. return new MDListItemBlock(this.readListItemContent(state, firstLineOffset));
  609. }
  610. readBlock(state) {
  611. var items = [];
  612. var item = null;
  613. do {
  614. item = this.#readUnorderedListItem(state);
  615. if (item) items.push(item);
  616. } while (item);
  617. if (items.length == 0) return null;
  618. return new MDUnorderedListBlock(items);
  619. }
  620. }
  621. /**
  622. * Block reader for ordered (numbered) lists. The number of the first item is
  623. * used to begin counting. The subsequent items increase by 1, regardless of
  624. * their value.
  625. *
  626. * Example:
  627. *
  628. * > ```markdown
  629. * > 1. First
  630. * > 2. Second
  631. * > 3. Third
  632. * > ```
  633. */
  634. class MDOrderedListBlockReader extends MDBaseListBlockReader {
  635. static #orderedListRegex = /^(\d+)(\.\s+)(.*)$/; // 1=number, 2=dot, 3=content
  636. constructor(priority=16.0) {
  637. super(priority);
  638. }
  639. /**
  640. * @param {MDState} state
  641. * @returns {MDListItemBlock|null}
  642. */
  643. #readOrderedListItem(state) {
  644. var p = state.p;
  645. let line = state.lines[p];
  646. let groups = MDOrderedListBlockReader.#orderedListRegex.exec(line);
  647. if (groups === null) return null;
  648. const ordinal = parseInt(groups[1]);
  649. const firstLineOffset = groups[1].length + groups[2].length;
  650. return new MDListItemBlock(this.readListItemContent(state, firstLineOffset), ordinal);
  651. }
  652. readBlock(state) {
  653. var items = [];
  654. var item = null;
  655. do {
  656. item = this.#readOrderedListItem(state);
  657. if (item) items.push(item);
  658. } while (item);
  659. if (items.length == 0) return null;
  660. return new MDOrderedListBlock(items, items[0].ordinal);
  661. }
  662. }
  663. /**
  664. * Block reader for code blocks denoted by pairs of triple tickmarks.
  665. *
  666. * Example:
  667. *
  668. * > ```markdown
  669. * > \`\`\`
  670. * > function formattedAsCode() {
  671. * > }
  672. * > \`\`\`
  673. * > ```
  674. */
  675. class MDFencedCodeBlockReader extends MDBlockReader {
  676. constructor(priority=20.0) {
  677. super(priority);
  678. }
  679. readBlock(state) {
  680. if (!state.hasLines(2)) return null;
  681. var p = state.p;
  682. let openFenceLine = state.lines[p++];
  683. var modifier;
  684. [openFenceLine, modifier] = MDTagModifier.fromLine(openFenceLine);
  685. if (openFenceLine.trim() != '```') return null;
  686. var codeLines = [];
  687. while (state.hasLines(1, p)) {
  688. let line = state.lines[p++];
  689. if (line.trim() == '```') {
  690. state.p = p;
  691. let block = new MDCodeBlock(codeLines.join("\n"));
  692. if (modifier) modifier.applyTo(block);
  693. return block;
  694. }
  695. codeLines.push(line);
  696. }
  697. return null;
  698. }
  699. }
  700. /**
  701. * Block reader for code blocks denoted by indenting text.
  702. *
  703. * Example (indent spaces rendered visibly for clarity):
  704. *
  705. * > ```markdown
  706. * > ⎵⎵⎵⎵function formattedAsCode() {
  707. * > ⎵⎵⎵⎵}
  708. * > ```
  709. */
  710. class MDIndentedCodeBlockReader extends MDBlockReader {
  711. constructor(priority=21.0) {
  712. super(priority);
  713. }
  714. readBlock(state) {
  715. var p = state.p;
  716. var codeLines = [];
  717. while (state.hasLines(1, p)) {
  718. let line = state.lines[p++];
  719. if (MDUtils.countIndents(line, true) < 1) {
  720. p--;
  721. break;
  722. }
  723. codeLines.push(MDUtils.stripIndent(line));
  724. }
  725. if (codeLines.length == 0) return null;
  726. state.p = p;
  727. return new MDCodeBlock(codeLines.join("\n"));
  728. }
  729. }
  730. /**
  731. * Block reader for horizontal rules. Composed of three or more hypens or
  732. * asterisks on a line by themselves, with or without intermediate whitespace.
  733. *
  734. * Examples:
  735. *
  736. * > ```markdown
  737. * > ---
  738. * >
  739. * > - - -
  740. * >
  741. * > * * * * *
  742. * >
  743. * > ****
  744. * > ```
  745. */
  746. class MDHorizontalRuleBlockReader extends MDBlockReader {
  747. static #horizontalRuleRegex = /^\s*(?:\-(?:\s*\-){2,}|\*(?:\s*\*){2,})\s*$/;
  748. constructor(priority=25.0) {
  749. super(priority);
  750. }
  751. /**
  752. * @param {MDState} state
  753. * @returns {MDBlock|null}
  754. */
  755. readBlock(state) {
  756. var p = state.p;
  757. let line = state.lines[p++];
  758. var modifier;
  759. [line, modifier] = MDTagModifier.fromLine(line);
  760. if (MDHorizontalRuleBlockReader.#horizontalRuleRegex.exec(line)) {
  761. state.p = p;
  762. let block = new MDHorizontalRuleBlock();
  763. if (modifier) modifier.applyTo(block);
  764. return block;
  765. }
  766. return null;
  767. }
  768. }
  769. /**
  770. * Block reader for tables.
  771. *
  772. * Examples:
  773. *
  774. * > ```markdown
  775. * > Name | Age
  776. * > --- | ---
  777. * > Joe | 34
  778. * > Alice | 25
  779. * >
  780. * > | Leading | And Trailing |
  781. * > | - | - |
  782. * > | Required | for single column tables |
  783. * >
  784. * > | Left aligned column | Center aligned | Right aligned |
  785. * > | :-- | :--: | --: |
  786. * > | Joe | x | 34 |
  787. * > ```
  788. */
  789. class MDTableBlockReader extends MDBlockReader {
  790. constructor(priority=30.0) {
  791. super(priority);
  792. }
  793. /**
  794. * @param {MDState} state
  795. * @param {boolean} isHeader
  796. * @return {MDTableRowBlock|null}
  797. */
  798. #readTableRow(state, isHeader) {
  799. if (!state.hasLines(1)) return null;
  800. var p = state.p;
  801. let line = MDTagModifier.strip(state.lines[p++].trim());
  802. if (/.*\|.*/.exec(line) === null) return null;
  803. if (line.startsWith('|')) line = line.substring(1);
  804. if (line.endsWith('|')) line = line.substring(0, line.length - 1);
  805. let cellTokens = line.split('|');
  806. let cells = cellTokens.map(function(token) {
  807. let content = state.inlineMarkdownToSpan(token);
  808. return isHeader ? new MDTableHeaderCellBlock(content) : new MDTableCellBlock(content);
  809. });
  810. state.p = p;
  811. return new MDTableRowBlock(cells);
  812. }
  813. /**
  814. * @param {string} line
  815. * @returns {string[]}
  816. */
  817. #parseColumnAlignments(line) {
  818. line = line.trim();
  819. if (line.startsWith('|')) line = line.substring(1);
  820. if (line.endsWith('|')) line = line.substring(0, line.length - 1);
  821. return line.split(/\s*\|\s*/).map(function(token) {
  822. if (token.startsWith(':')) {
  823. if (token.endsWith(':')) {
  824. return MDTableCellBlock.AlignCenter;
  825. }
  826. return MDTableCellBlock.AlignLeft;
  827. } else if (token.endsWith(':')) {
  828. return MDTableCellBlock.AlignRight;
  829. }
  830. return null;
  831. });
  832. }
  833. static #tableDividerRegex = /^\s*[|]?\s*(?:[:]?-+[:]?)(?:\s*\|\s*[:]?-+[:]?)*\s*[|]?\s*$/;
  834. readBlock(state) {
  835. if (!state.hasLines(2)) return null;
  836. let startP = state.p;
  837. let firstLine = state.lines[startP];
  838. var modifier = MDTagModifier.fromLine(firstLine)[1];
  839. let headerRow = this.#readTableRow(state, true);
  840. if (headerRow === null) {
  841. state.p = startP;
  842. return null;
  843. }
  844. let dividerLine = state.lines[state.p++];
  845. let dividerGroups = MDTableBlockReader.#tableDividerRegex.exec(dividerLine);
  846. if (dividerGroups === null) {
  847. state.p = startP;
  848. return null;
  849. }
  850. let columnAlignments = this.#parseColumnAlignments(dividerLine);
  851. headerRow.applyAlignments(columnAlignments);
  852. var bodyRows = [];
  853. while (state.hasLines(1)) {
  854. let row = this.#readTableRow(state, false);
  855. if (row === null) break;
  856. row.applyAlignments(columnAlignments);
  857. bodyRows.push(row);
  858. }
  859. let table = new MDTableBlock(headerRow, bodyRows);
  860. if (modifier) modifier.applyTo(table);
  861. return table;
  862. }
  863. }
  864. /**
  865. * Block reader for definition lists. Definitions go directly under terms starting
  866. * with a colon.
  867. *
  868. * Example:
  869. *
  870. * > ```markdown
  871. * > markdown
  872. * > : a language for generating HTML from simplified syntax
  873. * > parser
  874. * > : code that converts human-readable code into machine language
  875. * > ```
  876. */
  877. class MDDefinitionListBlockReader extends MDBlockReader {
  878. constructor(priority=35.0) {
  879. super(priority);
  880. }
  881. readBlock(state) {
  882. var p = state.p;
  883. var groups;
  884. var termCount = 0;
  885. var definitionCount = 0;
  886. var defLines = [];
  887. while (state.hasLines(1, p)) {
  888. let line = state.lines[p++];
  889. if (line.trim().length == 0) {
  890. p--;
  891. break;
  892. }
  893. if (/^\s+/.exec(line)) {
  894. if (defLines.length == 0) return null;
  895. defLines[defLines.length - 1] += "\n" + line;
  896. } else if (/^:\s+/.exec(line)) {
  897. defLines.push(line);
  898. definitionCount++;
  899. } else {
  900. defLines.push(line);
  901. termCount++;
  902. }
  903. }
  904. if (termCount == 0 || definitionCount == 0) return null;
  905. let blocks = defLines.map(function(line) {
  906. if (groups = /^:\s+(.*)$/.exec(line)) {
  907. return new MDDefinitionDefinitionBlock(state.inlineMarkdownToSpans(groups[1]));
  908. } else {
  909. return new MDDefinitionTermBlock(state.inlineMarkdownToSpans(line));
  910. }
  911. });
  912. state.p = p;
  913. return new MDDefinitionListBlock(blocks);
  914. }
  915. }
  916. /**
  917. * Block reader for defining footnote contents. Footnotes can be defined anywhere
  918. * in the document but will always be rendered at the end of a page or end of
  919. * the document.
  920. *
  921. * Examples:
  922. *
  923. * > ```markdown
  924. * > [^1]: Content of a footnote. Anywhere `[^1]` appears in the
  925. * > main text, it will hyperlink to this content at the bottom
  926. * > of the document. There will also be backlinks at the end
  927. * > of this footnote to all references to it.
  928. * > ```
  929. */
  930. class MDFootnoteDefinitionBlockReader extends MDBlockReader {
  931. constructor(priority=40.0) {
  932. super(priority);
  933. }
  934. /**
  935. * @param {MDState} state
  936. */
  937. readBlock(state) {
  938. var p = state.p;
  939. let groups = /^\s*\[\^\s*([^\]]+)\s*\]:\s+(.*)\s*$/.exec(state.lines[p++]);
  940. if (groups === null) return null;
  941. let symbol = groups[1];
  942. let def = groups[2];
  943. while (state.hasLines(1, p)) {
  944. let line = state.lines[p++];
  945. if (/^\s+/.exec(line)) {
  946. def += "\n" + line;
  947. } else {
  948. p--;
  949. break;
  950. }
  951. }
  952. state.p = p;
  953. let content = state.inlineMarkdownToSpan(def);
  954. state.defineFootnote(symbol, content);
  955. state.p = p;
  956. return new MDMultiBlock([]);
  957. }
  958. }
  959. /**
  960. * Block reader for abbreviation definitions. Anywhere the abbreviation appears
  961. * in the text will have its definition available when hovering over it.
  962. * Definitions can appear anywhere in the document. Their content should only
  963. * contain simple text, not markdown.
  964. *
  965. * Example:
  966. *
  967. * > ```markdown
  968. * > *[HTML]: Hyper Text Markup Language
  969. * > ```
  970. */
  971. class MDAbbreviationDefinitionBlockReader extends MDBlockReader {
  972. constructor(priority=45.0) {
  973. super(priority);
  974. }
  975. readBlock(state) {
  976. var p = state.p;
  977. let line = state.lines[p++];
  978. let groups = /^\s*\*\[([^\]]+?)\]:\s+(.*?)\s*$/.exec(line);
  979. if (groups === null) return null;
  980. let abbrev = groups[1];
  981. let def = groups[2];
  982. state.defineAbbreviation(abbrev, def);
  983. state.p = p;
  984. return new MDMultiBlock([]);
  985. }
  986. }
  987. /**
  988. * Block reader for URL definitions. Links in the document can include a
  989. * reference instead of a verbatim URL so it can be defined in one place and
  990. * reused in many places. These can be defined anywhere in the document. Nothing
  991. * of the definition is rendered in the document.
  992. *
  993. * Example:
  994. *
  995. * > ```markdown
  996. * > [foo]: https://example.com
  997. * > ```
  998. */
  999. class MDURLDefinitionBlockReader extends MDBlockReader {
  1000. constructor(priority=50.0) {
  1001. super(priority);
  1002. }
  1003. readBlock(state) {
  1004. var p = state.p;
  1005. let line = state.lines[p++];
  1006. var symbol;
  1007. var url;
  1008. var title = null;
  1009. let groups = /^\s*\[(.+?)]:\s*(\S+)\s+"(.*?)"\s*$/.exec(line);
  1010. if (groups) {
  1011. symbol = groups[1];
  1012. url = groups[2];
  1013. title = groups[3];
  1014. } else {
  1015. groups = /^\s*\[(.+?)]:\s*(\S+)\s*$/.exec(line);
  1016. if (groups) {
  1017. symbol = groups[1];
  1018. url = groups[2];
  1019. } else {
  1020. return null;
  1021. }
  1022. }
  1023. state.defineURL(symbol, url, title);
  1024. state.p = p;
  1025. return new MDInlineBlock([]);
  1026. }
  1027. }
  1028. /**
  1029. * Block reader for simple paragraphs. Paragraphs are separated by a blank (or
  1030. * whitespace-only) line. This reader should be prioritized last since there
  1031. * is no distinguishing syntax.
  1032. *
  1033. * Example:
  1034. *
  1035. * > ```markdown
  1036. * > Lorem ipsum dolor
  1037. * > sit amet. This is all one paragraph.
  1038. * >
  1039. * > Beginning of a new paragraph.
  1040. * > ```
  1041. */
  1042. class MDParagraphBlockReader extends MDBlockReader {
  1043. constructor(priority=100.0) {
  1044. super(priority);
  1045. }
  1046. readBlock(state) {
  1047. var paragraphLines = [];
  1048. var p = state.p;
  1049. while (p < state.lines.length) {
  1050. let line = state.lines[p++];
  1051. if (line.trim().length == 0) {
  1052. break;
  1053. }
  1054. paragraphLines.push(line);
  1055. }
  1056. if (paragraphLines.length > 0) {
  1057. state.p = p;
  1058. let content = paragraphLines.join("\n");
  1059. return new MDParagraphBlock(new MDInlineBlock(state.inlineMarkdownToSpans(content)));
  1060. }
  1061. return null;
  1062. }
  1063. }
  1064. // -- Inline reader ---------------------------------------------------------
  1065. class MDInlineReader {
  1066. /** @type {number} */
  1067. #tokenizePriority;
  1068. /** @type {number|number[]} */
  1069. #substitutePriority;
  1070. /**
  1071. * A unitless relative tokenizing priority value that determines which
  1072. * readers are tried first. Lower values are tried first. Standard readers
  1073. * are in the range of 0.0 to 100.0 but any value is valid. Longer
  1074. * tokens should generally be prioritized over short or single character tokens.
  1075. *
  1076. * @returns {number} priority or priorities for tokenization
  1077. */
  1078. get tokenizePriority() { return this.#tokenizePriority; }
  1079. /**
  1080. * A unitless relative substitution priority value that determines which
  1081. * readers are tried first. Lower values are tried first. If an array of
  1082. * values is given, the same reader will be included twice in the
  1083. * prioritization. This allows for multiple passes. Standard readers
  1084. * are in the range of 0.0 to 100.0 but any value is valid. Priority should
  1085. * be used to help resolve ambiguous parsings, with longer, more complex
  1086. * constructions best prioritized before shorter, simpler ones.
  1087. *
  1088. * @returns {number|number[]} priority or priorities for substitution
  1089. */
  1090. get substitutePriority() { return this.#substitutePriority; }
  1091. /**
  1092. * @param {number} tokenizePriority
  1093. * @param {number|number[]} substitutePriority
  1094. */
  1095. constructor(tokenizePriority, substitutePriority) {
  1096. this.#tokenizePriority = tokenizePriority;
  1097. this.#substitutePriority = substitutePriority;
  1098. }
  1099. /**
  1100. * Attempts to read a token from the start of the given string.
  1101. *
  1102. * @param {string} remainingText - remainder of the current line of markdown text left to tokenize
  1103. * @returns {MDToken|null} a token or `null` if not found
  1104. */
  1105. readFirstToken(state, priority, remainingText) {
  1106. throw new Error(`Abstract readFirstToken must be overridden in ${this.constructor.name}`);
  1107. }
  1108. /**
  1109. * Attempts to substitute one or more tokens in the given array. The given
  1110. * array is edited in-place via `.splice` operations. It consists of mixed
  1111. * elements of unprocessed `MDToken` and interpreted `MDSpan` elements.
  1112. *
  1113. * If a structure consists of inner content that is also markdown encoded,
  1114. * those elements can be passed to `state.tokensToSpans` to resolve to an
  1115. * array of `MDSpan` elements.
  1116. *
  1117. * @param {MDState} state
  1118. * @param {number} priority - used to differentiate passes when multiple
  1119. * values of `this.substitutePriority` are given
  1120. * @param {MDToken[]|MDSpan[]} tokens - mixed array of `MDToken` and
  1121. * `MDSpan` elements to be modified by reference
  1122. * @returns {boolean} `true` if any substitutions were made; `false` if not
  1123. */
  1124. substituteTokens(state, priority, tokens) {
  1125. throw new Error(`Abstract substituteTokens must be overridden in ${this.constructor.name}`);
  1126. }
  1127. /**
  1128. * Called after the full document has been generated for optional
  1129. * post-processing.
  1130. *
  1131. * @param {MDState} state
  1132. * @param {MDBlock[]} blocks - top-level document block list
  1133. */
  1134. postProcess(state, blocks) {
  1135. // no op
  1136. }
  1137. }
  1138. /**
  1139. * Abstract base class for readers that look for one or more delimiting tokens
  1140. * around some content.
  1141. */
  1142. class MDSimplePairInlineReader extends MDInlineReader {
  1143. constructor(tokenizePriority, substitutePriority) {
  1144. super(tokenizePriority, substitutePriority);
  1145. }
  1146. /**
  1147. * Attempts a substitution of a matched pair of delimiting token types.
  1148. * If successful, the substitution is performed on `tokens` and `true` is
  1149. * returned, otherwise `false` is returned and the array is untouched.
  1150. *
  1151. * If multiple `substitutePriority` values are specified, the first pass
  1152. * will reject matches with the delimiting character inside the content
  1153. * tokens. If a single `substitutePriority` is given or a subsequent pass
  1154. * is performed with multiple values any contents will be accepted.
  1155. *
  1156. * @param {MDState} state
  1157. * @param {number} priority
  1158. * @param {MDToken[]} tokens
  1159. * @param {class} spanClass
  1160. * @param {MDTokenType} delimiter
  1161. * @param {number} count - how many times the token is repeated to form the delimiter
  1162. * @returns {boolean} `true` if substitution performed, `false` if not
  1163. */
  1164. attemptPair(state, priority, tokens, spanClass, delimiter, count=1, plaintext=false) {
  1165. let delimiters = Array(count).fill(delimiter);
  1166. let firstPassPriority = (this.substitutePriority instanceof Array) ? this.substitutePriority[0] : null;
  1167. let match = MDToken.findPairedTokens(tokens, delimiters, delimiters, function(content) {
  1168. const firstType = content[0] instanceof MDToken ? content[0].type : null;
  1169. const lastType = content[content.length - 1] instanceof MDToken ? content[content.length - 1].type : null;
  1170. if (firstType == MDTokenType.Whitespace) return false;
  1171. if (lastType == MDTokenType.Whitespace) return false;
  1172. if (priority == firstPassPriority) {
  1173. var innerCount = 0;
  1174. for (let token of content) {
  1175. if (token instanceof MDToken && token.type == delimiter) innerCount++;
  1176. }
  1177. if ((innerCount % 2) != 0) return false;
  1178. }
  1179. return true;
  1180. });
  1181. if (match === null) return false;
  1182. let content = (plaintext)
  1183. ? match.contentTokens.map((token) => token.original).join('')
  1184. : state.tokensToSpans(match.contentTokens);
  1185. tokens.splice(match.startIndex, match.totalLength, new spanClass(content));
  1186. return true;
  1187. }
  1188. }
  1189. class MDStrongInlineReader extends MDSimplePairInlineReader {
  1190. constructor(tokenizePriority=0.0, substitutePriority=[0.0, 50.0]) {
  1191. super(tokenizePriority, substitutePriority);
  1192. }
  1193. readFirstToken(state, priority, remainingText) {
  1194. if (remainingText.startsWith('*')) return new MDToken('*', MDTokenType.Asterisk);
  1195. if (remainingText.startsWith('_')) return new MDToken('_', MDTokenType.Underscore);
  1196. return null;
  1197. }
  1198. substituteTokens(state, priority, tokens) {
  1199. if (this.attemptPair(state, priority, tokens, MDStrongSpan, MDTokenType.Asterisk, 2)) return true;
  1200. if (this.attemptPair(state, priority, tokens, MDStrongSpan, MDTokenType.Underscore, 2)) return true;
  1201. return false;
  1202. }
  1203. }
  1204. class MDEmphasisInlineReader extends MDSimplePairInlineReader {
  1205. constructor(tokenizePriority=0.0, substitutePriority=[0.0, 50.0]) {
  1206. super(tokenizePriority, substitutePriority);
  1207. }
  1208. readFirstToken(state, priority, remainingText) {
  1209. if (remainingText.startsWith('*')) return new MDToken('*', MDTokenType.Asterisk);
  1210. if (remainingText.startsWith('_')) return new MDToken('_', MDTokenType.Underscore);
  1211. return null;
  1212. }
  1213. substituteTokens(state, priority, tokens) {
  1214. if (this.attemptPair(state, priority, tokens, MDEmphasisSpan, MDTokenType.Asterisk)) return true;
  1215. if (this.attemptPair(state, priority, tokens, MDEmphasisSpan, MDTokenType.Underscore)) return true;
  1216. return false;
  1217. }
  1218. }
  1219. class MDCodeInlineReader extends MDSimplePairInlineReader {
  1220. constructor(tokenizePriority=0.0, substitutePriority=0.0) {
  1221. super(tokenizePriority, substitutePriority);
  1222. }
  1223. readFirstToken(state, priority, remainingText) {
  1224. if (remainingText.startsWith('`')) return new MDToken('`', MDTokenType.Backtick);
  1225. return null;
  1226. }
  1227. substituteTokens(state, priority, tokens) {
  1228. // ignore priority
  1229. if (this.attemptPair(state, -1, tokens, MDCodeSpan, MDTokenType.Backtick, 2, true)) return true;
  1230. if (this.attemptPair(state, -1, tokens, MDCodeSpan, MDTokenType.Backtick, 1, true)) return true;
  1231. return false;
  1232. }
  1233. }
  1234. class MDStrikethroughInlineReader extends MDSimplePairInlineReader {
  1235. constructor(tokenizePriority=0.0, substitutePriority=[0.0, 50.0]) {
  1236. super(tokenizePriority, substitutePriority);
  1237. }
  1238. readFirstToken(state, priority, remainingText) {
  1239. if (remainingText.startsWith('~')) return new MDToken('~', MDTokenType.Tilde);
  1240. return null;
  1241. }
  1242. substituteTokens(state, priority, tokens) {
  1243. if (this.attemptPair(state, priority, tokens, MDStrikethroughSpan, MDTokenType.Tilde, 2)) return true;
  1244. if (this.attemptPair(state, priority, tokens, MDStrikethroughSpan, MDTokenType.Tilde)) return true;
  1245. return false;
  1246. }
  1247. }
  1248. class MDImageInlineReader extends MDInlineReader {
  1249. constructor(tokenizePriority=0.0, substitutePriority=0.0) {
  1250. super(tokenizePriority, substitutePriority);
  1251. }
  1252. readFirstToken(state, priority, remainingText) {
  1253. if (remainingText.startsWith('!')) return new MDToken('!', MDTokenType.Bang);
  1254. var groups;
  1255. if (groups = MDUtils.tokenizeLabel(remainingText)) {
  1256. return new MDToken(groups[0], MDTokenType.Label, groups[1]);
  1257. }
  1258. if (groups = MDUtils.tokenizeURL(remainingText)) {
  1259. return new MDToken(groups[0], MDTokenType.URL, groups[1], groups[2]);
  1260. }
  1261. return null;
  1262. }
  1263. substituteTokens(state, priority, tokens) {
  1264. var match;
  1265. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.Bang, MDTokenType.Label, MDTokenType.META_OptionalWhitespace, MDTokenType.URL ])) {
  1266. let alt = match.tokens[1].content;
  1267. let url = match.tokens[match.tokens.length - 1].content;
  1268. let title = match.tokens[match.tokens.length - 1].extra;
  1269. tokens.splice(match.index, match.tokens.length, new MDImageSpan(url, alt, title));
  1270. return true;
  1271. }
  1272. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.Bang, MDTokenType.Label, MDTokenType.META_OptionalWhitespace, MDTokenType.Label ])) {
  1273. let alt = match.tokens[1].content;
  1274. let ref = match.tokens[match.tokens.length - 1].content;
  1275. tokens.splice(match.index, match.tokens.length, new MDReferencedImageSpan(ref, alt));
  1276. return true;
  1277. }
  1278. return false;
  1279. }
  1280. }
  1281. class MDFootnoteInlineReader extends MDInlineReader {
  1282. static #footnoteWithTitleRegex = /^\[\^([^\]]+?)\s+"(.*?)"\]/; // 1=symbol, 2=title
  1283. static #footnoteRegex = /^\[\^([^\]]+?)\]/; // 1=symbol
  1284. constructor(tokenizePriority=0.0, substitutePriority=0.0) {
  1285. super(tokenizePriority, substitutePriority);
  1286. }
  1287. readFirstToken(state, priority, remainingText) {
  1288. var groups;
  1289. if (groups = MDFootnoteInlineReader.#footnoteWithTitleRegex.exec(remainingText)) {
  1290. return new MDToken(groups[0], MDTokenType.Footnote, groups[1], groups[2]);
  1291. }
  1292. if (groups = MDFootnoteInlineReader.#footnoteRegex.exec(remainingText)) {
  1293. return new MDToken(groups[0], MDTokenType.Footnote, groups[1]);
  1294. }
  1295. return null;
  1296. }
  1297. substituteTokens(state, priority, tokens) {
  1298. var match;
  1299. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.Footnote ])) {
  1300. let footnoteToken = match.tokens[0];
  1301. tokens.splice(match.index, 1, new MDFootnoteReferenceSpan(footnoteToken.content));
  1302. return true;
  1303. }
  1304. return false;
  1305. }
  1306. /**
  1307. * @param {MDState} state
  1308. * @param {MDBlock[]} blocks
  1309. */
  1310. postProcess(state, blocks) {
  1311. var unique = 1;
  1312. for (const block of blocks) {
  1313. block.visitChildren(function(node) {
  1314. if (!(node instanceof MDFootnoteReferenceSpan)) return;
  1315. node.differentiator = unique++;
  1316. state.registerUniqueFootnote(node.symbol, node.differentiator);
  1317. });
  1318. }
  1319. if (Object.keys(state.footnotes).length == 0) return;
  1320. blocks.push(new MDFootnoteListingBlock());
  1321. }
  1322. }
  1323. class MDLinkInlineReader extends MDInlineReader {
  1324. constructor(tokenizePriority=0.0, substitutePriority=0.0) {
  1325. super(tokenizePriority, substitutePriority);
  1326. }
  1327. readFirstToken(state, priority, remainingText) {
  1328. var groups;
  1329. if (groups = MDUtils.tokenizeLabel(remainingText)) {
  1330. return new MDToken(groups[0], MDTokenType.Label, groups[1]);
  1331. }
  1332. if (groups = MDUtils.tokenizeEmail(remainingText)) {
  1333. return new MDToken(groups[0], MDTokenType.Email, groups[1], groups[2]);
  1334. }
  1335. if (groups = MDUtils.tokenizeURL(remainingText)) {
  1336. return new MDToken(groups[0], MDTokenType.URL, groups[1], groups[2]);
  1337. }
  1338. return null;
  1339. }
  1340. /**
  1341. * @param {MDState} state
  1342. */
  1343. substituteTokens(state, priority, tokens) {
  1344. var match;
  1345. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.Label, MDTokenType.META_OptionalWhitespace, MDTokenType.URL ])) {
  1346. let text = match.tokens[0].content;
  1347. let url = match.tokens[match.tokens.length - 1].content;
  1348. let title = match.tokens[match.tokens.length - 1].extra;
  1349. tokens.splice(match.index, match.tokens.length, new MDLinkSpan(url, state.inlineMarkdownToSpan(text), title));
  1350. return true;
  1351. }
  1352. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.Label, MDTokenType.META_OptionalWhitespace, MDTokenType.Email ])) {
  1353. let text = match.tokens[0].content;
  1354. let email = match.tokens[match.tokens.length - 1].content;
  1355. let url = `mailto:${email}`;
  1356. let title = match.tokens[match.tokens.length - 1].extra;
  1357. tokens.splice(match.index, match.tokens.length, new MDLinkSpan(url, state.inlineMarkdownToSpan(text), title));
  1358. return true;
  1359. }
  1360. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.Label, MDTokenType.META_OptionalWhitespace, MDTokenType.Label ])) {
  1361. let text = match.tokens[0].content;
  1362. let ref = match.tokens[match.tokens.length - 1].content;
  1363. tokens.splice(match.index, match.tokens.length, new MDReferencedLinkSpan(ref, state.inlineMarkdownToSpan(text)));
  1364. return true;
  1365. }
  1366. return false;
  1367. }
  1368. }
  1369. class MDSimpleLinkInlineReader extends MDInlineReader {
  1370. static #simpleEmailRegex = new RegExp("^<(" + MDUtils.baseEmailRegex.source + ")>", "i"); // 1=email
  1371. static #simpleURLRegex = new RegExp("^<(" + MDUtils.baseURLRegex.source + ")>", "i"); // 1=URL
  1372. constructor(tokenizePriority=0.0, substitutePriority=0.0) {
  1373. super(tokenizePriority, substitutePriority);
  1374. }
  1375. readFirstToken(state, priority, remainingText) {
  1376. var groups;
  1377. if (groups = MDSimpleLinkInlineReader.#simpleEmailRegex.exec(remainingText)) {
  1378. return new MDToken(groups[0], MDTokenType.SimpleEmail, groups[1]);
  1379. }
  1380. if (groups = MDSimpleLinkInlineReader.#simpleURLRegex.exec(remainingText)) {
  1381. return new MDToken(groups[0], MDTokenType.SimpleLink, groups[1]);
  1382. }
  1383. return null;
  1384. }
  1385. #substituteEmail(state, tokens) {
  1386. const result = MDToken.findFirstTokens(tokens, [ MDTokenType.SimpleEmail ]);
  1387. if (result === null) return false;
  1388. /** @type {MDToken} */
  1389. const token = result.tokens[0];
  1390. const link = `mailto:${token.content}`;
  1391. const span = new MDLinkSpan(link, new MDObfuscatedTextSpan(token.content));
  1392. tokens.splice(result.index, 1, span);
  1393. return true;
  1394. }
  1395. #substituteURL(state, tokens) {
  1396. const result = MDToken.findFirstTokens(tokens, [ MDTokenType.SimpleLink ]);
  1397. if (result === null) return false;
  1398. /** @type {MDToken} */
  1399. const token = result.tokens[0];
  1400. const link = token.content;
  1401. const span = new MDLinkSpan(link, new MDTextSpan(link));
  1402. tokens.splice(result.index, 1, span);
  1403. return true;
  1404. }
  1405. substituteTokens(state, priority, tokens) {
  1406. if (this.#substituteEmail(state, tokens)) return true;
  1407. if (this.#substituteURL(state, tokens)) return true;
  1408. return false;
  1409. }
  1410. }
  1411. class MDHTMLTagInlineReader extends MDInlineReader {
  1412. constructor(tokenizePriority=0.0, substitutePriority=95.0) {
  1413. super(tokenizePriority, substitutePriority);
  1414. }
  1415. readFirstToken(state, priority, remainingText) {
  1416. var tag = MDHTMLTag.fromLineStart(remainingText);
  1417. if (tag) return new MDToken(tag.fullTag, MDTokenType.HTMLTag, tag.fullTag, null, tag);
  1418. return null;
  1419. }
  1420. substituteTokens(state, priority, tokens) {
  1421. const result = MDToken.findFirstTokens(tokens, [ MDTokenType.HTMLTag ]);
  1422. if (result === null) return false;
  1423. /** @type {MDToken} */
  1424. const token = result.tokens[0];
  1425. const tag = token.tag;
  1426. const span = new MDHTMLSpan(tag.fullTag);
  1427. tokens.splice(result.index, 1, span);
  1428. return true;
  1429. }
  1430. }
  1431. class MDModifierInlineReader extends MDInlineReader {
  1432. constructor(tokenizePriority=0.0, substitutePriority=100.0) {
  1433. super(tokenizePriority, substitutePriority);
  1434. }
  1435. readFirstToken(state, priority, remainingText) {
  1436. var modifier = MDTagModifier.fromStart(remainingText);
  1437. if (modifier) return new MDToken(modifier.original, MDTokenType.Modifier, modifier);
  1438. return null;
  1439. }
  1440. substituteTokens(state, priority, tokens) {
  1441. // Modifiers are applied elsewhere, and if they're not it's fine if they're
  1442. // rendered as the original syntax.
  1443. return false;
  1444. }
  1445. }
  1446. // -- Blocks ----------------------------------------------------------------
  1447. class MDBlock {
  1448. /** @type {string[]} */
  1449. cssClasses = [];
  1450. /** @type {string|null} */
  1451. cssId = null;
  1452. /** @type {object} */
  1453. attributes = {};
  1454. /**
  1455. * @param {MDState} state
  1456. */
  1457. toHTML(state) {
  1458. throw new Error(`Abstract toHTML must be overridden in ${self.constructor.name}`);
  1459. }
  1460. htmlAttributes() {
  1461. var html = '';
  1462. if (this.cssClasses.length > 0) {
  1463. html += ` class="${this.cssClasses.join(' ')}"`;
  1464. }
  1465. if (this.cssId !== null) {
  1466. html += ` id="${this.cssId}"`;
  1467. }
  1468. for (const name in this.attributes) {
  1469. let value = this.attributes[name];
  1470. html += ` ${name}="${MDUtils.escapeHTML(value)}"`;
  1471. }
  1472. return html;
  1473. }
  1474. /**
  1475. * @param {MDBlock[]} blocks
  1476. * @param {MDState} state
  1477. * @returns {string}
  1478. */
  1479. static toHTML(blocks, state) {
  1480. return blocks.map((block) => block.toHTML(state)).join("\n");
  1481. }
  1482. /**
  1483. * Visits all block and inline children of this block, calling the given
  1484. * function with each. Should be implemented for any block with child nodes.
  1485. *
  1486. * @param {function} fn
  1487. */
  1488. visitChildren(fn) {}
  1489. }
  1490. class MDMultiBlock extends MDBlock {
  1491. /** @type {MDBlock[]} */
  1492. #blocks;
  1493. /**
  1494. * @param {MDBlock[]} blocks
  1495. */
  1496. constructor(blocks) {
  1497. super();
  1498. if (blocks instanceof Array) {
  1499. this.#blocks = blocks;
  1500. } else {
  1501. throw new Error(`${MDUtils.typename(this)} expects MDBlock[], got ${MDUtils.typename(blocks)}`);
  1502. }
  1503. }
  1504. toHTML(state) {
  1505. return MDBlock.toHTML(this.#blocks, state);
  1506. }
  1507. visitChildren(fn) {
  1508. for (const block of this.#blocks) {
  1509. fn(block);
  1510. block.visitChildren(fn);
  1511. }
  1512. }
  1513. }
  1514. class MDParagraphBlock extends MDBlock {
  1515. /** @type {MDBlock[]} */
  1516. #content;
  1517. /**
  1518. * @param {MDBlock|MDBlock[]} content
  1519. */
  1520. constructor(content) {
  1521. super();
  1522. if (content instanceof Array) {
  1523. this.#content = content;
  1524. } else if (content instanceof MDBlock) {
  1525. this.#content = [ content ];
  1526. } else {
  1527. throw new Error(`${MDUtils.typename(this)} expects MDBlock[] or MDBlock, got ${MDUtils.typename(content)}`);
  1528. }
  1529. }
  1530. toHTML(state) {
  1531. const contentHTML = MDBlock.toHTML(this.#content, state);
  1532. return `<p${this.htmlAttributes()}>${contentHTML}</p>\n`;
  1533. }
  1534. visitChildren(fn) {
  1535. for (const child of this.#content) {
  1536. fn(child);
  1537. child.visitChildren(fn);
  1538. }
  1539. }
  1540. }
  1541. class MDHeaderBlock extends MDBlock {
  1542. /** @type {number} */
  1543. #level;
  1544. /** @type {MDBlock} */
  1545. #content;
  1546. /**
  1547. * @param {number} level
  1548. * @param {MDBlock} content
  1549. */
  1550. constructor(level, content) {
  1551. super();
  1552. this.#level = level;
  1553. this.#content = content;
  1554. }
  1555. toHTML(state) {
  1556. let contentHTML = this.#content.toHTML(state);
  1557. return `<h${this.#level}${this.htmlAttributes()}>${contentHTML}</h${this.level}>\n`;
  1558. }
  1559. visitChildren(fn) {
  1560. fn(this.content);
  1561. this.content.visitChildren(fn);
  1562. }
  1563. }
  1564. class MDBlockquoteBlock extends MDBlock {
  1565. /** @type {MDBlock[]} */
  1566. content;
  1567. /**
  1568. * @param {MDBlock|MDBlock[]} content
  1569. */
  1570. constructor(content) {
  1571. super();
  1572. this.content = (content instanceof MDBlock) ? [ content ] : content;
  1573. }
  1574. toHTML(state) {
  1575. let contentHTML = MDBlock.toHTML(this.content, state);
  1576. return `<blockquote${this.htmlAttributes()}>\n${contentHTML}\n</blockquote>`;
  1577. }
  1578. visitChildren(fn) {
  1579. for (const block of this.content) {
  1580. fn(block);
  1581. block.visitChildren(fn);
  1582. }
  1583. }
  1584. }
  1585. class MDUnorderedListBlock extends MDBlock {
  1586. /** @type {MDListItemBlock[]} */
  1587. items;
  1588. /**
  1589. * @param {MDListItemBlock[]} items
  1590. */
  1591. constructor(items) {
  1592. super();
  1593. this.items = items;
  1594. }
  1595. toHTML(state) {
  1596. let contentHTML = MDBlock.toHTML(this.items, state);
  1597. return `<ul${this.htmlAttributes()}>\n${contentHTML}\n</ul>`;
  1598. }
  1599. visitChildren(fn) {
  1600. for (const item of this.items) {
  1601. fn(item);
  1602. item.visitChildren(fn);
  1603. }
  1604. }
  1605. }
  1606. class MDOrderedListBlock extends MDBlock {
  1607. /** @type {MDListItemBlock[]} */
  1608. items;
  1609. /** @type {number|null} */
  1610. startOrdinal;
  1611. /**
  1612. * @param {MDListItemBlock[]} items
  1613. */
  1614. constructor(items, startOrdinal=null) {
  1615. super();
  1616. this.items = items;
  1617. this.startOrdinal = startOrdinal;
  1618. }
  1619. htmlAttributes() {
  1620. var html = super.htmlAttributes();
  1621. if (this.startOrdinal !== null) {
  1622. html += ` start="${this.startOrdinal}"`;
  1623. }
  1624. return html;
  1625. }
  1626. toHTML(state) {
  1627. let contentHTML = MDBlock.toHTML(this.items, state);
  1628. return `<ol${this.htmlAttributes()}>\n${contentHTML}\n</ol>`;
  1629. }
  1630. visitChildren(fn) {
  1631. for (const item of this.items) {
  1632. fn(item);
  1633. item.visitChildren(fn);
  1634. }
  1635. }
  1636. }
  1637. class MDListItemBlock extends MDBlock {
  1638. /** @type {MDBlock} */
  1639. content;
  1640. /** @type {number|null} */
  1641. ordinal;
  1642. /**
  1643. * @param {MDBlock} content
  1644. */
  1645. constructor(content, ordinal=null) {
  1646. super();
  1647. this.content = content;
  1648. this.ordinal = ordinal;
  1649. }
  1650. toHTML(state) {
  1651. let contentHTML = this.content.toHTML(state);
  1652. return `<li${this.htmlAttributes()}>${contentHTML}</li>`;
  1653. }
  1654. visitChildren(fn) {
  1655. fn(this.content);
  1656. this.content.visitChildren(fn);
  1657. }
  1658. }
  1659. class MDCodeBlock extends MDBlock {
  1660. /** @type {string} */
  1661. #code;
  1662. /**
  1663. * @param {string} code
  1664. */
  1665. constructor(code) {
  1666. super();
  1667. this.#code = code;
  1668. }
  1669. toHTML(state) {
  1670. return `<pre${this.htmlAttributes()}><code>${MDUtils.escapeHTML(this.#code)}</code></pre>`;
  1671. }
  1672. }
  1673. class MDHorizontalRuleBlock extends MDBlock {
  1674. toHTML(state) {
  1675. return `<hr${this.htmlAttributes()}>\n`;
  1676. }
  1677. }
  1678. class MDTableCellBlock extends MDBlock {
  1679. static AlignLeft = 'left';
  1680. static AlignCenter = 'center';
  1681. static AlignRight = 'right';
  1682. /** @type {MDBlock} */
  1683. #content;
  1684. /** @type {string|null} */
  1685. align = null;
  1686. /**
  1687. * @param {MDBlock} content
  1688. */
  1689. constructor(content) {
  1690. super();
  1691. this.#content = content;
  1692. }
  1693. #alignAttribute() {
  1694. switch (this.align) {
  1695. case MDTableCellBlock.AlignLeft: return ' align="left"';
  1696. case MDTableCellBlock.AlignCenter: return ' align="center"';
  1697. case MDTableCellBlock.AlignRight: return ' align="right"';
  1698. default: return '';
  1699. }
  1700. }
  1701. htmlAttributes() {
  1702. var html = super.htmlAttributes();
  1703. html += this.#alignAttribute();
  1704. return html;
  1705. }
  1706. toHTML(state) {
  1707. let contentHTML = this.#content.toHTML(state);
  1708. return `<td${this.htmlAttributes()}>${contentHTML}</td>`;
  1709. }
  1710. visitChildren(fn) {
  1711. fn(this.#content);
  1712. this.#content.visitChildren(fn);
  1713. }
  1714. }
  1715. class MDTableHeaderCellBlock extends MDTableCellBlock {
  1716. toHTML(state) {
  1717. let html = super.toHTML(state);
  1718. let groups = /^<td(.*)td>$/.exec(html);
  1719. return `<th${groups[1]}th>`;
  1720. }
  1721. }
  1722. class MDTableRowBlock extends MDBlock {
  1723. /** @type {MDTableCellBlock[]|MDTableHeaderCellBlock[]} */
  1724. #cells;
  1725. /**
  1726. * @param {MDTableCellBlock[]|MDTableHeaderCellBlock[]} cells
  1727. */
  1728. constructor(cells) {
  1729. super();
  1730. this.#cells = cells;
  1731. }
  1732. /**
  1733. * @param {string[]} alignments
  1734. */
  1735. applyAlignments(alignments) {
  1736. for (var i = 0; i < this.#cells.length; i++) {
  1737. let cell = this.#cells[i];
  1738. let align = i < alignments.length ? alignments[i] : null;
  1739. cell.align = align;
  1740. }
  1741. }
  1742. toHTML(state) {
  1743. let cellsHTML = MDBlock.toHTML(this.#cells, state);
  1744. return `<tr${this.htmlAttributes()}>\n${cellsHTML}\n</tr>`;
  1745. }
  1746. visitChildren(fn) {
  1747. for (const cell of this.#cells) {
  1748. fn(cell);
  1749. cell.visitChildren(fn);
  1750. }
  1751. }
  1752. }
  1753. class MDTableBlock extends MDBlock {
  1754. /** @type {MDTableRowBlock} */
  1755. #headerRow;
  1756. /** @type {MDTableRowBlock[]} */
  1757. #bodyRows;
  1758. /**
  1759. * @param {MDTableRowBlock} headerRow
  1760. * @param {MDTableRowBlock[]} bodyRows
  1761. */
  1762. constructor(headerRow, bodyRows) {
  1763. super();
  1764. this.#headerRow = headerRow;
  1765. this.#bodyRows = bodyRows;
  1766. }
  1767. toHTML(state) {
  1768. let headerRowHTML = this.#headerRow.toHTML(state);
  1769. let bodyRowsHTML = MDBlock.toHTML(this.#bodyRows, state);
  1770. return `<table${this.htmlAttributes()}>\n<thead>\n${headerRowHTML}\n</thead>\n<tbody>\n${bodyRowsHTML}\n</tbody>\n</table>`;
  1771. }
  1772. visitChildren(fn) {
  1773. fn(this.#headerRow);
  1774. this.#headerRow.visitChildren(fn);
  1775. for (const row of this.#bodyRows) {
  1776. fn(row);
  1777. row.visitChildren(fn);
  1778. }
  1779. }
  1780. }
  1781. class MDDefinitionListBlock extends MDBlock {
  1782. /** @type {MDBlock[]} */
  1783. #content;
  1784. /**
  1785. * @param {MDBlock[]} content
  1786. */
  1787. constructor(content) {
  1788. super();
  1789. this.#content = content;
  1790. }
  1791. toHTML(state) {
  1792. let contentHTML = MDBlock.toHTML(this.#content, state);
  1793. return `<dl${this.htmlAttributes()}>\n${contentHTML}\n</dl>`;
  1794. }
  1795. visitChildren(fn) {
  1796. for (const block of this.#content) {
  1797. fn(block);
  1798. block.visitChildren(fn);
  1799. }
  1800. }
  1801. }
  1802. class MDDefinitionTermBlock extends MDBlock {
  1803. /** @type {MDBlock} */
  1804. #content;
  1805. /**
  1806. * @param {MDBlock} content
  1807. */
  1808. constructor(content) {
  1809. super();
  1810. this.#content = content;
  1811. }
  1812. toHTML(state) {
  1813. let contentHTML = this.#content.toHTML(state);
  1814. return `<dt${this.htmlAttributes()}>${contentHTML}</dt>`;
  1815. }
  1816. visitChildren(fn) {
  1817. fn(this.#content);
  1818. this.#content.visitChildren(fn);
  1819. }
  1820. }
  1821. class MDDefinitionDefinitionBlock extends MDBlock {
  1822. /** @type {MDBlock} */
  1823. #content;
  1824. /**
  1825. * @param {MDBlock} content
  1826. */
  1827. constructor(content) {
  1828. super();
  1829. this.#content = content;
  1830. }
  1831. toHTML(state) {
  1832. let contentHTML = this.#content.toHTML(state);
  1833. return `<dd${this.htmlAttributes()}>${contentHTML}</dd>`;
  1834. }
  1835. visitChildren(fn) {
  1836. fn(this.#content);
  1837. this.#content.visitChildren(fn);
  1838. }
  1839. }
  1840. class MDFootnoteListingBlock extends MDBlock {
  1841. constructor() {
  1842. super();
  1843. }
  1844. /**
  1845. * @param {MDState} state
  1846. */
  1847. toHTML(state) {
  1848. const footnotes = state.footnotes;
  1849. var symbolOrder = Object.keys(footnotes);
  1850. if (Object.keys(footnotes).length == 0) return '';
  1851. const footnoteUniques = state.footnoteInstances;
  1852. var html = '';
  1853. html += '<div class="footnotes"><hr/>';
  1854. html += '<ol>';
  1855. for (const symbol of symbolOrder) {
  1856. /** @type {MDBlock} */
  1857. let content = footnotes[symbol];
  1858. if (!content) continue;
  1859. html += `<li value="${symbol}" id="footnote_${symbol}">${content.toHTML(state)}`;
  1860. for (const unique of footnoteUniques[symbol]) {
  1861. html += ` <a href="#footnoteref_${unique}" class="footnote-backref">↩︎</a>`;
  1862. }
  1863. html += `</li>\n`;
  1864. }
  1865. html += '</ol>';
  1866. html += '</div>';
  1867. return html;
  1868. }
  1869. }
  1870. class MDInlineBlock extends MDBlock {
  1871. /** @type {MDSpan[]} */
  1872. #content;
  1873. /**
  1874. * @param {MDSpan[]} content
  1875. */
  1876. constructor(content) {
  1877. super();
  1878. this.#content = content;
  1879. }
  1880. toHTML(state) {
  1881. return MDSpan.toHTML(this.#content, state);
  1882. }
  1883. visitChildren(fn) {
  1884. for (const span of this.#content) {
  1885. fn(span);
  1886. span.visitChildren(fn);
  1887. }
  1888. }
  1889. }
  1890. // -- Spans -----------------------------------------------------------------
  1891. class MDSpan {
  1892. /** @type {string[]} */
  1893. cssClasses = [];
  1894. /** @type {string|null} */
  1895. cssId = null;
  1896. /** @type {object} */
  1897. attributes = {};
  1898. /**
  1899. * @param {MDState} state
  1900. * @returns {string} HTML
  1901. */
  1902. toHTML(state) {
  1903. throw new Error(`Abstract toHTML must be overridden in ${self.constructor.name}`);
  1904. }
  1905. htmlAttributes() {
  1906. var html = '';
  1907. if (this.cssClasses.length > 0) {
  1908. html += ` class="${this.cssClasses.join(' ')}"`;
  1909. }
  1910. if (this.cssId !== null) {
  1911. html += ` id="${this.cssId}"`;
  1912. }
  1913. for (const name in this.attributes) {
  1914. let value = this.attributes[name];
  1915. html += ` ${name}="${MDUtils.escapeHTML(value)}"`;
  1916. }
  1917. return html;
  1918. }
  1919. /**
  1920. * @param {MDSpan[]} spans
  1921. * @param {MDState} state
  1922. */
  1923. static toHTML(spans, state) {
  1924. return spans.map((span) => span.toHTML(state)).join("");
  1925. }
  1926. /**
  1927. * Visits all inline children of this span, calling the given function with
  1928. * each. Should be implemented for any span with child nodes.
  1929. *
  1930. * @param {function} fn
  1931. */
  1932. visitChildren(fn) {}
  1933. }
  1934. class MDMultiSpan extends MDSpan {
  1935. /** @type {MDSpan[]} */
  1936. content;
  1937. /**
  1938. * @param {MDSpan[]} content
  1939. */
  1940. constructor(content) {
  1941. super();
  1942. this.content = content;
  1943. }
  1944. toHTML(state) {
  1945. return MDSpan.toHTML(this.content, state);
  1946. }
  1947. visitChildren(fn) {
  1948. for (const span of this.content) {
  1949. fn(span);
  1950. span.visitChildren(fn);
  1951. }
  1952. }
  1953. }
  1954. class MDTextSpan extends MDSpan {
  1955. /** @param {string} text */
  1956. text;
  1957. /**
  1958. * @param {string} text
  1959. */
  1960. constructor(text) {
  1961. super();
  1962. this.text = text;
  1963. }
  1964. toHTML(state) {
  1965. let html = MDUtils.escapeHTML(this.text);
  1966. let abbrevs = state.abbreviations;
  1967. let regexes = state.abbreviationRegexes;
  1968. for (const abbrev in abbrevs) {
  1969. let def = abbrevs[abbrev];
  1970. let regex = regexes[abbrev];
  1971. let escapedDef = MDUtils.escapeHTML(def);
  1972. html = html.replace(regex, `<abbr title="${escapedDef}">$1</abbr>`);
  1973. }
  1974. return html;
  1975. }
  1976. }
  1977. class MDHTMLSpan extends MDSpan {
  1978. /** @param {string} html */
  1979. html;
  1980. /**
  1981. * @param {string} html
  1982. */
  1983. constructor(html) {
  1984. super();
  1985. this.html = html;
  1986. }
  1987. toHTML(state) {
  1988. return this.html;
  1989. }
  1990. }
  1991. class MDObfuscatedTextSpan extends MDSpan {
  1992. /** @param {string} text */
  1993. text;
  1994. /**
  1995. * @param {string} text
  1996. */
  1997. constructor(text) {
  1998. super();
  1999. this.text = text;
  2000. }
  2001. toHTML(state) {
  2002. return MDUtils.escapeObfuscated(this.text);
  2003. }
  2004. }
  2005. class MDLinkSpan extends MDSpan {
  2006. /** @type {string} */
  2007. link;
  2008. /** @type {string|null} */
  2009. target = null;
  2010. /** @type {MDSpan} */
  2011. content;
  2012. /** @type {string|null} */
  2013. title = null;
  2014. /**
  2015. * @param {string} link
  2016. * @param {MDSpan} content
  2017. * @param {string|null} title
  2018. */
  2019. constructor(link, content, title=null) {
  2020. super();
  2021. this.link = link;
  2022. this.content = content;
  2023. this.title = title;
  2024. }
  2025. toHTML(state) {
  2026. var escapedLink;
  2027. if (this.link.startsWith('mailto:')) {
  2028. escapedLink = 'mailto:' + MDUtils.escapeObfuscated(this.link.substring(7));
  2029. } else {
  2030. escapedLink = MDUtils.escapeHTML(this.link);
  2031. }
  2032. var html = `<a href="${escapedLink}"`;
  2033. if (this.target) {
  2034. html += ` target="${MDUtils.escapeHTML(this.target)}"`;
  2035. }
  2036. if (this.title) {
  2037. html += ` title="${MDUtils.escapeHTML(this.title)}"`;
  2038. }
  2039. html += this.htmlAttributes();
  2040. html += '>' + this.content.toHTML(state) + '</a>';
  2041. return html;
  2042. }
  2043. visitChildren(fn) {
  2044. fn(this.content);
  2045. this.content.visitChildren(fn);
  2046. }
  2047. }
  2048. class MDReferencedLinkSpan extends MDLinkSpan {
  2049. /** @type {string} */
  2050. ref;
  2051. constructor(ref, content) {
  2052. super(null, content);
  2053. this.ref = ref;
  2054. }
  2055. /**
  2056. * @param {MDState} state
  2057. */
  2058. toHTML(state) {
  2059. if (!this.link) {
  2060. let url = state.urls[this.ref.toLowerCase()];
  2061. let title = state.urlTitles[this.ref.toLowerCase()];
  2062. this.link = url;
  2063. this.title = title || this.title;
  2064. }
  2065. if (this.link) {
  2066. return super.toHTML(state);
  2067. } else {
  2068. let contentHTML = this.content.toHTML(state);
  2069. return `[${contentHTML}][${this.ref}]`;
  2070. }
  2071. }
  2072. }
  2073. class MDEmphasisSpan extends MDSpan {
  2074. /** @type {MDSpan[]} */
  2075. #content;
  2076. /**
  2077. * @param {MDSpan|MDSpan[]} content
  2078. */
  2079. constructor(content) {
  2080. super();
  2081. this.#content = (content instanceof MDSpan) ? [ content ] : content;
  2082. }
  2083. toHTML(state) {
  2084. let contentHTML = MDSpan.toHTML(this.#content, state);
  2085. return `<em${this.htmlAttributes()}>${contentHTML}</em>`;
  2086. }
  2087. visitChildren(fn) {
  2088. for (const span of this.#content) {
  2089. fn(span);
  2090. span.visitChildren(fn);
  2091. }
  2092. }
  2093. }
  2094. class MDStrongSpan extends MDSpan {
  2095. /** @type {MDSpan[]} content */
  2096. #content;
  2097. /**
  2098. * @param {MDSpan|MDSpan[]} content
  2099. */
  2100. constructor(content) {
  2101. super();
  2102. this.#content = (content instanceof MDSpan) ? [content] : content;
  2103. }
  2104. toHTML(state) {
  2105. let contentHTML = MDSpan.toHTML(this.#content, state);
  2106. return `<strong${this.htmlAttributes()}>${contentHTML}</strong>`;
  2107. }
  2108. visitChildren(fn) {
  2109. for (const span of this.#content) {
  2110. fn(span);
  2111. span.visitChildren(fn);
  2112. }
  2113. }
  2114. }
  2115. class MDStrikethroughSpan extends MDSpan {
  2116. /** @type {MDSpan[]} content */
  2117. #content;
  2118. /**
  2119. * @param {MDSpan|MDSpan[]} content
  2120. */
  2121. constructor(content) {
  2122. super();
  2123. this.#content = (content instanceof MDSpan) ? [content] : content;
  2124. }
  2125. toHTML(state) {
  2126. let contentHTML = MDSpan.toHTML(this.#content, state);
  2127. return `<strike${this.htmlAttributes()}>${contentHTML}</strike>`;
  2128. }
  2129. visitChildren(fn) {
  2130. for (const span of this.#content) {
  2131. fn(span);
  2132. span.visitChildren(fn);
  2133. }
  2134. }
  2135. }
  2136. class MDCodeSpan extends MDSpan {
  2137. /** @type {String} content */
  2138. #content;
  2139. /**
  2140. * @param {String} content
  2141. */
  2142. constructor(content) {
  2143. super();
  2144. if (typeof content == 'string') {
  2145. this.#content = content;
  2146. } else {
  2147. throw new Error(`${this.constructor.name} content must be String, got ${typeof content}`);
  2148. }
  2149. }
  2150. toHTML(state) {
  2151. return `<code${this.htmlAttributes()}>${MDUtils.escapeHTML(this.#content)}</code>`;
  2152. }
  2153. }
  2154. class MDImageSpan extends MDSpan {
  2155. /** @type {string} */
  2156. source;
  2157. /** @type {string|null} */
  2158. alt;
  2159. /** @type {string|null} */
  2160. title;
  2161. /**
  2162. * @param {string} source - image URL
  2163. * @param {string} alt - alt text
  2164. * @param {string|null} title - optional title attribute
  2165. */
  2166. constructor(source, alt, title=null) {
  2167. super();
  2168. this.source = source;
  2169. this.alt = alt;
  2170. this.title = title;
  2171. }
  2172. toHTML(state) {
  2173. let html = `<img src="${MDUtils.escapeHTML(this.source)}"`;
  2174. if (this.alt) {
  2175. html += ` alt="${MDUtils.escapeHTML(this.alt)}"`;
  2176. }
  2177. if (this.title) {
  2178. html += ` title="${MDUtils.escapeHTML(this.title)}"`;
  2179. }
  2180. html += this.htmlAttributes();
  2181. html += '>';
  2182. return html;
  2183. }
  2184. }
  2185. class MDReferencedImageSpan extends MDImageSpan {
  2186. /** @type {string} */
  2187. ref;
  2188. /**
  2189. * @param {string} ref
  2190. * @param {string|null} alt
  2191. */
  2192. constructor(ref, alt) {
  2193. super(null, alt);
  2194. this.ref = ref;
  2195. }
  2196. toHTML(state) {
  2197. if (!this.source) {
  2198. let url = state.urls[this.ref.toLowerCase()];
  2199. let title = state.urlTitles[this.ref.toLowerCase()];
  2200. this.source = url;
  2201. this.title = title || this.title;
  2202. }
  2203. if (this.source) {
  2204. return super.toHTML(state);
  2205. } else {
  2206. return `![${MDUtils.escapeHTML(this.alt)}][${MDUtils.escapeHTML(this.ref)}]`;
  2207. }
  2208. }
  2209. }
  2210. class MDFootnoteReferenceSpan extends MDSpan {
  2211. /** @type {string} */
  2212. symbol;
  2213. /** @type {number|null} */
  2214. differentiator = null;
  2215. /**
  2216. * @param {string} symbol
  2217. */
  2218. constructor(symbol) {
  2219. super();
  2220. this.symbol = symbol;
  2221. }
  2222. toHTML(state) {
  2223. if (this.differentiator !== null) {
  2224. return `<sup id="footnoteref_${this.differentiator}"><a href="#footnote_${this.symbol}">${this.symbol}</a></sup>`;
  2225. }
  2226. return `<!--FNREF:{${this.symbol}}-->`;
  2227. }
  2228. }
  2229. // -- Other -----------------------------------------------------------------
  2230. class MDHTMLTag {
  2231. /** @type {string} */
  2232. fullTag;
  2233. /** @type {string} */
  2234. tagName;
  2235. /** @type {boolean} */
  2236. isCloser;
  2237. /** @type {object} */
  2238. attributes;
  2239. /**
  2240. * @param {string} fullTag
  2241. * @param {string} tagName
  2242. * @param {boolean} isCloser
  2243. * @param {object} attributes
  2244. */
  2245. constructor(fullTag, tagName, isCloser, attributes) {
  2246. this.fullTag = fullTag;
  2247. this.tagName = tagName;
  2248. this.isCloser = isCloser;
  2249. this.attributes = attributes;
  2250. }
  2251. static #htmlTagNameFirstRegex = /[a-z]/i;
  2252. static #htmlTagNameMedialRegex = /[a-z0-9]/i;
  2253. static #htmlAttributeNameFirstRegex = /[a-z]/i;
  2254. static #htmlAttributeNameMedialRegex = /[a-z0-9-]/i;
  2255. static #whitespaceCharRegex = /\s/;
  2256. /**
  2257. * @param {string} line
  2258. * @returns {MDHTMLTag|null} HTML tag if possible
  2259. */
  2260. static fromLineStart(line) {
  2261. let expectOpenBracket = 0;
  2262. let expectCloserOrName = 1;
  2263. let expectName = 2;
  2264. let expectAttributeNameOrEnd = 3;
  2265. let expectEqualsOrAttributeOrEnd = 4;
  2266. let expectAttributeValue = 5;
  2267. let expectCloseBracket = 6;
  2268. var isCloser = false;
  2269. var tagName = '';
  2270. var attributeName = '';
  2271. var attributeValue = '';
  2272. var attributeQuote = null;
  2273. var attributes = {};
  2274. var fullTag = null;
  2275. let endAttribute = function() {
  2276. if (attributeName.length > 0) {
  2277. if (attributeValue.length > 0 || attributeQuote) {
  2278. attributes[attributeName] = attributeValue;
  2279. } else {
  2280. attributes[attributeName] = true;
  2281. }
  2282. }
  2283. attributeName = '';
  2284. attributeValue = '';
  2285. attributeQuote = null;
  2286. };
  2287. var expect = expectOpenBracket;
  2288. for (var p = 0; p < line.length && fullTag === null; p++) {
  2289. let ch = line.substring(p, p + 1);
  2290. let isWhitespace = this.#whitespaceCharRegex.exec(ch) !== null;
  2291. switch (expect) {
  2292. case expectOpenBracket:
  2293. if (ch != '<') return null;
  2294. expect = expectCloserOrName;
  2295. break;
  2296. case expectCloserOrName:
  2297. if (ch == '/') {
  2298. isCloser = true;
  2299. } else {
  2300. p--;
  2301. }
  2302. expect = expectName;
  2303. break;
  2304. case expectName:
  2305. if (tagName.length == 0) {
  2306. if (this.#htmlTagNameFirstRegex.exec(ch) === null) return null;
  2307. tagName += ch;
  2308. } else {
  2309. if (this.#htmlTagNameMedialRegex.exec(ch)) {
  2310. tagName += ch;
  2311. } else {
  2312. p--;
  2313. expect = (isCloser) ? expectCloseBracket : expectAttributeNameOrEnd;
  2314. }
  2315. }
  2316. break;
  2317. case expectAttributeNameOrEnd:
  2318. if (attributeName.length == 0) {
  2319. if (isWhitespace) {
  2320. // skip whitespace
  2321. } else if (ch == '/') {
  2322. expect = expectCloseBracket;
  2323. } else if (ch == '>') {
  2324. fullTag = line.substring(0, p + 1);
  2325. break;
  2326. } else if (this.#htmlAttributeNameFirstRegex.exec(ch)) {
  2327. attributeName += ch;
  2328. } else {
  2329. return null;
  2330. }
  2331. } else if (isWhitespace) {
  2332. expect = expectEqualsOrAttributeOrEnd;
  2333. } else if (ch == '/') {
  2334. endAttribute();
  2335. expect = expectCloseBracket;
  2336. } else if (ch == '>') {
  2337. endAttribute();
  2338. fullTag = line.substring(0, p + 1);
  2339. break;
  2340. } else if (ch == '=') {
  2341. expect = expectAttributeValue;
  2342. } else if (this.#htmlAttributeNameMedialRegex.exec(ch)) {
  2343. attributeName += ch;
  2344. } else {
  2345. return null;
  2346. }
  2347. break;
  2348. case expectEqualsOrAttributeOrEnd:
  2349. if (ch == '=') {
  2350. expect = expectAttributeValue;
  2351. } else if (isWhitespace) {
  2352. // skip whitespace
  2353. } else if (ch == '/') {
  2354. expect = expectCloseBracket;
  2355. } else if (ch == '>') {
  2356. fullTag = line.substring(0, p + 1);
  2357. break;
  2358. } else if (this.#htmlAttributeNameFirstRegex.exec(ch)) {
  2359. endAttribute();
  2360. expect = expectAttributeNameOrEnd;
  2361. p--;
  2362. }
  2363. break;
  2364. case expectAttributeValue:
  2365. if (attributeValue.length == 0) {
  2366. if (attributeQuote === null) {
  2367. if (isWhitespace) {
  2368. // skip whitespace
  2369. } else if (ch == '"' || ch == "'") {
  2370. attributeQuote = ch;
  2371. } else {
  2372. attributeQuote = ''; // explicitly unquoted
  2373. p--;
  2374. }
  2375. } else {
  2376. if (ch === attributeQuote) {
  2377. // Empty string
  2378. endAttribute();
  2379. expect = expectAttributeNameOrEnd;
  2380. } else if (attributeQuote === '' && (ch == '/' || ch == '>')) {
  2381. return null;
  2382. } else {
  2383. attributeValue += ch;
  2384. }
  2385. }
  2386. } else {
  2387. if (ch === attributeQuote) {
  2388. endAttribute();
  2389. expect = expectAttributeNameOrEnd;
  2390. } else if (attributeQuote === '' && isWhitespace) {
  2391. endAttribute();
  2392. expect = expectAttributeNameOrEnd;
  2393. } else {
  2394. attributeValue += ch;
  2395. }
  2396. }
  2397. break;
  2398. case expectCloseBracket:
  2399. if (isWhitespace) {
  2400. // ignore whitespace
  2401. } else if (ch == '>') {
  2402. fullTag = line.substring(0, p + 1);
  2403. break;
  2404. }
  2405. break;
  2406. }
  2407. }
  2408. if (fullTag === null) return null;
  2409. endAttribute();
  2410. return new MDHTMLTag(fullTag, tagName, isCloser, attributes);
  2411. }
  2412. }
  2413. class MDState {
  2414. /** @type {string[]} */
  2415. #lines = [];
  2416. /** @type {object} */
  2417. #abbreviations = {};
  2418. /** @type {object} */
  2419. #abbreviationRegexes = {};
  2420. /**
  2421. * symbol:string -> content:MDBlock
  2422. * @type {object}
  2423. */
  2424. #footnotes = {};
  2425. /**
  2426. * symbol:string -> number[]
  2427. * @type {object}
  2428. */
  2429. #footnoteInstances = {};
  2430. /** @type {object} */
  2431. #urlDefinitions = {};
  2432. /** @type {object} */
  2433. #urlTitles = {};
  2434. /** @type {number} */
  2435. p = 0;
  2436. /** @type {MDState|null} */
  2437. #parent = null;
  2438. /**
  2439. * Block readers sorted by priority.
  2440. * @type {MDBlockReader[]} readers
  2441. */
  2442. #blockReadersByPriority = [];
  2443. /**
  2444. * Tuples of priority:number and MDInlineReader sorted by `tokenizePriority`.
  2445. * @type {Array}
  2446. */
  2447. #inlineReadersByTokenPriority = [];
  2448. /**
  2449. * Tuples of priority:number and MDInlineReader sorted by `substitutePriority`.
  2450. */
  2451. #inlineReadersBySubstitutePriority = [];
  2452. /** @returns {string[]} */
  2453. get lines() { return this.#lines; }
  2454. /** @returns {string|null} */
  2455. get currentLine() { return (this.p < this.#lines.length) ? this.#lines[this.p] : null; }
  2456. /** @returns {object} */
  2457. get abbreviations() {
  2458. return (this.#parent) ? this.#parent.abbreviations : this.#abbreviations;
  2459. }
  2460. /** @returns {object} */
  2461. get abbreviationRegexes() {
  2462. return (this.#parent) ? this.#parent.abbreviationRegexes : this.#abbreviationRegexes;
  2463. }
  2464. /** @returns {object} */
  2465. get footnotes() {
  2466. return (this.#parent) ? this.#parent.footnotes : this.#footnotes;
  2467. }
  2468. /** @returns {object} */
  2469. get footnoteInstances() {
  2470. return (this.#parent) ? this.#parent.footnoteInstances : this.#footnoteInstances;
  2471. }
  2472. /** @returns {object} */
  2473. get urls() {
  2474. return (this.#parent) ? this.#parent.urls : this.#urlDefinitions;
  2475. }
  2476. /** @returns {object} */
  2477. get urlTitles() {
  2478. return (this.#parent) ? this.#parent.urlTitles : this.#urlTitles;
  2479. }
  2480. /**
  2481. * Block readers sorted by priority.
  2482. * @type {MDBlockReader[]} readers
  2483. */
  2484. get blockReadersByPriority() {
  2485. return (this.#parent) ? this.#parent.blockReadersByPriority : this.#blockReadersByPriority;
  2486. }
  2487. set blockReadersByPriority(newValue) {
  2488. this.#blockReadersByPriority = newValue;
  2489. }
  2490. /**
  2491. * Tuples of priority:number and MDInlineReader sorted by `tokenizePriority`.
  2492. * @type {Array}
  2493. */
  2494. get inlineReadersByTokenPriority() {
  2495. return (this.#parent) ? this.#parent.inlineReadersByTokenPriority : this.#inlineReadersByTokenPriority;
  2496. }
  2497. set inlineReadersByTokenPriority(newValue) {
  2498. this.#inlineReadersByTokenPriority = newValue;
  2499. }
  2500. /**
  2501. * Tuples of priority:number and MDInlineReader sorted by `substitutePriority`.
  2502. */
  2503. get inlineReadersBySubstitutePriority() {
  2504. return (this.#parent) ? this.#parent.inlineReadersBySubstitutePriority : this.#inlineReadersBySubstitutePriority;
  2505. }
  2506. set inlineReadersBySubstitutePriority(newValue) {
  2507. this.#inlineReadersBySubstitutePriority = newValue;
  2508. }
  2509. /**
  2510. * @param {string[]} lines
  2511. */
  2512. constructor(lines) {
  2513. this.#lines = lines;
  2514. }
  2515. /**
  2516. * Creates a copy of this state with new lines. Useful for parsing nested
  2517. * content.
  2518. *
  2519. * @param {string[]} lines
  2520. * @returns {MDState} copied sub-state
  2521. */
  2522. copy(lines) {
  2523. let cp = new MDState(lines);
  2524. cp.#parent = this;
  2525. return cp;
  2526. }
  2527. /**
  2528. * Defines an abbreviation.
  2529. *
  2530. * @param {string} abbreviation - case sensitive
  2531. * @param {string} definition - brief definition of the abbreviation
  2532. */
  2533. defineAbbreviation(abbreviation, definition) {
  2534. if (this.#parent) {
  2535. this.#parent.defineAbbreviation(abbreviation, definition);
  2536. return;
  2537. }
  2538. this.#abbreviations[abbreviation] = definition;
  2539. const regex = new RegExp("\\b(" + abbreviation + ")\\b", "ig");
  2540. this.#abbreviationRegexes[abbreviation] = regex;
  2541. }
  2542. /**
  2543. * Defines content of a footnote.
  2544. *
  2545. * @param {string} symbol - footnote symbol (e.g. "1")
  2546. * @param {MDBlock} footnote - content of the footnote
  2547. */
  2548. defineFootnote(symbol, footnote) {
  2549. if (this.#parent) {
  2550. this.#parent.defineFootnote(symbol, footnote);
  2551. } else {
  2552. this.#footnotes[symbol] = footnote;
  2553. }
  2554. }
  2555. /**
  2556. * @param {string} symbol
  2557. * @param {number} unique
  2558. */
  2559. registerUniqueFootnote(symbol, unique) {
  2560. if (this.#parent) {
  2561. this.#parent.registerUniqueFootnote(symbol, unique);
  2562. } else {
  2563. var uniques = this.#footnoteInstances[symbol] || [];
  2564. uniques.push(unique);
  2565. this.#footnoteInstances[symbol] = uniques;
  2566. }
  2567. }
  2568. /**
  2569. * Defines the URL for a given reference symbol.
  2570. *
  2571. * @param {string} symbol - reference symbol
  2572. * @param {string} url - URL
  2573. * @param {string|null} title - mouseover title attribute for links
  2574. */
  2575. defineURL(symbol, url, title=null) {
  2576. if (this.#parent) {
  2577. this.#parent.defineURL(symbol, url, title);
  2578. } else {
  2579. this.#urlDefinitions[symbol.toLowerCase()] = url;
  2580. if (title !== null) {
  2581. this.#urlTitles[symbol.toLowerCase()] = title;
  2582. }
  2583. }
  2584. }
  2585. /**
  2586. * Tests if there are at least `minCount` lines available to read. If `p`
  2587. * is not provided it will be relative to `this.p`.
  2588. *
  2589. * @param {number} minCount - minimum number of lines
  2590. * @param {number|null} p - line pointer
  2591. * @returns {boolean} whether at least the given number of lines is available
  2592. */
  2593. hasLines(minCount, p=null) {
  2594. let relativeTo = (p === null) ? this.p : p;
  2595. return relativeTo + minCount <= this.lines.length;
  2596. }
  2597. /**
  2598. * Reads and returns an array of blocks from the current line pointer.
  2599. *
  2600. * @returns {MDBlock[]} parsed blocks
  2601. */
  2602. readBlocks() {
  2603. var blocks = [];
  2604. while (this.hasLines(1)) {
  2605. let block = this.#readNextBlock();
  2606. if (block) {
  2607. blocks.push(block);
  2608. } else {
  2609. break;
  2610. }
  2611. }
  2612. return blocks;
  2613. }
  2614. /**
  2615. * Attempts to read one block from the current line pointer. The pointer
  2616. * will be positioned just after the end of the block.
  2617. *
  2618. * @param {MDState} state
  2619. * @returns {MDBlock}
  2620. */
  2621. #readNextBlock() {
  2622. while (this.hasLines(1) && this.lines[this.p].trim().length == 0) {
  2623. this.p++;
  2624. }
  2625. if (!this.hasLines(1)) return null;
  2626. for (const reader of this.blockReadersByPriority) {
  2627. const block = reader.readBlock(this);
  2628. if (block) return block;
  2629. }
  2630. return null;
  2631. }
  2632. static #textWhitespaceRegex = /^(\s*)(?:(\S|\S.*\S)(\s*?))?$/; // 1=leading WS, 2=text, 3=trailing WS
  2633. /**
  2634. * @param {string} line
  2635. * @returns {MDToken[]}
  2636. */
  2637. #inlineMarkdownToTokens(line) {
  2638. if (this.#parent) return this.#parent.#inlineMarkdownToTokens(line);
  2639. var tokens = [];
  2640. var text = '';
  2641. var expectLiteral = false;
  2642. /**
  2643. * Flushes accumulated content in `text` to `tokens`.
  2644. */
  2645. const endText = function() {
  2646. if (text.length == 0) return;
  2647. const textGroups = MDState.#textWhitespaceRegex.exec(text);
  2648. if (textGroups !== null) {
  2649. if (textGroups[1].length > 0) {
  2650. tokens.push(new MDToken(textGroups[1], MDTokenType.Whitespace, textGroups[1]));
  2651. }
  2652. if (textGroups[2] !== undefined && textGroups[2].length > 0) {
  2653. tokens.push(new MDToken(textGroups[2], MDTokenType.Text, textGroups[2]));
  2654. }
  2655. if (textGroups[3] !== undefined && textGroups[3].length > 0) {
  2656. tokens.push(new MDToken(textGroups[3], MDTokenType.Whitespace, textGroups[3]));
  2657. }
  2658. } else {
  2659. tokens.push(new MDToken(text, MDTokenType.Text, text));
  2660. }
  2661. text = '';
  2662. }
  2663. for (var p = 0; p < line.length; p++) {
  2664. const ch = line.substring(p, p + 1);
  2665. const remainder = line.substring(p);
  2666. if (expectLiteral) {
  2667. text += ch;
  2668. expectLiteral = false;
  2669. continue;
  2670. }
  2671. if (ch == '\\') {
  2672. expectLiteral = true;
  2673. continue;
  2674. }
  2675. var found = false;
  2676. for (const readerTuple of this.inlineReadersByTokenPriority) {
  2677. /** @type {number} */
  2678. const priority = readerTuple[0];
  2679. /** @type {MDInlineReader} */
  2680. const reader = readerTuple[1];
  2681. const token = reader.readFirstToken(this, priority, remainder);
  2682. if (token === null) continue;
  2683. endText();
  2684. tokens.push(token);
  2685. p += token.original.length - 1;
  2686. found = true;
  2687. break;
  2688. }
  2689. if (!found) {
  2690. text += ch;
  2691. }
  2692. }
  2693. endText();
  2694. return tokens;
  2695. }
  2696. /**
  2697. * Converts a line of markdown to an MDSpan.
  2698. *
  2699. * @param {string|string[]} line
  2700. * @returns {MDSpan}
  2701. */
  2702. inlineMarkdownToSpan(line) {
  2703. let spans = this.inlineMarkdownToSpans(line);
  2704. return (spans.length == 1) ? spans[0] : new MDMultiSpan(spans);
  2705. }
  2706. /**
  2707. * Converts a line of markdown to an array of MDSpan.
  2708. *
  2709. * @param {string|string[]} line
  2710. * @returns {MDSpan[]}
  2711. */
  2712. inlineMarkdownToSpans(line) {
  2713. var tokens = this.#inlineMarkdownToTokens((line instanceof Array) ? line.join('\n') : line);
  2714. return this.tokensToSpans(tokens);
  2715. }
  2716. /**
  2717. * Converts a mixed array of `MDToken` and `MDSpan` elements into an array
  2718. * of only `MDSpan`.
  2719. *
  2720. * @param {MDToken[]|MDSpan[]} tokens
  2721. * @returns {MDSpan[]}
  2722. */
  2723. tokensToSpans(tokens) {
  2724. var spans = tokens.slice();
  2725. // Perform repeated substitutions, converting sequences of tokens into
  2726. // spans, until no more substitutions can be made.
  2727. var anyChanges = false;
  2728. do {
  2729. anyChanges = false;
  2730. for (const readerTuple of this.inlineReadersBySubstitutePriority) {
  2731. /** @type {number} */
  2732. const priority = readerTuple[0];
  2733. /** @type {MDInlineReader} */
  2734. const reader = readerTuple[1];
  2735. const changed = reader.substituteTokens(this, priority, spans);
  2736. if (!changed) continue;
  2737. anyChanges = true;
  2738. break;
  2739. }
  2740. } while (anyChanges);
  2741. // Convert any remaining tokens to spans, apply CSS modifiers.
  2742. var lastSpan = null;
  2743. spans = spans.map(function(span) {
  2744. if (span instanceof MDToken) {
  2745. if (span.type == MDTokenType.Modifier && lastSpan) {
  2746. span.modifier.applyTo(lastSpan);
  2747. lastSpan = null;
  2748. return new MDTextSpan('');
  2749. }
  2750. lastSpan = null;
  2751. return new MDTextSpan(span.original);
  2752. } else if (span instanceof MDSpan) {
  2753. lastSpan = (span instanceof MDTextSpan) ? null : span;
  2754. return span;
  2755. } else {
  2756. throw new Error(`Unexpected span type ${span.constructor.name}`);
  2757. }
  2758. });
  2759. return spans;
  2760. }
  2761. }
  2762. class MDTagModifier {
  2763. /** @type {string} */
  2764. original;
  2765. /** @type {string[]} */
  2766. cssClasses = [];
  2767. /** @type {string|null} */
  2768. cssId = null;
  2769. /** @type {object} */
  2770. attributes = {};
  2771. static #baseClassRegex = /\.([a-z_\-][a-z0-9_\-]*?)/i;
  2772. static #baseIdRegex = /#([a-z_\-][a-z0-9_\-]*?)/i;
  2773. static #baseAttributeRegex = /([a-z0-9]+?)=([^\s\}]+?)/i;
  2774. static #baseRegex = /\{([^}]+?)}/i;
  2775. static #leadingClassRegex = new RegExp('^' + this.#baseRegex.source, 'i');
  2776. static #trailingClassRegex = new RegExp('^(.*?)\\s*' + this.#baseRegex.source + '\\s*$', 'i');
  2777. static #classRegex = new RegExp('^' + this.#baseClassRegex.source + '$', 'i'); // 1=classname
  2778. static #idRegex = new RegExp('^' + this.#baseIdRegex.source + '$', 'i'); // 1=id
  2779. static #attributeRegex = new RegExp('^' + this.#baseAttributeRegex.source + '$', 'i'); // 1=attribute name, 2=attribute value
  2780. /**
  2781. * @param {MDBlock|MDSpan} elem
  2782. */
  2783. applyTo(elem) {
  2784. if (elem instanceof MDBlock || elem instanceof MDSpan) {
  2785. elem.cssClasses = elem.cssClasses.concat(this.cssClasses);
  2786. if (this.cssId) elem.cssId = this.cssId;
  2787. for (const name in this.attributes) {
  2788. elem.attributes[name] = this.attributes[name];
  2789. }
  2790. }
  2791. }
  2792. static #fromContents(contents) {
  2793. let modifierTokens = contents.split(/\s+/);
  2794. let mod = new MDTagModifier();
  2795. mod.original = `{${contents}}`;
  2796. var groups;
  2797. for (const token of modifierTokens) {
  2798. if (token.trim() == '') continue;
  2799. if (groups = this.#classRegex.exec(token)) {
  2800. mod.cssClasses.push(groups[1]);
  2801. } else if (groups = this.#idRegex.exec(token)) {
  2802. mod.cssId = groups[1];
  2803. } else if (groups = this.#attributeRegex.exec(token)) {
  2804. mod.attributes[groups[1]] = groups[2];
  2805. } else {
  2806. return null;
  2807. }
  2808. }
  2809. return mod;
  2810. }
  2811. /**
  2812. * Extracts modifier from line.
  2813. * @param {string} line
  2814. * @returns {Array} Tuple with remaining line and MDTagModifier.
  2815. */
  2816. static fromLine(line) {
  2817. let groups = this.#trailingClassRegex.exec(line);
  2818. if (groups === null) return [ line, null ];
  2819. let bareLine = groups[1];
  2820. let mod = this.#fromContents(groups[2]);
  2821. return [ bareLine, mod ];
  2822. }
  2823. /**
  2824. * Extracts modifier from head of string.
  2825. * @param {string} line
  2826. * @returns {MDTagModifier}
  2827. */
  2828. static fromStart(line) {
  2829. let groups = this.#leadingClassRegex.exec(line);
  2830. if (groups === null) return null;
  2831. return this.#fromContents(groups[1]);
  2832. }
  2833. /**
  2834. * @param {string} line
  2835. * @returns {string}
  2836. */
  2837. static strip(line) {
  2838. let groups = this.#trailingClassRegex.exec(line);
  2839. if (groups === null) return line;
  2840. return groups[1];
  2841. }
  2842. }
  2843. class Markdown {
  2844. /**
  2845. * Set of standard block readers.
  2846. * @type {MDBlockReader[]}
  2847. */
  2848. static standardBlockReaders = [
  2849. new MDUnderlinedHeaderBlockReader(10.0),
  2850. new MDHashHeaderBlockReader(15.0),
  2851. new MDBlockQuoteBlockReader(20.0),
  2852. new MDHorizontalRuleBlockReader(25.0),
  2853. new MDFencedCodeBlockReader(30.0),
  2854. new MDIndentedCodeBlockReader(40.0),
  2855. new MDOrderedListBlockReader(45.0),
  2856. new MDUnorderedListBlockReader(50.0),
  2857. new MDURLDefinitionBlockReader(95.0),
  2858. new MDParagraphBlockReader(100.0),
  2859. ];
  2860. /**
  2861. * All supported block readers.
  2862. * @type {MDBlockReader[]}
  2863. */
  2864. static allBlockReaders = [
  2865. ...this.standardBlockReaders,
  2866. new MDTableBlockReader(55.0),
  2867. new MDDefinitionListBlockReader(60.0),
  2868. new MDAbbreviationDefinitionBlockReader(90.0),
  2869. new MDFootnoteDefinitionBlockReader(91.0),
  2870. ];
  2871. /**
  2872. * Set of standard inline readers.
  2873. * @type {MDInlineReader[]}
  2874. */
  2875. static standardInlineReaders = [
  2876. new MDStrongInlineReader(10.0, [ 0.0, 2.0 ]),
  2877. new MDEmphasisInlineReader(15.0, [ 5.0, 55.0 ]),
  2878. new MDCodeInlineReader(20.0, [ 10.0, 60.0 ]),
  2879. new MDImageInlineReader(25.0, 15.0),
  2880. new MDLinkInlineReader(30.0, 20.0),
  2881. new MDSimpleLinkInlineReader(35.0, 25.0),
  2882. new MDHTMLTagInlineReader(80.0, 30.0),
  2883. ];
  2884. /**
  2885. * All supported inline readers.
  2886. * @type {MDInlineReader[]}
  2887. */
  2888. static allInlineReaders = [
  2889. ...this.standardInlineReaders,
  2890. new MDStrikethroughInlineReader(21.0, [ 12.0, 50.0 ]),
  2891. new MDFootnoteInlineReader(5.0, 40.0),
  2892. new MDModifierInlineReader(90.0, 45.0),
  2893. ];
  2894. /**
  2895. * Shared instance of a parser with standard syntax.
  2896. */
  2897. static standardParser = new Markdown(this.standardBlockReaders, this.standardInlineReaders);
  2898. /**
  2899. * Shared instance of a parser with all supported syntax.
  2900. */
  2901. static completeParser = new Markdown(this.allBlockReaders, this.allInlineReaders);
  2902. #blockReaders;
  2903. #inlineReaders;
  2904. #blockReadersByPriority;
  2905. #inlineReadersByTokenPriority;
  2906. #inlineReadersBySubstitutePriority;
  2907. /**
  2908. * @param {MDBlockReader[]} blockReaders
  2909. * @param {MDInlineReader[]} inlineReaders
  2910. */
  2911. constructor(blockReaders=Markdown.allBlockReaders, inlineReaders=Markdown.allInlineReaders) {
  2912. this.#blockReaders = blockReaders;
  2913. this.#inlineReaders = inlineReaders;
  2914. this.#blockReadersByPriority = blockReaders.slice();
  2915. this.#blockReadersByPriority.sort((a, b) => a.priority - b.priority);
  2916. const duplicateAndSort = function(priorityFn) {
  2917. var readers = [];
  2918. for (const reader of inlineReaders) {
  2919. const p = priorityFn(reader);
  2920. const priorities = (p instanceof Array) ? p : [ p ];
  2921. for (const priority of priorities) {
  2922. readers.push([priority, reader]);
  2923. }
  2924. }
  2925. readers.sort((a, b) => a[0] - b[0]);
  2926. return readers;
  2927. }
  2928. this.#inlineReadersByTokenPriority = duplicateAndSort((reader) => reader.tokenizePriority);
  2929. this.#inlineReadersBySubstitutePriority = duplicateAndSort((reader) => reader.substitutePriority);
  2930. }
  2931. /**
  2932. * @param {string} markdown
  2933. * @returns {string} HTML
  2934. */
  2935. toHTML(markdown) {
  2936. const lines = markdown.split(/(?:\n|\r|\r\n)/);
  2937. const state = new MDState(lines);
  2938. state.blockReadersByPriority = this.#blockReadersByPriority;
  2939. state.inlineReadersByTokenPriority = this.#inlineReadersByTokenPriority;
  2940. state.inlineReadersBySubstitutePriority = this.#inlineReadersBySubstitutePriority;
  2941. const blocks = state.readBlocks();
  2942. for (const reader of this.#blockReaders) {
  2943. reader.postProcess(state, blocks);
  2944. }
  2945. for (const reader of this.#inlineReaders) {
  2946. reader.postProcess(state, blocks);
  2947. }
  2948. return MDBlock.toHTML(blocks, state);
  2949. }
  2950. }