PHP and Javascript implementations of a simple markdown parser
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

markdown.js 83KB

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