PHP and Javascript implementations of a simple markdown parser
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

markdown.js 90KB

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