PHP and Javascript implementations of a simple markdown parser
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

markdown.js 103KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837
  1. class MDTokenType {
  2. static Text = new MDTokenType('Text');
  3. static Whitespace = new MDTokenType('Whitespace');
  4. static Underscore = new MDTokenType('Underscore');
  5. static Asterisk = new MDTokenType('Asterisk');
  6. static Slash = new MDTokenType('Slash');
  7. static Tilde = new MDTokenType('Tilde');
  8. static Bang = new MDTokenType('Bang');
  9. static Backtick = new MDTokenType('Backtick');
  10. static Equal = new MDTokenType('Equal');
  11. static Caret = new MDTokenType('Caret');
  12. static Label = new MDTokenType('Label'); // content=label
  13. static URL = new MDTokenType('URL'); // content=URL, extra=title
  14. static Email = new MDTokenType('Email'); // content=email address, extra=title
  15. static SimpleLink = new MDTokenType('SimpleLink'); // content=URL
  16. static SimpleEmail = new MDTokenType('SimpleEmail'); // content=email address
  17. static Footnote = new MDTokenType('Footnote'); // content=symbol
  18. static Modifier = new MDTokenType('Modifier'); // content
  19. static HTMLTag = new MDTokenType('HTMLTag'); // content=tag string, tag=MDHTMLTag
  20. static META_AnyNonWhitespace = new MDTokenType('METAAnyNonWhitespace');
  21. static META_OptionalWhitespace = new MDTokenType('METAOptionalWhitespace');
  22. /** @type {string} */
  23. name;
  24. /**
  25. * @param {string} name
  26. */
  27. constructor(name) {
  28. this.name = name;
  29. }
  30. toString() {
  31. return `${this.constructor.name}.${this.name}`;
  32. }
  33. equals(other) {
  34. return (other instanceof MDTokenType) && other.name == this.name;
  35. }
  36. }
  37. class MDToken {
  38. /**
  39. * The original token string.
  40. * @type {string}
  41. */
  42. original;
  43. /** @type {MDTokenType} */
  44. type;
  45. /** @type {string|null} */
  46. content;
  47. /** @type {string|null} */
  48. extra;
  49. /** @type {MDHTMLTag|null} */
  50. tag;
  51. /** @type {MDTagModifier|null} */
  52. modifier;
  53. /**
  54. * @param {string} original
  55. * @param {MDTokenType} type
  56. * @param {string|MDTagModifier|null} content
  57. * @param {string|null} extra
  58. * @param {MDHTMLTag|null} tag
  59. */
  60. constructor(original, type, content=null, extra=null, tag=null) {
  61. this.original = original;
  62. this.type = type;
  63. if (content instanceof MDTagModifier) {
  64. this.content = null;
  65. this.modifier = content;
  66. } else {
  67. this.content = content;
  68. this.modifier = null;
  69. }
  70. this.extra = extra;
  71. this.tag = tag;
  72. }
  73. toString() {
  74. return `(${this.constructor.name} type=${this.type.toString()} content=${this.content})`;
  75. }
  76. /**
  77. * Searches an array of MDToken for the given pattern of MDTokenTypes.
  78. * If found, returns an object with the given keys.
  79. * - `tokens: MDToken[]` - the subarray of `tokensToSearch` that match the pattern
  80. * - `index: number` - index into `tokensToSearch` of first matching token
  81. *
  82. * @param {MDToken[]|MDNode[]} tokensToSearch - mixed array of `MDToken` and `MDNode` elements
  83. * @param {MDTokenType[]} pattern - contiguous run of token types to find
  84. * @param {number} startIndex - token index to begin searching (defaults to 0)
  85. * @returns {object|null} match object as described, or `null` if not found
  86. */
  87. static findFirstTokens(tokensToSearch, pattern, startIndex=0) {
  88. var matched = [];
  89. for (var t = startIndex; t < tokensToSearch.length; t++) {
  90. var matchedAll = true;
  91. matched = [];
  92. var patternOffset = 0;
  93. for (var p = 0; p < pattern.length; p++) {
  94. var t0 = t + p + patternOffset;
  95. if (t0 >= tokensToSearch.length) return null;
  96. let token = tokensToSearch[t0];
  97. let elem = pattern[p];
  98. if (elem == MDTokenType.META_OptionalWhitespace) {
  99. if (token instanceof MDToken && token.type == MDTokenType.Whitespace) {
  100. matched.push(token);
  101. } else {
  102. patternOffset--;
  103. }
  104. } else if (elem == MDTokenType.META_AnyNonWhitespace) {
  105. if (token instanceof MDToken && token.type == MDTokenType.Whitespace) {
  106. matchedAll = false;
  107. break;
  108. }
  109. matched.push(token);
  110. } else {
  111. if (!(token instanceof MDToken) || token.type != elem) {
  112. matchedAll = false;
  113. break;
  114. }
  115. matched.push(token);
  116. }
  117. }
  118. if (matchedAll) {
  119. return {
  120. 'tokens': matched,
  121. 'index': t,
  122. };
  123. }
  124. }
  125. return null;
  126. }
  127. /**
  128. * Searches an array of MDToken for a given starting pattern and ending
  129. * pattern and returns match info about both and the tokens in between.
  130. *
  131. * If `contentValidator` is specified, it will be called with the content
  132. * tokens of a potential match. If the validator returns `true`, the result
  133. * will be accepted and returned by this method. If the validator returns
  134. * `false`, this method will keep looking for another matching pair. If no
  135. * validator is given the first match will be returned regardless of content.
  136. *
  137. * If a match is found, returns an object with the given keys:
  138. * - `startTokens: MDToken[]` - tokens that matched `startPattern`
  139. * - `contentTokens: MDToken[]` - tokens between the start and end pattern. May be an empty array.
  140. * - `endTokens: MDToken[]` - tokens that matched `endPattern`
  141. * - `startIndex: number` - index into `tokensToSearch` where `startPattern` begins
  142. * - `contentIndex: number` - index into `tokensToSearch` of the first token that is between the start and end patterns
  143. * - `endIndex: number` - index into `tokensToSearch` where `endPattern` begins
  144. * - `totalLength: number` - total number of matched tokens
  145. *
  146. * @param {MDToken[]} tokensToSearch - array of `MDToken` to search in
  147. * @param {MDTokenType[]} startPattern - array of `MDTokenType` to find first
  148. * @param {MDTokenType[]} endPattern - array of `MDTokenType` to find positioned after `startPattern`
  149. * @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
  150. * @param {number} startIndex - token index where searching should begin
  151. * @returns {object|null} match object
  152. */
  153. static findPairedTokens(tokensToSearch, startPattern, endPattern, contentValidator=null, startIndex=0) {
  154. for (var s = startIndex; s < tokensToSearch.length; s++) {
  155. var startMatch = this.findFirstTokens(tokensToSearch, startPattern, s);
  156. if (startMatch === null) return null;
  157. var endStart = startMatch.index + startMatch.tokens.length;
  158. while (endStart < tokensToSearch.length) {
  159. var endMatch = this.findFirstTokens(tokensToSearch, endPattern, endStart);
  160. if (endMatch === null) break;
  161. var contents = tokensToSearch.slice(startMatch.index + startMatch.tokens.length, endMatch.index);
  162. if (contents.length > 0 && (contentValidator === null || contentValidator(contents))) {
  163. return {
  164. 'startTokens': startMatch.tokens,
  165. 'contentTokens': contents,
  166. 'endTokens': endMatch.tokens,
  167. 'startIndex': startMatch.index,
  168. 'contentIndex': startMatch.index + startMatch.tokens.length,
  169. 'endIndex': endMatch.index,
  170. 'totalLength': endMatch.index + endMatch.tokens.length - startMatch.index,
  171. };
  172. } else {
  173. // Contents rejected. Try next end match.
  174. endStart = endMatch.index + 1;
  175. }
  176. }
  177. // No end matches. Increment start match.
  178. s = startMatch.index;
  179. }
  180. return null;
  181. }
  182. equals(other) {
  183. if (!(other instanceof MDToken)) return false;
  184. if (other.original !== this.original) return false;
  185. if (!other.type.equals(this.type)) return false;
  186. if (other.content !== this.content) return false;
  187. if (other.extra !== this.extra) return false;
  188. if (!MDUtils.equal(other.tag, this.tag)) return false;
  189. if (!MDUtils.equals(other.modifier, this.modifier)) return false;
  190. return true
  191. }
  192. }
  193. class MDUtils {
  194. // Modified from https://urlregex.com/ to remove capture groups. Matches fully qualified URLs only.
  195. static baseURLRegex = /(?:(?:(?:[a-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[a-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[a-z0-9\.\-]+)(?:(?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/i;
  196. // Modified from https://emailregex.com/ to remove capture groups.
  197. 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;
  198. /**
  199. * Escapes special HTML characters.
  200. *
  201. * @param {string} str - string to escape
  202. * @param {boolean} encodeNewlinesAsBreaks - whether to convert newline characters to `<br>` tags
  203. * @returns {string} escaped HTML
  204. */
  205. static escapeHTML(str, encodeNewlinesAsBreaks=false) {
  206. if (typeof str !== 'string') return '';
  207. var html = str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  208. if (encodeNewlinesAsBreaks) {
  209. html = html.replace(/\n/g, "<br>\n");
  210. }
  211. return html;
  212. }
  213. /**
  214. * Converts HTML entities to characters. HTML tags are ignored.
  215. * @param {string} html
  216. * @returns {string} plain text
  217. */
  218. static unescapeHTML(html, decodeBRsAsNewlines=false) {
  219. if (decodeBRsAsNewlines) {
  220. html = html.replace(/<br[\/]?>\n?/g, "\n");
  221. }
  222. const doc = (new DOMParser()).parseFromString(html, "text/html");
  223. return doc.documentElement.textContent;
  224. }
  225. /**
  226. * Encodes characters as HTML numeric entities to make it marginally more
  227. * difficult for web scrapers to grab sensitive info. If `text` starts with
  228. * `mailto:` only the email address following it will be obfuscated.
  229. *
  230. * @param {string} text - text to escape
  231. * @returns {string} escaped HTML
  232. */
  233. static escapeObfuscated(text) {
  234. if (text.startsWith('mailto:')) {
  235. return 'mailto:' + this.escapeObfuscated(text.substring(7));
  236. }
  237. var html = '';
  238. for (var p = 0; p < text.length; p++) {
  239. const cp = text.codePointAt(p);
  240. html += `&#${cp};`;
  241. }
  242. return html;
  243. }
  244. /**
  245. * Removes illegal characters from an HTML attribute name.
  246. * @param {string} name
  247. * @returns {string}
  248. */
  249. static scrubAttributeName(name) {
  250. return name.replace(/[\t\n\f \/>"'=]+/, '');
  251. }
  252. /**
  253. * Strips one or more leading indents from a line or lines of markdown. An
  254. * indent is defined as 4 spaces or one tab. Incomplete indents (i.e. 1-3
  255. * spaces) are treated like one indent level.
  256. *
  257. * @param {string|string[]} line - string or strings to strip
  258. * @param {number} levels - how many indent levels to strip
  259. * @returns {string|string[]} stripped lines
  260. */
  261. static stripIndent(line, levels=1) {
  262. const regex = new RegExp(`^(?: {1,4}|\t){${levels}}`);
  263. return (line instanceof Array) ? line.map((l) => l.replace(regex, '')) : line.replace(regex, '');
  264. }
  265. /**
  266. * Returns a copy of an array without any whitespace-only lines at the end.
  267. *
  268. * @param {String[]} lines - text lines
  269. * @returns {String[]} - text lines without trailing blank lines
  270. */
  271. static withoutTrailingBlankLines(lines) {
  272. var stripped = lines.slice();
  273. while (stripped.length > 0 && stripped[stripped.length - 1].trim().length == 0) {
  274. stripped.pop();
  275. }
  276. return stripped;
  277. }
  278. /**
  279. * Tests if an array of lines contains at least one blank. A blank line
  280. * can contain whitespace.
  281. *
  282. * @param {String[]} lines
  283. * @returns {boolean} whether `lines` contains any whitespace-only lines
  284. */
  285. static containsBlankLine(lines) {
  286. for (const line of lines) {
  287. if (line.trim().length == 0) return true;
  288. }
  289. return false;
  290. }
  291. /**
  292. * Counts the number of indent levels in a line of text. Partial indents
  293. * (1 to 3 spaces) are counted as one indent level unless `fullIndentsOnly`
  294. * is `true`.
  295. *
  296. * @param {string} line - line of markdown
  297. * @param {boolean} fullIndentsOnly - whether to only count full indent levels (4 spaces or a tab)
  298. * @returns {number} number of indent levels found
  299. */
  300. static countIndents(line, fullIndentsOnly=false) {
  301. // normalize indents to tabs
  302. return line.replace(fullIndentsOnly
  303. ? /(?: {4}|\t)/g
  304. : /(?: {1,4}|\t)/g,
  305. "\t")
  306. // remove content after indent
  307. .replace(/^(\t*)(.*?)$/, '$1')
  308. // count tabs
  309. .length;
  310. }
  311. /**
  312. * Attempts to parse a label from the beginning of `line`. A label is of the
  313. * form `[content]`. If found, returns an array with element 0 being the
  314. * entire label and element 1 being the content of the label.
  315. *
  316. * @param {string} line
  317. * @returns {string[]|null} match groups or null if not found
  318. */
  319. static tokenizeLabel(line) {
  320. if (!line.startsWith('[')) return null;
  321. var parenCount = 0;
  322. var bracketCount = 0;
  323. for (var p = 1; p < line.length; p++) {
  324. let ch = line.substring(p, p + 1);
  325. if (ch == '\\') {
  326. p++;
  327. } else if (ch == '(') {
  328. parenCount++;
  329. } else if (ch == ')') {
  330. parenCount--;
  331. if (parenCount < 0) return null;
  332. } else if (ch == '[') {
  333. bracketCount++;
  334. } else if (ch == ']') {
  335. if (bracketCount > 0) {
  336. bracketCount--;
  337. } else {
  338. return [ line.substring(0, p + 1), line.substring(1, p) ];
  339. }
  340. }
  341. }
  342. return null;
  343. }
  344. static #urlWithTitleRegex = /^\((\S+?)\s+"(.*?)"\)/i; // 1=URL, 2=title
  345. static #urlRegex = /^\((\S+?)\)/i; // 1=URL
  346. /**
  347. * Attempts to parse a URL from the beginning of `line`. A URL is of the
  348. * form `(url)` or `(url "title")`. If found, returns an array with element
  349. * 0 being the entire URL token, 1 is the URL, 2 is the optional title.
  350. *
  351. * @param {string} line
  352. * @returns {string[]} token tuple
  353. */
  354. static tokenizeURL(line) {
  355. var groups;
  356. if (groups = this.#urlWithTitleRegex.exec(line)) {
  357. if (this.tokenizeEmail(line)) return null; // make sure it's not better described as an email address
  358. return groups;
  359. }
  360. if (groups = this.#urlRegex.exec(line)) {
  361. if (this.tokenizeEmail(line)) return null;
  362. return [...groups, null];
  363. }
  364. return null;
  365. }
  366. static #emailWithTitleRegex = new RegExp("^\\(\\s*(" + MDUtils.baseEmailRegex.source + ")\\s+\"(.*?)\"\\s*\\)", "i"); // 1=email, 2=title
  367. static #emailRegex = new RegExp("^\\(\\s*(" + MDUtils.baseEmailRegex.source + ")\\s*\\)", "i"); // 1=email
  368. /**
  369. * Attempts to parse an email address from the beginning of `line`. An
  370. * email address is of the form `(user@example.com)` or `(user@example.com "link title")`.
  371. * If found, returns an array with element 0 being the entire token, 1 is the
  372. * email address, and 2 is the optional link title.
  373. *
  374. * @param {string} line
  375. * @returns {string[]} token tuple
  376. */
  377. static tokenizeEmail(line) {
  378. var groups;
  379. if (groups = this.#emailWithTitleRegex.exec(line)) {
  380. return groups;
  381. }
  382. if (groups = this.#emailRegex.exec(line)) {
  383. return [...groups, null];
  384. }
  385. return null;
  386. }
  387. /**
  388. * Describes the type of a variable for debugging.
  389. *
  390. * @param {any} value - value
  391. * @returns {String} description of type
  392. */
  393. static typename(value) {
  394. if (value === null) return 'null';
  395. if (value instanceof Object) {
  396. return value.constructor.name;
  397. }
  398. return typeof value;
  399. }
  400. static #equalArrays(a, b) {
  401. if (a === b) return true;
  402. if (!(a instanceof Array) || !(b instanceof Array)) return false;
  403. if (a == null || b == null) return false;
  404. if (a.length != b.length) return false;
  405. for (var i = 0; i < a.length; i++) {
  406. if (!this.equal(a[i], b[i])) return false;
  407. }
  408. return true;
  409. }
  410. static #equalObjects(a, b) {
  411. if (a === b) return true;
  412. if (!(a instanceof Object) || !(b instanceof Object)) return false;
  413. if (a == null || b == null) return false;
  414. if (a.equals !== undefined) {
  415. return a.equals(b);
  416. }
  417. for (const key of Object.keys(a)) {
  418. if (!this.equal(a[key], b[key])) return false;
  419. }
  420. for (const key of Object.keys(b)) {
  421. if (!this.equal(a[key], b[key])) return false;
  422. }
  423. return true;
  424. }
  425. /**
  426. * Tests for equality on lots of different kinds of values including objects
  427. * and arrays. Will use `.equals` on objects that implement it.
  428. *
  429. * @param {any} a
  430. * @param {any} b
  431. * @returns {boolean}
  432. */
  433. static equal(a, b, floatDifferencePercent=0.0) {
  434. if (a instanceof Array && b instanceof Array) {
  435. return this.#equalArrays(a, b);
  436. }
  437. if (a instanceof Object && b instanceof Object) {
  438. return this.#equalObjects(a, b);
  439. }
  440. if (typeof a == 'number' && typeof b == 'number') {
  441. if (a === b) return true;
  442. const delta = b - a;
  443. const ratio = delta / a;
  444. return Math.abs(ratio) <= floatDifferencePercent;
  445. }
  446. return a == b;
  447. }
  448. /**
  449. * @param {string} text
  450. */
  451. static escapeRegex(text) {
  452. // Partially following escaping scheme from not-yet-widely-supported RegExp.escape.
  453. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape
  454. const escapeHex = function(ch) {
  455. const codepoint = ch.codePointAt(0);
  456. const s = '00' + codepoint.toString(16);
  457. return `\\x${s.substring(s.length - 2)}`;
  458. }
  459. var escaped = '';
  460. const l = text.length;
  461. for (var i = 0; i < l; i++) {
  462. const ch = text.substring(i, i + 1);
  463. if (i == 0 && /[a-zA-Z0-9]/.exec(ch)) {
  464. escaped += escapeHex(ch);
  465. } else if ("^$\\.*+?()[]{}|/".indexOf(ch) >= 0) {
  466. escaped += `\\${ch}`;
  467. } else if (",-=<>#&!%:;@~'`\"".indexOf(ch) >= 0) {
  468. escaped += escapeHex(ch);
  469. } else if (ch == '\f') {
  470. escaped += "\\f";
  471. } else if (ch == '\n') {
  472. escaped += "\\n";
  473. } else if (ch == '\r') {
  474. escaped += "\\r";
  475. } else if (ch == '\t') {
  476. escaped += "\\t";
  477. } else if (ch == '\v') {
  478. escaped += "\\v";
  479. } else {
  480. escaped += ch;
  481. }
  482. }
  483. return escaped;
  484. }
  485. /**
  486. * Recursively search and replaces nodes in a tree. The given `replacer` is
  487. * passed every node in the tree. If the function returns a new `MDNode` the
  488. * original will be replaced with it. If the function returns `null` no
  489. * change will be made to that node. Traversal is depth-first.
  490. *
  491. * @param {MDState} state
  492. * @param {MDNode[]} nodes
  493. * @param {function} replacer - takes a node as an argument, returns either a new node or `null` to leave it unchanged
  494. */
  495. static replaceNodes(state, nodes, replacer) {
  496. for (var i = 0; i < nodes.length; i++) {
  497. var originalNode = nodes[i];
  498. const replacement = replacer(originalNode);
  499. if (replacement !== null) {
  500. nodes.splice(i, 1, replacement);
  501. } else {
  502. this.replaceNodes(state, originalNode.children, replacer);
  503. }
  504. }
  505. }
  506. }
  507. /**
  508. * Parsing and rendering state
  509. */
  510. class MDState {
  511. /**
  512. * Ascends the parent chain to the root `MDState` instance. This should be
  513. * used when referencing most stored fields except `lines` and `p`.
  514. *
  515. * @type {MDState}
  516. */
  517. get root() { return this.#parent ? this.#parent.root : this; }
  518. /**
  519. * Lines of the markdown document. The current line index is pointed to by `p`.
  520. *
  521. * @returns {string[]} markdown lines
  522. */
  523. get lines() { return this.#lines; }
  524. /**
  525. * The current line in `lines`.
  526. *
  527. * @returns {string|null} current line or `null` if out of content
  528. */
  529. get currentLine() { return (this.p < this.#lines.length) ? this.#lines[this.p] : null; }
  530. /**
  531. * Current line pointer into array `lines`.
  532. *
  533. * @type {number} line pointer
  534. */
  535. p = 0;
  536. /** @type {string[]} */
  537. #lines = [];
  538. /** @type {MDState|null} */
  539. #parent = null;
  540. /** @type {MDConfig} */
  541. config;
  542. /**
  543. * Array of `MDReader`s sorted by block reading priority.
  544. * @type {MDReader[]}
  545. */
  546. #readersByBlockPriority = [];
  547. /**
  548. * Array of `MDReader`s sorted by tokenization priority.
  549. * @type {MDReader[]}
  550. */
  551. #readersByTokenPriority = [];
  552. /**
  553. * Tuples of `pass:number` and `MDReader` sorted substitution priority.
  554. * @type {Array}
  555. */
  556. #readersBySubstitutePriority = [];
  557. /**
  558. * Mapping of reference symbols to URLs.
  559. * @type {object}
  560. */
  561. #referenceToURL = {};
  562. /**
  563. * Mapping of reference symbols to titles.
  564. * @type {object}
  565. */
  566. #referenceToTitle = {};
  567. /** @type {MDHTMLFilter} */
  568. tagFilter;
  569. static #textWhitespaceRegex = /^(\s*)(?:(\S|\S.*\S)(\s*?))?$/; // 1=leading WS, 2=text, 3=trailing WS
  570. /**
  571. * @param {string[]} lines - lines of markdown text
  572. * @param {MDConfig} config
  573. * @param {MDReader[]} readersByBlockPriority
  574. * @param {MDReader[]} readersByTokenPriority
  575. * @param {Array} readersBySubstitutePriority - tuple arrays of priority and MDReader
  576. */
  577. constructor(lines,
  578. config=null,
  579. readersByBlockPriority=null,
  580. readersByTokenPriority=null,
  581. readersBySubstitutePriority=null,
  582. tagFilter=null) {
  583. this.#lines = lines;
  584. this.config = config;
  585. this.#readersByBlockPriority = readersByBlockPriority
  586. this.#readersByTokenPriority = readersByTokenPriority
  587. this.#readersBySubstitutePriority = readersBySubstitutePriority
  588. this.tagFilter = tagFilter;
  589. }
  590. /**
  591. * Creates a copy of this state with new lines. Useful for parsing nested
  592. * content.
  593. *
  594. * @param {string[]} lines
  595. * @returns {MDState} copied sub-state
  596. */
  597. copy(lines) {
  598. let cp = new MDState(lines);
  599. cp.#parent = this;
  600. cp.config = this.config;
  601. return cp;
  602. }
  603. /**
  604. * Tests if there are at least `minCount` lines available to read. If `p`
  605. * is not provided it will be relative to `this.p`.
  606. *
  607. * @param {number} minCount - minimum number of lines
  608. * @param {number|null} p - line pointer, or `null` to use `this.p`
  609. * @returns {boolean} whether at least the given number of lines is available
  610. */
  611. hasLines(minCount, p=null) {
  612. let relativeTo = (p === null) ? this.p : p;
  613. return relativeTo + minCount <= this.lines.length;
  614. }
  615. /**
  616. * Reads and returns an array of blocks from the current line pointer.
  617. *
  618. * @returns {MDBlockNode[]} parsed blocks
  619. */
  620. readBlocks() {
  621. var blocks = [];
  622. while (this.hasLines(1)) {
  623. let block = this.#readNextBlock();
  624. if (block) {
  625. blocks.push(block);
  626. } else {
  627. break;
  628. }
  629. }
  630. return blocks;
  631. }
  632. /**
  633. * Creates a simple `MDBlockNode` if no other registered blocks match.
  634. *
  635. * @returns {MDBlockNode|null} fallback block
  636. */
  637. #readFallbackBlock() {
  638. if (this.p >= this.lines.length) return null;
  639. const lines = MDUtils.withoutTrailingBlankLines(this.lines.slice(this.p));
  640. if (lines.length == 0) return null;
  641. this.p = this.lines.length;
  642. return this.inlineMarkdownToNode(lines.join("\n"));
  643. }
  644. /**
  645. * Attempts to read one block from the current line pointer. The pointer
  646. * will be positioned just after the end of the block.
  647. *
  648. * @param {MDState} state
  649. * @returns {MDBlockNode|null}
  650. */
  651. #readNextBlock() {
  652. while (this.hasLines(1) && this.lines[this.p].trim().length == 0) {
  653. this.p++;
  654. }
  655. if (!this.hasLines(1)) return null;
  656. for (const reader of this.root.#readersByBlockPriority) {
  657. const startP = this.p;
  658. const block = reader.readBlock(this);
  659. if (block) {
  660. if (this.p == startP) {
  661. throw new Error(`${reader.constructor.name} returned an ${block.constructor.name} without incrementing MDState.p. This could lead to an infinite loop.`);
  662. }
  663. return block;
  664. }
  665. }
  666. const fallback = this.#readFallbackBlock();
  667. return fallback;
  668. }
  669. /**
  670. * @param {string} line
  671. * @returns {MDToken[]}
  672. */
  673. #inlineMarkdownToTokens(line) {
  674. if (this.#parent) return this.#parent.#inlineMarkdownToTokens(line);
  675. var tokens = [];
  676. var text = '';
  677. var expectLiteral = false;
  678. /**
  679. * Flushes accumulated content in `text` to `tokens`.
  680. */
  681. const endText = function() {
  682. if (text.length == 0) return;
  683. const textGroups = MDState.#textWhitespaceRegex.exec(text);
  684. if (textGroups !== null) {
  685. if (textGroups[1].length > 0) {
  686. tokens.push(new MDToken(textGroups[1], MDTokenType.Whitespace, textGroups[1]));
  687. }
  688. if (textGroups[2] !== undefined && textGroups[2].length > 0) {
  689. tokens.push(new MDToken(textGroups[2], MDTokenType.Text, textGroups[2]));
  690. }
  691. if (textGroups[3] !== undefined && textGroups[3].length > 0) {
  692. tokens.push(new MDToken(textGroups[3], MDTokenType.Whitespace, textGroups[3]));
  693. }
  694. } else {
  695. tokens.push(new MDToken(text, MDTokenType.Text, text));
  696. }
  697. text = '';
  698. }
  699. for (var p = 0; p < line.length; p++) {
  700. const ch = line.substring(p, p + 1);
  701. const remainder = line.substring(p);
  702. if (expectLiteral) {
  703. text += ch;
  704. expectLiteral = false;
  705. continue;
  706. }
  707. if (ch == '\\') {
  708. expectLiteral = true;
  709. continue;
  710. }
  711. var found = false;
  712. for (const reader of this.root.#readersByTokenPriority) {
  713. const token = reader.readToken(this, remainder);
  714. if (token === null) continue;
  715. if (token === undefined) {
  716. console.warn(`${reader.constructor.name}.readToken returned undefined instead of null`);
  717. }
  718. endText();
  719. tokens.push(token);
  720. if (token.original == null || token.original.length == 0) {
  721. throw new Error(`${reader.constructor.name} returned a token with an empty .original. This would cause an infinite loop.`);
  722. }
  723. p += token.original.length - 1;
  724. found = true;
  725. break;
  726. }
  727. if (!found) {
  728. text += ch;
  729. }
  730. }
  731. endText();
  732. return tokens;
  733. }
  734. /**
  735. * Converts a line of markdown to an `MDInlineNode`.
  736. *
  737. * @param {string|string[]} line
  738. * @returns {MDInlineNode}
  739. */
  740. inlineMarkdownToNode(line) {
  741. let nodes = this.inlineMarkdownToNodes(line);
  742. return (nodes.length == 1) ? nodes[0] : new MDInlineNode(nodes);
  743. }
  744. /**
  745. * Converts a line of markdown to an array of `MDInlineNode`s.
  746. *
  747. * @param {string|string[]} line
  748. * @returns {MDInlineNode[]}
  749. */
  750. inlineMarkdownToNodes(line) {
  751. var tokens = this.#inlineMarkdownToTokens((line instanceof Array) ? line.join('\n') : line);
  752. return this.tokensToNodes(tokens);
  753. }
  754. /**
  755. * Converts a mixed array of `MDToken` and `MDInlineNode` elements into an array
  756. * of only `MDInlineNode`.
  757. *
  758. * @param {MDToken[]|MDInlineNode[]} tokens
  759. * @returns {MDInlineNode[]}
  760. */
  761. tokensToNodes(tokens) {
  762. var nodes = tokens.slice();
  763. // Perform repeated substitutions, converting sequences of tokens into
  764. // nodes, until no more substitutions can be made.
  765. var anyChanges = false;
  766. do {
  767. anyChanges = false;
  768. for (const readerTuple of this.root.#readersBySubstitutePriority) {
  769. /** @type {number} */
  770. const pass = readerTuple[0];
  771. /** @type {MDReader} */
  772. const reader = readerTuple[1];
  773. const changed = reader.substituteTokens(this, pass, nodes);
  774. if (!changed) continue;
  775. anyChanges = true;
  776. break;
  777. }
  778. } while (anyChanges);
  779. // Convert any remaining tokens to nodes, apply CSS modifiers.
  780. var lastNode = null;
  781. const me = this;
  782. nodes = nodes.map(function(node) {
  783. if (node instanceof MDToken) {
  784. /** @type {MDToken} */
  785. const token = node;
  786. if (token.type == MDTokenType.Modifier && lastNode) {
  787. me.root.tagFilter.scrubModifier(token.modifier);
  788. token.modifier.applyTo(lastNode);
  789. lastNode = null;
  790. return new MDTextNode('');
  791. }
  792. lastNode = null;
  793. return new MDTextNode(token.original);
  794. } else if (node instanceof MDNode) {
  795. lastNode = (node instanceof MDTextNode) ? null : node;
  796. return node;
  797. } else {
  798. throw new Error(`Unexpected node type ${node.constructor.name}`);
  799. }
  800. });
  801. return nodes;
  802. }
  803. /**
  804. * Defines a URL by reference symbol.
  805. *
  806. * @param {string} reference - case-insensitive reference symbol
  807. * @param {string} url - URL to map the symbol to
  808. * @param {string|null} title - optional link title
  809. */
  810. defineURL(reference, url, title=null) {
  811. this.root.#referenceToURL[reference.toLowerCase()] = url;
  812. if (title !== null) this.root.#referenceToTitle[reference.toLowerCase()] = title;
  813. }
  814. /**
  815. * Returns the URL associated with a reference symbol.
  816. *
  817. * @param {string} reference - case-insensitive reference symbol
  818. * @returns {string|null} URL for the given reference, or `null` if not defined
  819. */
  820. urlForReference(reference) {
  821. return this.root.#referenceToURL[reference.toLowerCase()] ?? null;
  822. }
  823. /**
  824. * Returns the link title associated with a reference symbol.
  825. *
  826. * @param {string} reference - case-insensitive reference symbol
  827. * @returns {string|null} link title for the given reference, or `null` if not defined
  828. */
  829. urlTitleForReference(reference) {
  830. return this.root.#referenceToTitle[reference.toLowerCase()] ?? null;
  831. }
  832. }
  833. // -- Readers ---------------------------------------------------------------
  834. /**
  835. * Base class for readers of various markdown syntax. A `Markdown` instance can
  836. * be created with any combination of subclasses of these to customize the
  837. * flavor of markdown parsed.
  838. *
  839. * Parsing occurs in three phases, and `MDReader` implementations can implement
  840. * any combination of these.
  841. * 1. **Blocks** - Processing an array of lines to find block-level structures,
  842. * such as paragraphs, lists, tables, blockquotes, etc. and converting them
  843. * into block-level `MDNode`s. Override `readBlock`.
  844. * 2. **Inline tokens** - Carving up single lines of markdown into tokens for
  845. * inline formatting, such as strong, emphasis, links, images, etc.
  846. * Override `readToken`.
  847. * 3. **Inline substitution** - Finding patterns of tokens and substituting them
  848. * with `MDNode`s. Override `substituteTokens`. (`readToken` and
  849. * `substituteTokens` are usually overridden together.)
  850. *
  851. * Readers may have similar, ambiguous syntax (such as `**strong**` and
  852. * `*emphasis*`) and need to process in a certain order. This can be done by
  853. * overriding the `compare` methods to influence which readers to put before
  854. * others in each phase. Furthermore, substitution can occur in multiple passes
  855. * if necessary. These two mechanisms can be used to resolve ambiguities.
  856. */
  857. class MDReader {
  858. /**
  859. * Called before processing begins. `state.lines` is populated and the
  860. * line pointer `state.p` will be at `0`. Default implementation does nothing.
  861. *
  862. * @param {MDState} state
  863. */
  864. preProcess(state) {}
  865. /**
  866. * Attempts to read an `MDBlockNode` subclass at the current line pointer
  867. * `state.p`. Only matches if the block pattern starts at the line pointer,
  868. * not elsewhere in the `state.lines` array. If a block is found, `state.p`
  869. * should be incremented to the next line _after_ the block structure and
  870. * a `MDBlockNode` subclass instance is returned. If no block is found,
  871. * returns `null`.
  872. *
  873. * @param {MDState} state
  874. * @returns {MDBlockNode|null} found block, or `null` if not found
  875. */
  876. readBlock(state) { return null; }
  877. /**
  878. * Attempts to read a token from the beginning of `line`. Only the start of
  879. * the given `line` is considered. If a matching token is found, an
  880. * `MDToken` is returned. Otherwise `null` is returned.
  881. *
  882. * @param {MDState} state
  883. * @param {string} line - string to check for a leading token
  884. * @returns {MDToken|null} found token, or `null` if not found
  885. */
  886. readToken(state, line) { return null; }
  887. /**
  888. * Attempts to find a pattern in `tokens` and perform an in-place substitution
  889. * with one or more `MDNode` subclass instances.
  890. *
  891. * @param {MDState} state
  892. * @param {number} pass - what substitution pass this is, starting with 1
  893. * @param {Array} tokens - mixed array of `MDToken` and `MDInlineNode` elements
  894. * @returns {boolean} `true` if a substitution was performed, `false` if not
  895. */
  896. substituteTokens(state, pass, tokens) { return false; }
  897. /**
  898. * Called after all parsing has completed. An array `blocks` is passed of all
  899. * top-level `MDBlockNode` elements is passed which can be altered in-place
  900. * via `.splice` operations if necessary.
  901. *
  902. * `MDNode.visitChildren` is useful for recursively looking for certain
  903. * `MDNode` instances. `MDUtils.replaceNodes` is useful for swapping in
  904. * replacements.
  905. *
  906. * @param {MDState} state
  907. * @param {MDBlockNode[]} blocks
  908. */
  909. postProcess(state, blocks) {}
  910. /**
  911. * @param {MDReader} other
  912. * @returns {number} -1 if this should be before other, 0 if the same or don't care, 1 if this should be after other
  913. */
  914. compareBlockOrdering(other) {
  915. return 0;
  916. }
  917. /**
  918. * @param {MDReader} other
  919. * @returns {number}
  920. */
  921. compareTokenizeOrdering(other) {
  922. return 0;
  923. }
  924. /**
  925. * @param {MDReader} other
  926. * @param {number} pass
  927. * @returns {number}
  928. */
  929. compareSubstituteOrdering(other, pass) {
  930. return 0;
  931. }
  932. get substitutionPassCount() { return 1; }
  933. /**
  934. * For sorting readers with ordering preferences. The `compare` methods
  935. * don't have the properties of normal sorting compares so need to sort
  936. * differently.
  937. *
  938. * @param {MDReader[]} arr - array to sort
  939. * @param {function} compareFn - comparison function, taking two array element
  940. * arguments and returning -1, 0, or 1 for a < b, a == b, and a > b,
  941. * respectively
  942. * @param {function} idFn - function for returning a unique hashable id for
  943. * the array element
  944. * @returns {MDReader[]} sorted array
  945. */
  946. static #kahnTopologicalSort(arr, compareFn, idFn) {
  947. const graph = {};
  948. const inDegrees = {};
  949. const valuesById = {};
  950. // Build the graph and compute in-degrees
  951. for (const elem of arr) {
  952. const id = idFn(elem);
  953. graph[id] = [];
  954. inDegrees[id] = 0;
  955. valuesById[id] = elem;
  956. }
  957. for (let i = 0; i < arr.length; i++) {
  958. const elemA = arr[i];
  959. const idA = idFn(elemA);
  960. for (let j = 0; j < arr.length; j++) {
  961. if (i === j) continue;
  962. const elemB = arr[j];
  963. const idB = idFn(elemB);
  964. const comparisonResult = compareFn(elemA, elemB);
  965. if (comparisonResult < 0) {
  966. graph[idA].push(idB);
  967. inDegrees[idB]++;
  968. } else if (comparisonResult > 0) {
  969. graph[idB].push(idA);
  970. inDegrees[idA]++;
  971. }
  972. }
  973. }
  974. // Initialize the queue with zero-inDegree nodes
  975. const queue = [];
  976. for (const elemId in inDegrees) {
  977. if (inDegrees[elemId] === 0) {
  978. queue.push(elemId);
  979. }
  980. }
  981. // Process the queue and build the topological order list
  982. const sorted = [];
  983. while (queue.length > 0) {
  984. const elemId = queue.shift();
  985. sorted.push(valuesById[elemId]);
  986. delete valuesById[elemId];
  987. for (const neighbor of graph[elemId]) {
  988. inDegrees[neighbor]--;
  989. if (inDegrees[neighbor] === 0) {
  990. queue.push(neighbor);
  991. }
  992. }
  993. }
  994. // Anything left over can go at the end. No ordering dependencies.
  995. for (const elemId in valuesById) {
  996. sorted.push(valuesById[elemId]);
  997. }
  998. return sorted;
  999. }
  1000. /**
  1001. * @param {MDReader[]} readers
  1002. * @returns {MDReader[]}
  1003. */
  1004. static sortReaderForBlocks(readers) {
  1005. const sorted = readers.slice();
  1006. return MDReader.#kahnTopologicalSort(sorted, (a, b) => {
  1007. return a.compareBlockOrdering(b);
  1008. // if (ab != 0) return ab;
  1009. // return -b.compareBlockOrdering(a);
  1010. }, (elem) => elem.constructor.name);
  1011. }
  1012. /**
  1013. * @param {MDReader[]} readers
  1014. * @returns {MDReader[]}
  1015. */
  1016. static sortReadersForTokenizing(readers) {
  1017. const sorted = readers.slice();
  1018. return MDReader.#kahnTopologicalSort(sorted, (a, b) => {
  1019. return a.compareTokenizeOrdering(b);
  1020. // if (ab != 0) return ab;
  1021. // return -b.compareTokenizeOrdering(a);
  1022. }, (elem) => elem.constructor.name);
  1023. }
  1024. /**
  1025. * @param {MDReader[]} readers
  1026. * @returns {MDReader[]}
  1027. */
  1028. static sortReadersForSubstitution(readers) {
  1029. var tuples = [];
  1030. var maxPass = 1;
  1031. for (const reader of readers) {
  1032. const passCount = reader.substitutionPassCount;
  1033. for (var pass = 1; pass <= passCount; pass++) {
  1034. tuples.push([ pass, reader ]);
  1035. }
  1036. maxPass = Math.max(maxPass, pass);
  1037. }
  1038. var result = [];
  1039. for (var pass = 1; pass <= maxPass; pass++) {
  1040. var readersThisPass = tuples.filter((tup) => tup[0] == pass);
  1041. const passResult = MDReader.#kahnTopologicalSort(readersThisPass, (a, b) => {
  1042. const aReader = a[1];
  1043. const bReader = b[1];
  1044. return aReader.compareSubstituteOrdering(bReader, pass);
  1045. }, (elem) => `${elem[1].constructor.name}:${elem[0]}`);
  1046. result = result.concat(passResult);
  1047. }
  1048. return result;
  1049. }
  1050. }
  1051. /**
  1052. * Reads markdown blocks for headings denoted with the underline syntax.
  1053. *
  1054. * Example:
  1055. *
  1056. * > ```markdown
  1057. * > Heading 1
  1058. * > ========
  1059. * > ```
  1060. */
  1061. class MDUnderlinedHeadingReader extends MDReader {
  1062. readBlock(state) {
  1063. var p = state.p;
  1064. if (!state.hasLines(2)) return null;
  1065. var modifier;
  1066. let contentLine = state.lines[p++].trim();
  1067. [contentLine, modifier] = MDTagModifier.fromLine(contentLine);
  1068. let underLine = state.lines[p++].trim();
  1069. if (contentLine == '') return null;
  1070. if (/^=+$/.exec(underLine)) {
  1071. state.p = p;
  1072. let block = new MDHeadingNode(1, state.inlineMarkdownToNodes(contentLine));
  1073. if (modifier) modifier.applyTo(block);
  1074. return block;
  1075. }
  1076. if (/^\-+$/.exec(underLine)) {
  1077. state.p = p;
  1078. let block = new MDHeadingNode(2, state.inlineMarkdownToNodes(contentLine));
  1079. if (modifier) modifier.applyTo(block);
  1080. return block;
  1081. }
  1082. return null;
  1083. }
  1084. }
  1085. /**
  1086. * Reads markdown blocks for headings denoted with hash marks. Heading levels 1 to
  1087. * 6 are supported.
  1088. *
  1089. * Examples:
  1090. *
  1091. * > ```markdown
  1092. * > # Heading 1
  1093. * >
  1094. * > ## Heading 2
  1095. * >
  1096. * > # Enclosing Hashes Are Optional #
  1097. * >
  1098. * > ## Trailing Hashes Don't Have to Match in Number ####
  1099. * > ```
  1100. */
  1101. class MDHashHeadingReader extends MDReader {
  1102. static #hashHeadingRegex = /^(#{1,6})\s*([^#].*?)\s*\#*\s*$/; // 1=hashes, 2=content
  1103. readBlock(state) {
  1104. var p = state.p;
  1105. let line = state.lines[p++];
  1106. var modifier;
  1107. [line, modifier] = MDTagModifier.fromLine(line);
  1108. var groups = MDHashHeadingReader.#hashHeadingRegex.exec(line);
  1109. if (groups === null) return null;
  1110. state.p = p;
  1111. const level = groups[1].length;
  1112. const content = groups[2];
  1113. let block = new MDHeadingNode(level, state.inlineMarkdownToNodes(content));
  1114. if (modifier) modifier.applyTo(block);
  1115. return block;
  1116. }
  1117. }
  1118. class MDSubtextReader extends MDReader {
  1119. static #subtextRegex = /^\-#\s*(.*?)\s*$/; // 1=content
  1120. readBlock(state) {
  1121. var p = state.p;
  1122. let line = state.lines[p++];
  1123. var modifier;
  1124. [line, modifier] = MDTagModifier.fromLine(line);
  1125. var groups = MDSubtextReader.#subtextRegex.exec(line);
  1126. if (groups === null) return null;
  1127. state.p = p;
  1128. const content = groups[1];
  1129. let block = new MDSubtextNode(state.inlineMarkdownToNodes(content));
  1130. if (modifier) modifier.applyTo(block);
  1131. return block;
  1132. }
  1133. compareBlockOrdering(other) {
  1134. if (other instanceof MDUnorderedListReader) {
  1135. return -1;
  1136. }
  1137. return 0;
  1138. }
  1139. }
  1140. /**
  1141. * Reads markdown blocks for blockquoted text.
  1142. *
  1143. * Example:
  1144. *
  1145. * > ```markdown
  1146. * > > Blockquoted text
  1147. * > ```
  1148. */
  1149. class MDBlockQuoteReader extends MDReader {
  1150. readBlock(state) {
  1151. var blockquoteLines = [];
  1152. var p = state.p;
  1153. while (p < state.lines.length) {
  1154. let line = state.lines[p++];
  1155. if (line.startsWith(">")) {
  1156. blockquoteLines.push(line);
  1157. } else {
  1158. break;
  1159. }
  1160. }
  1161. if (blockquoteLines.length > 0) {
  1162. let contentLines = blockquoteLines.map(function(line) {
  1163. return line.substring(1).replace(/^ {0,3}\t?/, '');
  1164. });
  1165. let substate = state.copy(contentLines);
  1166. let quotedBlocks = substate.readBlocks();
  1167. state.p = p;
  1168. return new MDBlockquoteNode(quotedBlocks);
  1169. }
  1170. return null;
  1171. }
  1172. }
  1173. /**
  1174. * Abstract base class for ordered and unordered lists.
  1175. */
  1176. class _MDListReader extends MDReader {
  1177. #readItemLines(state, firstLineStartPos) {
  1178. var p = state.p;
  1179. var lines = [];
  1180. var seenBlankLine = false;
  1181. var stripTrailingBlankLines = true;
  1182. while (state.hasLines(1, p)) {
  1183. const isFirstLine = p == state.p;
  1184. var line = state.lines[p++];
  1185. if (isFirstLine) {
  1186. line = line.substring(firstLineStartPos);
  1187. }
  1188. if (/^(?:\*|\+|\-|\d+\.)\s+/.exec(line)) {
  1189. // Found next list item
  1190. stripTrailingBlankLines = false; // because this signals extra spacing intended
  1191. break;
  1192. }
  1193. const isBlankLine = line.trim().length == 0;
  1194. const isIndented = /^\s+\S/.exec(line) !== null;
  1195. if (isBlankLine) {
  1196. seenBlankLine = true;
  1197. } else if (!isIndented && seenBlankLine) {
  1198. // Post-list content
  1199. break;
  1200. }
  1201. lines.push(line);
  1202. }
  1203. lines = MDUtils.withoutTrailingBlankLines(lines);
  1204. return MDUtils.stripIndent(lines);
  1205. }
  1206. /**
  1207. * @param {MDState} state
  1208. * @param {number} firstLineStart
  1209. * @return {MDBlockNode}
  1210. */
  1211. _readListItemContent(state, firstLineStartPos) {
  1212. const itemLines = this.#readItemLines(state, firstLineStartPos);
  1213. state.p += Math.max(itemLines.length, 1);
  1214. if (itemLines.length == 1) {
  1215. return state.inlineMarkdownToNode(itemLines[0]);
  1216. }
  1217. const hasBlankLines = itemLines.filter((line) => line.trim().length == 0).length > 0;
  1218. if (hasBlankLines) {
  1219. const substate = state.copy(itemLines);
  1220. const blocks = substate.readBlocks();
  1221. return (blocks.length == 1) ? blocks[0] : new MDNode(blocks);
  1222. }
  1223. // Multiline content with no blank lines. Search for new block
  1224. // boundaries without the benefit of a blank line to demarcate it.
  1225. for (var p = 1; p < itemLines.length; p++) {
  1226. const line = itemLines[p];
  1227. if (/^(?:\*|\-|\+|\d+\.)\s+/.exec(line)) {
  1228. // Nested list found
  1229. const firstBlock = state.inlineMarkdownToNode(itemLines.slice(0, p).join("\n"));
  1230. const substate = state.copy(itemLines.slice(p));
  1231. const blocks = substate.readBlocks();
  1232. return new MDNode([ firstBlock, ...blocks ]);
  1233. }
  1234. }
  1235. // Ok, give up and just do a standard block read
  1236. {
  1237. const substate = state.copy(itemLines);
  1238. const blocks = substate.readBlocks();
  1239. return (blocks.length == 1) ? blocks[0] : new MDNode(blocks);
  1240. }
  1241. }
  1242. readBlock(state) {
  1243. throw new Error(`Abstract readBlock must be overridden in ${this.constructor.name}`);
  1244. }
  1245. }
  1246. /**
  1247. * Block reader for unordered (bulleted) lists.
  1248. *
  1249. * Example:
  1250. *
  1251. * > ```markdown
  1252. * > * First item
  1253. * > * Second item
  1254. * > * Third item
  1255. * > ```
  1256. */
  1257. class MDUnorderedListReader extends _MDListReader {
  1258. static #unorderedListRegex = /^([\*\+\-]\s+)(.*)$/; // 1=bullet, 2=content
  1259. /**
  1260. * @param {MDState} state
  1261. * @returns {MDListItemNode|null}
  1262. */
  1263. #readUnorderedListItem(state) {
  1264. var p = state.p;
  1265. let line = state.lines[p];
  1266. let groups = MDUnorderedListReader.#unorderedListRegex.exec(line);
  1267. if (groups === null) return null;
  1268. const firstLineOffset = groups[1].length;
  1269. return new MDListItemNode(this._readListItemContent(state, firstLineOffset));
  1270. }
  1271. readBlock(state) {
  1272. var items = [];
  1273. var item = null;
  1274. do {
  1275. item = this.#readUnorderedListItem(state);
  1276. if (item) items.push(item);
  1277. } while (item);
  1278. if (items.length == 0) return null;
  1279. return new MDUnorderedListNode(items);
  1280. }
  1281. }
  1282. /**
  1283. * Block reader for ordered (numbered) lists. The number of the first item is
  1284. * used to begin counting. The subsequent items increase by 1, regardless of
  1285. * their value.
  1286. *
  1287. * Example:
  1288. *
  1289. * > ```markdown
  1290. * > 1. First
  1291. * > 2. Second
  1292. * > 3. Third
  1293. * > ```
  1294. */
  1295. class MDOrderedListReader extends _MDListReader {
  1296. static #orderedListRegex = /^(\d+)(\.\s+)(.*)$/; // 1=number, 2=dot, 3=content
  1297. /**
  1298. * @param {MDState} state
  1299. * @returns {MDListItemNode|null}
  1300. */
  1301. #readOrderedListItem(state) {
  1302. var p = state.p;
  1303. let line = state.lines[p];
  1304. let groups = MDOrderedListReader.#orderedListRegex.exec(line);
  1305. if (groups === null) return null;
  1306. const ordinal = parseInt(groups[1]);
  1307. const firstLineOffset = groups[1].length + groups[2].length;
  1308. return new MDListItemNode(this._readListItemContent(state, firstLineOffset), ordinal);
  1309. }
  1310. readBlock(state) {
  1311. var items = [];
  1312. var item = null;
  1313. do {
  1314. item = this.#readOrderedListItem(state);
  1315. if (item) items.push(item);
  1316. } while (item);
  1317. if (items.length == 0) return null;
  1318. return new MDOrderedListNode(items, items[0].ordinal);
  1319. }
  1320. }
  1321. /**
  1322. * Block reader for code blocks denoted by pairs of triple tickmarks.
  1323. *
  1324. * Example:
  1325. *
  1326. * > ```markdown
  1327. * > \`\`\`
  1328. * > function formattedAsCode() {
  1329. * > }
  1330. * > \`\`\`
  1331. * > ```
  1332. */
  1333. class MDFencedCodeBlockReader extends MDReader {
  1334. readBlock(state) {
  1335. if (!state.hasLines(2)) return null;
  1336. var p = state.p;
  1337. let openFenceLine = state.lines[p++];
  1338. var modifier;
  1339. [openFenceLine, modifier] = MDTagModifier.fromLine(openFenceLine);
  1340. if (openFenceLine.trim() != '```') return null;
  1341. var codeLines = [];
  1342. while (state.hasLines(1, p)) {
  1343. let line = state.lines[p++];
  1344. if (line.trim() == '```') {
  1345. state.p = p;
  1346. let block = new MDCodeBlockNode(codeLines.join("\n"));
  1347. if (modifier) modifier.applyTo(block);
  1348. return block;
  1349. }
  1350. codeLines.push(line);
  1351. }
  1352. return null;
  1353. }
  1354. }
  1355. /**
  1356. * Block reader for code blocks denoted by indenting text.
  1357. *
  1358. * Example (indent spaces rendered visibly for clarity):
  1359. *
  1360. * > ```markdown
  1361. * > ⎵⎵⎵⎵function formattedAsCode() {
  1362. * > ⎵⎵⎵⎵}
  1363. * > ```
  1364. */
  1365. class MDIndentedCodeBlockReader extends MDReader {
  1366. readBlock(state) {
  1367. var p = state.p;
  1368. var codeLines = [];
  1369. while (state.hasLines(1, p)) {
  1370. let line = state.lines[p++];
  1371. if (MDUtils.countIndents(line, true) < 1) {
  1372. p--;
  1373. break;
  1374. }
  1375. codeLines.push(MDUtils.stripIndent(line));
  1376. }
  1377. if (codeLines.length == 0) return null;
  1378. state.p = p;
  1379. return new MDCodeBlockNode(codeLines.join("\n"));
  1380. }
  1381. }
  1382. /**
  1383. * Block reader for horizontal rules. Composed of three or more hypens or
  1384. * asterisks on a line by themselves, with or without intermediate whitespace.
  1385. *
  1386. * Examples:
  1387. *
  1388. * > ```markdown
  1389. * > ---
  1390. * >
  1391. * > - - -
  1392. * >
  1393. * > * * * * *
  1394. * >
  1395. * > ****
  1396. * > ```
  1397. */
  1398. class MDHorizontalRuleReader extends MDReader {
  1399. static #horizontalRuleRegex = /^\s*(?:\-(?:\s*\-){2,}|\*(?:\s*\*){2,})\s*$/;
  1400. readBlock(state) {
  1401. var p = state.p;
  1402. let line = state.lines[p++];
  1403. var modifier;
  1404. [line, modifier] = MDTagModifier.fromLine(line);
  1405. if (MDHorizontalRuleReader.#horizontalRuleRegex.exec(line)) {
  1406. state.p = p;
  1407. let block = new MDHorizontalRuleNode();
  1408. if (modifier) modifier.applyTo(block);
  1409. return block;
  1410. }
  1411. return null;
  1412. }
  1413. compareBlockOrdering(other) {
  1414. if (other instanceof MDUnorderedListReader) {
  1415. return -1;
  1416. }
  1417. return 0;
  1418. }
  1419. }
  1420. /**
  1421. * Block reader for tables.
  1422. *
  1423. * Examples:
  1424. *
  1425. * > ```markdown
  1426. * > Name | Age
  1427. * > --- | ---
  1428. * > Joe | 34
  1429. * > Alice | 25
  1430. * >
  1431. * > | Leading | And Trailing |
  1432. * > | - | - |
  1433. * > | Required | for single column tables |
  1434. * >
  1435. * > | Left aligned column | Center aligned | Right aligned |
  1436. * > | :-- | :--: | --: |
  1437. * > | Joe | x | 34 |
  1438. * > ```
  1439. */
  1440. class MDTableReader extends MDReader {
  1441. /**
  1442. * @param {MDState} state
  1443. * @param {boolean} isHeader
  1444. * @return {MDTableRowNode|null}
  1445. */
  1446. #readTableRow(state, isHeader) {
  1447. if (!state.hasLines(1)) return null;
  1448. var p = state.p;
  1449. let line = MDTagModifier.strip(state.lines[p++].trim());
  1450. if (/.*\|.*/.exec(line) === null) return null;
  1451. if (line.startsWith('|')) line = line.substring(1);
  1452. if (line.endsWith('|')) line = line.substring(0, line.length - 1);
  1453. let cellTokens = line.split('|');
  1454. let cells = cellTokens.map(function(token) {
  1455. let content = state.inlineMarkdownToNode(token.trim());
  1456. return isHeader ? new MDTableHeaderCellNode(content) : new MDTableCellNode(content);
  1457. });
  1458. state.p = p;
  1459. return new MDTableRowNode(cells);
  1460. }
  1461. /**
  1462. * @param {string} line
  1463. * @returns {string[]}
  1464. */
  1465. #parseColumnAlignments(line) {
  1466. line = line.trim();
  1467. if (line.startsWith('|')) line = line.substring(1);
  1468. if (line.endsWith('|')) line = line.substring(0, line.length - 1);
  1469. return line.split(/\s*\|\s*/).map(function(token) {
  1470. if (token.startsWith(':')) {
  1471. if (token.endsWith(':')) {
  1472. return 'center';
  1473. }
  1474. return 'left';
  1475. } else if (token.endsWith(':')) {
  1476. return 'right';
  1477. }
  1478. return null;
  1479. });
  1480. }
  1481. static #tableDividerRegex = /^\s*[|]?\s*(?:[:]?-+[:]?)(?:\s*\|\s*[:]?-+[:]?)*\s*[|]?\s*$/;
  1482. readBlock(state) {
  1483. if (!state.hasLines(2)) return null;
  1484. let startP = state.p;
  1485. let firstLine = state.lines[startP];
  1486. var modifier = MDTagModifier.fromLine(firstLine)[1];
  1487. let headerRow = this.#readTableRow(state, true);
  1488. if (headerRow === null) {
  1489. state.p = startP;
  1490. return null;
  1491. }
  1492. let dividerLine = state.lines[state.p++];
  1493. let dividerGroups = MDTableReader.#tableDividerRegex.exec(dividerLine);
  1494. if (dividerGroups === null) {
  1495. state.p = startP;
  1496. return null;
  1497. }
  1498. let columnAlignments = this.#parseColumnAlignments(dividerLine);
  1499. var bodyRows = [];
  1500. while (state.hasLines(1)) {
  1501. let row = this.#readTableRow(state, false);
  1502. if (row === null) break;
  1503. bodyRows.push(row);
  1504. }
  1505. let table = new MDTableNode(headerRow, bodyRows);
  1506. table.columnAlignments = columnAlignments;
  1507. if (modifier) modifier.applyTo(table);
  1508. return table;
  1509. }
  1510. }
  1511. /**
  1512. * Block reader for definition lists. Definitions go directly under terms starting
  1513. * with a colon.
  1514. *
  1515. * Example:
  1516. *
  1517. * > ```markdown
  1518. * > markdown
  1519. * > : a language for generating HTML from simplified syntax
  1520. * > parser
  1521. * > : code that converts human-readable code into machine language
  1522. * > ```
  1523. */
  1524. class MDDefinitionListReader extends MDReader {
  1525. readBlock(state) {
  1526. var p = state.p;
  1527. var groups;
  1528. var termCount = 0;
  1529. var definitionCount = 0;
  1530. var defLines = [];
  1531. while (state.hasLines(1, p)) {
  1532. let line = state.lines[p++];
  1533. if (line.trim().length == 0) {
  1534. break;
  1535. }
  1536. if (/^\s+/.exec(line)) {
  1537. if (defLines.length == 0) return null;
  1538. defLines[defLines.length - 1] += "\n" + line;
  1539. } else if (/^:\s+/.exec(line)) {
  1540. defLines.push(line);
  1541. definitionCount++;
  1542. } else {
  1543. defLines.push(line);
  1544. termCount++;
  1545. }
  1546. }
  1547. if (termCount == 0 || definitionCount == 0) return null;
  1548. let blocks = defLines.map(function(line) {
  1549. if (groups = /^:\s+(.*?)$/s.exec(line)) {
  1550. return new MDDefinitionListDefinitionNode(state.inlineMarkdownToNodes(groups[1]));
  1551. } else {
  1552. return new MDDefinitionListTermNode(state.inlineMarkdownToNodes(line));
  1553. }
  1554. });
  1555. state.p = p;
  1556. return new MDDefinitionListNode(blocks);
  1557. }
  1558. }
  1559. /**
  1560. * Block reader for defining footnote contents. Footnotes can be defined anywhere
  1561. * in the document but will always be rendered at the end of a page or end of
  1562. * the document.
  1563. *
  1564. * Examples:
  1565. *
  1566. * > ```markdown
  1567. * > [^1]: Content of a footnote. Anywhere `[^1]` appears in the
  1568. * > main text, it will hyperlink to this content at the bottom
  1569. * > of the document. There will also be backlinks at the end
  1570. * > of this footnote to all references to it.
  1571. * > ```
  1572. */
  1573. class MDFootnoteReader extends MDReader {
  1574. static #footnoteWithTitleRegex = /^\[\^([^\]]+?)\s+"(.*?)"\]/; // 1=symbol, 2=title
  1575. static #footnoteRegex = /^\[\^([^\]]+?)\]/; // 1=symbol
  1576. /**
  1577. * @param {MDState} state
  1578. * @param {string} symbol
  1579. * @param {MDNode[]} content
  1580. */
  1581. #defineFootnote(state, symbol, footnote) {
  1582. var footnotes = state.root['footnotes'] ?? {};
  1583. footnotes[symbol] = footnote;
  1584. state.root['footnotes'] = footnotes;
  1585. }
  1586. /**
  1587. * @param {MDState} state
  1588. * @param {string} symbol
  1589. * @param {number} unique
  1590. */
  1591. #registerUniqueInstance(state, symbol, unique) {
  1592. var footnoteInstances = state.root['footnoteInstances'];
  1593. var instances = footnoteInstances[symbol] ?? [];
  1594. instances.push(unique);
  1595. footnoteInstances[symbol] = instances;
  1596. }
  1597. #idForFootnoteSymbol(state, symbol) {
  1598. var footnoteIds = state.root['footnoteIds'];
  1599. const existing = footnoteIds[symbol];
  1600. if (existing) return existing;
  1601. var nextFootnoteId = state.root['nextFootnoteId'];
  1602. const id = nextFootnoteId++;
  1603. footnoteIds[symbol] = id;
  1604. state.root['nextFootnoteId'] = nextFootnoteId;
  1605. return id;
  1606. }
  1607. preProcess(state) {
  1608. state.root['footnoteInstances'] = {};
  1609. state.root['footnotes'] = {};
  1610. state.root['footnoteIds'] = {};
  1611. state.root['nextFootnoteId'] = 1;
  1612. }
  1613. /**
  1614. * @param {MDState} state
  1615. */
  1616. readBlock(state) {
  1617. var p = state.p;
  1618. let groups = /^\s*\[\^\s*([^\]]+)\s*\]:\s+(.*)\s*$/.exec(state.lines[p++]);
  1619. if (groups === null) return null;
  1620. let symbol = groups[1];
  1621. let def = groups[2];
  1622. while (state.hasLines(1, p)) {
  1623. let line = state.lines[p++];
  1624. if (/^\s+/.exec(line)) {
  1625. def += "\n" + line;
  1626. } else {
  1627. p--;
  1628. break;
  1629. }
  1630. }
  1631. let content = state.inlineMarkdownToNodes(def);
  1632. this.#defineFootnote(state, symbol, content);
  1633. state.p = p;
  1634. return new MDNode(); // empty
  1635. }
  1636. readToken(state, line) {
  1637. var groups;
  1638. if (groups = MDFootnoteReader.#footnoteWithTitleRegex.exec(line)) {
  1639. return new MDToken(groups[0], MDTokenType.Footnote, groups[1], groups[2]);
  1640. }
  1641. if (groups = MDFootnoteReader.#footnoteRegex.exec(line)) {
  1642. return new MDToken(groups[0], MDTokenType.Footnote, groups[1]);
  1643. }
  1644. return null;
  1645. }
  1646. substituteTokens(state, pass, tokens) {
  1647. var match;
  1648. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.Footnote ])) {
  1649. let symbol = match.tokens[0].content;
  1650. tokens.splice(match.index, 1, new MDFootnoteNode(symbol));
  1651. return true;
  1652. }
  1653. return false;
  1654. }
  1655. /**
  1656. * @param {MDState} state
  1657. * @param {MDBlockNode[]} blocks
  1658. */
  1659. postProcess(state, blocks) {
  1660. var nextOccurrenceId = 1;
  1661. for (const block of blocks) {
  1662. const me = this;
  1663. block.visitChildren((function(node) {
  1664. if (!(node instanceof MDFootnoteNode)) return;
  1665. node.footnoteId = me.#idForFootnoteSymbol(state, node.symbol);
  1666. node.occurrenceId = nextOccurrenceId++;
  1667. node.displaySymbol = `${node.footnoteId}`;
  1668. me.#registerUniqueInstance(state, node.symbol, node.occurrenceId);
  1669. }).bind(this));
  1670. }
  1671. if (Object.keys(state.footnotes).length == 0) return;
  1672. blocks.push(new MDFootnoteListNode());
  1673. }
  1674. compareBlockOrdering(other) {
  1675. if (other instanceof MDLinkReader || other instanceof MDImageReader) {
  1676. return -1;
  1677. }
  1678. return 0;
  1679. }
  1680. compareTokenizeOrdering(other) {
  1681. if (other instanceof MDLinkReader || other instanceof MDImageReader) {
  1682. return -1;
  1683. }
  1684. return 0;
  1685. }
  1686. compareSubstituteOrdering(other, pass) {
  1687. if (other instanceof MDLinkReader || other instanceof MDImageReader) {
  1688. return -1;
  1689. }
  1690. return 0;
  1691. }
  1692. }
  1693. /**
  1694. * Block reader for abbreviation definitions. Anywhere the abbreviation appears
  1695. * in the text will have its definition available when hovering over it.
  1696. * Definitions can appear anywhere in the document. Their content should only
  1697. * contain simple text, not markdown.
  1698. *
  1699. * Example:
  1700. *
  1701. * > ```markdown
  1702. * > *[HTML]: Hyper Text Markup Language
  1703. * > ```
  1704. */
  1705. class MDAbbreviationReader extends MDReader {
  1706. /**
  1707. * @param {MDState} state
  1708. * @param {string} abbreviation
  1709. * @param {string} definition
  1710. */
  1711. #defineAbbreviation(state, abbreviation, definition) {
  1712. state.abbreviations[abbreviation] = definition;
  1713. const regex = new RegExp("\\b(" + MDUtils.escapeRegex(abbreviation) + ")\\b", "ig");
  1714. state.abbreviationRegexes[abbreviation] = regex;
  1715. }
  1716. preProcess(state) {
  1717. state.root['abbreviations'] = {};
  1718. state.root['abbreviationRegexes'] = {};
  1719. }
  1720. readBlock(state) {
  1721. var p = state.p;
  1722. let line = state.lines[p++];
  1723. let groups = /^\s*\*\[([^\]]+?)\]:\s+(.*?)\s*$/.exec(line);
  1724. if (groups === null) return null;
  1725. let abbrev = groups[1];
  1726. let def = groups[2];
  1727. this.#defineAbbreviation(state, abbrev, def);
  1728. state.p = p;
  1729. return new MDNode(); // empty
  1730. }
  1731. /**
  1732. * @param {MDState} state
  1733. * @param {MDNode[]} blocks
  1734. */
  1735. postProcess(state, blocks) {
  1736. const abbreviations = state.root['abbreviations'];
  1737. const regexes = state.root['abbreviationRegexes'];
  1738. MDUtils.replaceNodes(state, blocks, (original) => {
  1739. if (!(original instanceof MDTextNode)) return null;
  1740. var changed = false;
  1741. var elems = [ original.text ]; // mix of strings and MDNodes
  1742. for (var i = 0; i < elems.length; i++) {
  1743. var text = elems[i];
  1744. if (typeof text !== 'string') continue;
  1745. for (const abbreviation in abbreviations) {
  1746. const groups = regexes[abbreviation].exec(text);
  1747. if (groups === null) continue;
  1748. const definition = abbreviations[abbreviation];
  1749. const prefix = text.substring(0, groups.index);
  1750. const suffix = text.substring(groups.index + groups[0].length);
  1751. elems.splice(i, 1, prefix, new MDAbbreviationNode(groups[0], definition), suffix);
  1752. i = -1; // start over
  1753. changed = true;
  1754. break;
  1755. }
  1756. }
  1757. if (!changed) return null;
  1758. const nodes = elems.map((elem) => typeof elem === 'string' ? new MDTextNode(elem) : elem);
  1759. return new MDNode(nodes);
  1760. });
  1761. }
  1762. }
  1763. /**
  1764. * Block reader for simple paragraphs. Paragraphs are separated by a blank (or
  1765. * whitespace-only) line. This reader is prioritized after every other reader
  1766. * since there is no distinguishing syntax.
  1767. */
  1768. class MDParagraphReader extends MDReader {
  1769. readBlock(state) {
  1770. var paragraphLines = [];
  1771. var p = state.p;
  1772. while (p < state.lines.length) {
  1773. let line = state.lines[p++];
  1774. if (line.trim().length == 0) {
  1775. break;
  1776. }
  1777. paragraphLines.push(line);
  1778. }
  1779. if (state.p == 0 && p >= state.lines.length) {
  1780. // If it's the entire document don't wrap it in a paragraph
  1781. return null;
  1782. }
  1783. if (paragraphLines.length > 0) {
  1784. state.p = p;
  1785. let content = paragraphLines.join("\n");
  1786. return new MDParagraphNode(state.inlineMarkdownToNodes(content));
  1787. }
  1788. return null;
  1789. }
  1790. compareBlockOrdering(other) {
  1791. return 1; // always dead last
  1792. }
  1793. }
  1794. /**
  1795. * Abstract base class for readers that look for one or more delimiting tokens
  1796. * around some content.
  1797. */
  1798. class MDSimplePairInlineReader extends MDReader {
  1799. get substitutionPassCount() { return 4; }
  1800. /**
  1801. * Attempts a substitution of a matched pair of delimiting token types.
  1802. * If successful, the substitution is performed on `tokens` and `true` is
  1803. * returned, otherwise `false` is returned and the array is untouched.
  1804. *
  1805. * If `this.substitutionPassCount` is greater than 1, the first pass
  1806. * will reject matches with the delimiting character inside the content
  1807. * tokens. If the reader uses a single pass or a subsequent pass is performed
  1808. * with multiple pass any contents will be accepted.
  1809. *
  1810. * @param {MDState} state
  1811. * @param {number} pass
  1812. * @param {MDToken[]} tokens
  1813. * @param {class} nodeClass
  1814. * @param {MDTokenType} delimiter
  1815. * @param {number} count - how many times the token is repeated to form the delimiter
  1816. * @returns {boolean} `true` if substitution performed, `false` if not
  1817. */
  1818. attemptPair(state, pass, tokens, nodeClass, delimiter, count=1, plaintext=false) {
  1819. // We do four passes. #1: doubles without inner tokens, #2: singles
  1820. // without inner tokens, #3: doubles with paired inner tokens,
  1821. // #4: singles with paired inner tokens
  1822. if (count == 1 && pass != 2 && pass != 4) return;
  1823. if (count == 2 && pass != 1 && pass != 3) return;
  1824. let delimiters = Array(count).fill(delimiter);
  1825. const isFirstOfMultiplePasses = this.substitutionPassCount > 1 && pass == 1;
  1826. let match = MDToken.findPairedTokens(tokens, delimiters, delimiters, function(content) {
  1827. const firstType = content[0] instanceof MDToken ? content[0].type : null;
  1828. const lastType = content[content.length - 1] instanceof MDToken ? content[content.length - 1].type : null;
  1829. if (firstType == MDTokenType.Whitespace) return false;
  1830. if (lastType == MDTokenType.Whitespace) return false;
  1831. for (const token of content) {
  1832. // Don't allow nesting
  1833. if (token.constructor == nodeClass) return false;
  1834. }
  1835. if (isFirstOfMultiplePasses) {
  1836. var innerCount = 0;
  1837. for (let token of content) {
  1838. if (token instanceof MDToken && token.type == delimiter) innerCount++;
  1839. }
  1840. if ((innerCount % 2) != 0) return false;
  1841. }
  1842. return true;
  1843. });
  1844. if (match === null) return false;
  1845. let content = (plaintext)
  1846. ? match.contentTokens.map((token) => token.original).join('')
  1847. : state.tokensToNodes(match.contentTokens);
  1848. tokens.splice(match.startIndex, match.totalLength, new nodeClass(content));
  1849. return true;
  1850. }
  1851. }
  1852. class MDEmphasisReader extends MDSimplePairInlineReader {
  1853. readToken(state, line) {
  1854. if (line.startsWith('*')) return new MDToken('*', MDTokenType.Asterisk);
  1855. if (line.startsWith('_')) return new MDToken('_', MDTokenType.Underscore);
  1856. return null;
  1857. }
  1858. substituteTokens(state, pass, tokens) {
  1859. if (this.attemptPair(state, pass, tokens, MDEmphasisNode, MDTokenType.Asterisk)) return true;
  1860. if (this.attemptPair(state, pass, tokens, MDEmphasisNode, MDTokenType.Underscore)) return true;
  1861. return false;
  1862. }
  1863. compareSubstituteOrdering(other, pass) {
  1864. if (other instanceof MDStrongReader) {
  1865. return 1;
  1866. }
  1867. return 0;
  1868. }
  1869. }
  1870. class MDStrongReader extends MDSimplePairInlineReader {
  1871. readToken(state, line) {
  1872. if (line.startsWith('*')) return new MDToken('*', MDTokenType.Asterisk);
  1873. if (line.startsWith('_')) return new MDToken('_', MDTokenType.Underscore);
  1874. return null;
  1875. }
  1876. substituteTokens(state, pass, tokens) {
  1877. if (this.attemptPair(state, pass, tokens, MDStrongNode, MDTokenType.Asterisk, 2)) return true;
  1878. if (this.attemptPair(state, pass, tokens, MDStrongNode, MDTokenType.Underscore, 2)) return true;
  1879. return false;
  1880. }
  1881. compareSubstituteOrdering(other, pass) {
  1882. if (other instanceof MDEmphasisReader) {
  1883. return -1;
  1884. }
  1885. return 0;
  1886. }
  1887. }
  1888. class MDStrikethroughReader extends MDSimplePairInlineReader {
  1889. readToken(state, line) {
  1890. if (line.startsWith('~')) return new MDToken('~', MDTokenType.Tilde);
  1891. return null;
  1892. }
  1893. substituteTokens(state, pass, tokens) {
  1894. if (this.attemptPair(state, pass, tokens, MDStrikethroughNode, MDTokenType.Tilde, 2)) return true;
  1895. if (state.config.strikethroughSingleTildeEnabled) {
  1896. if (this.attemptPair(state, pass, tokens, MDStrikethroughNode, MDTokenType.Tilde)) return true;
  1897. }
  1898. return false;
  1899. }
  1900. }
  1901. class MDUnderlineReader extends MDSimplePairInlineReader {
  1902. readToken(state, line) {
  1903. if (line.startsWith('_')) return new MDToken('_', MDTokenType.Underscore);
  1904. return null;
  1905. }
  1906. substituteTokens(state, pass, tokens) {
  1907. return this.attemptPair(state, pass, tokens, MDUnderlineNode, MDTokenType.Underscore, 2);
  1908. }
  1909. compareSubstituteOrdering(other, pass) {
  1910. if (other instanceof MDStrongReader) {
  1911. return -1;
  1912. }
  1913. return 0;
  1914. }
  1915. }
  1916. class MDHighlightReader extends MDSimplePairInlineReader {
  1917. readToken(state, line) {
  1918. if (line.startsWith('=')) return new MDToken('=', MDTokenType.Equal);
  1919. return null;
  1920. }
  1921. substituteTokens(state, pass, tokens) {
  1922. return this.attemptPair(state, pass, tokens, MDHighlightNode, MDTokenType.Equal, 2);
  1923. }
  1924. }
  1925. class MDLinkReader extends MDReader {
  1926. static #simpleEmailRegex = new RegExp("^<(" + MDUtils.baseEmailRegex.source + ")>", "i"); // 1=email
  1927. static #simpleURLRegex = new RegExp("^<(" + MDUtils.baseURLRegex.source + ")>", "i"); // 1=URL
  1928. readToken(state, line) {
  1929. var groups;
  1930. if (groups = MDUtils.tokenizeLabel(line)) {
  1931. return new MDToken(groups[0], MDTokenType.Label, groups[1]);
  1932. }
  1933. if (groups = MDUtils.tokenizeEmail(line)) {
  1934. return new MDToken(groups[0], MDTokenType.Email, groups[1], groups[2]);
  1935. }
  1936. if (groups = MDUtils.tokenizeURL(line)) {
  1937. return new MDToken(groups[0], MDTokenType.URL, groups[1], groups[2]);
  1938. }
  1939. if (groups = MDLinkReader.#simpleEmailRegex.exec(line)) {
  1940. return new MDToken(groups[0], MDTokenType.SimpleEmail, groups[1]);
  1941. }
  1942. if (groups = MDLinkReader.#simpleURLRegex.exec(line)) {
  1943. return new MDToken(groups[0], MDTokenType.SimpleLink, groups[1]);
  1944. }
  1945. return null;
  1946. }
  1947. substituteTokens(state, pass, tokens) {
  1948. var match;
  1949. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.Label, MDTokenType.META_OptionalWhitespace, MDTokenType.URL ])) {
  1950. let text = match.tokens[0].content;
  1951. let url = match.tokens[match.tokens.length - 1].content;
  1952. let title = match.tokens[match.tokens.length - 1].extra;
  1953. tokens.splice(match.index, match.tokens.length, new MDLinkNode(url, state.inlineMarkdownToNode(text), title));
  1954. return true;
  1955. }
  1956. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.Label, MDTokenType.META_OptionalWhitespace, MDTokenType.Email ])) {
  1957. let text = match.tokens[0].content;
  1958. let email = match.tokens[match.tokens.length - 1].content;
  1959. let url = `mailto:${email}`;
  1960. let title = match.tokens[match.tokens.length - 1].extra;
  1961. tokens.splice(match.index, match.tokens.length, new MDLinkNode(url, state.inlineMarkdownToNodes(text), title));
  1962. return true;
  1963. }
  1964. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.SimpleEmail ])) {
  1965. const token = match.tokens[0];
  1966. const link = `mailto:${token.content}`;
  1967. const node = new MDLinkNode(link, new MDObfuscatedTextNode(token.content));
  1968. tokens.splice(match.index, 1, node);
  1969. return true;
  1970. }
  1971. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.SimpleLink ])) {
  1972. const token = match.tokens[0];
  1973. const link = token.content;
  1974. const node = new MDLinkNode(link, new MDTextNode(link));
  1975. tokens.splice(match.index, 1, node);
  1976. return true;
  1977. }
  1978. return false;
  1979. }
  1980. }
  1981. /**
  1982. * Block reader for URL definitions. Links in the document can include a
  1983. * reference instead of a verbatim URL so it can be defined in one place and
  1984. * reused in many places. These can be defined anywhere in the document. Nothing
  1985. * of the definition is rendered in the document.
  1986. *
  1987. * Example:
  1988. *
  1989. * > ```markdown
  1990. * > [foo]: https://example.com
  1991. * > ```
  1992. */
  1993. class MDReferencedLinkReader extends MDLinkReader {
  1994. /**
  1995. * @param {MDState} state
  1996. */
  1997. readBlock(state) {
  1998. var p = state.p;
  1999. let line = state.lines[p++];
  2000. var symbol;
  2001. var url;
  2002. var title = null;
  2003. let groups = /^\s*\[(.+?)]:\s*(\S+)\s+"(.*?)"\s*$/.exec(line);
  2004. if (groups) {
  2005. symbol = groups[1];
  2006. url = groups[2];
  2007. title = groups[3];
  2008. } else {
  2009. groups = /^\s*\[(.+?)]:\s*(\S+)\s*$/.exec(line);
  2010. if (groups) {
  2011. symbol = groups[1];
  2012. url = groups[2];
  2013. } else {
  2014. return null;
  2015. }
  2016. }
  2017. state.defineURL(symbol, url, title);
  2018. state.p = p;
  2019. return new MDNode([]); // empty
  2020. }
  2021. substituteTokens(state, pass, tokens) {
  2022. var match;
  2023. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.Label, MDTokenType.META_OptionalWhitespace, MDTokenType.Label ])) {
  2024. let text = match.tokens[0].content;
  2025. let ref = match.tokens[match.tokens.length - 1].content;
  2026. tokens.splice(match.index, match.tokens.length, new MDReferencedLinkNode(ref, state.inlineMarkdownToNodes(text)));
  2027. return true;
  2028. }
  2029. return false;
  2030. }
  2031. }
  2032. class MDImageReader extends MDLinkReader {
  2033. readToken(state, line) {
  2034. const s = super.readToken(state, line);
  2035. if (s) return s;
  2036. if (line.startsWith('!')) return new MDToken('!', MDTokenType.Bang);
  2037. return null;
  2038. }
  2039. substituteTokens(state, pass, tokens) {
  2040. var match;
  2041. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.Bang, MDTokenType.Label, MDTokenType.META_OptionalWhitespace, MDTokenType.URL ])) {
  2042. let alt = match.tokens[1].content;
  2043. let url = match.tokens[match.tokens.length - 1].content;
  2044. let title = match.tokens[match.tokens.length - 1].extra;
  2045. const node = new MDImageNode(url, alt);
  2046. if (title !== null) {
  2047. node.attributes['title'] = title;
  2048. }
  2049. tokens.splice(match.index, match.tokens.length, node);
  2050. return true;
  2051. }
  2052. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.Bang, MDTokenType.Label, MDTokenType.META_OptionalWhitespace, MDTokenType.Label ])) {
  2053. let alt = match.tokens[1].content;
  2054. let ref = match.tokens[match.tokens.length - 1].content;
  2055. tokens.splice(match.index, match.tokens.length, new MDReferencedImageNode(ref, alt));
  2056. return true;
  2057. }
  2058. return false;
  2059. }
  2060. compareSubstituteOrdering(other, pass) {
  2061. if (other instanceof MDLinkReader) {
  2062. return -1;
  2063. }
  2064. return 0;
  2065. }
  2066. }
  2067. class MDReferencedImageReader extends MDReferencedLinkReader {
  2068. readBlock(state) { return null; }
  2069. compareSubstituteOrdering(other, pass) {
  2070. if (other instanceof MDLinkReader) {
  2071. return -1;
  2072. }
  2073. return 0;
  2074. }
  2075. }
  2076. class MDCodeSpanReader extends MDSimplePairInlineReader {
  2077. readToken(state, line) {
  2078. if (line.startsWith('`')) return new MDToken('`', MDTokenType.Backtick);
  2079. return null;
  2080. }
  2081. substituteTokens(state, pass, tokens) {
  2082. if (this.attemptPair(state, pass, tokens, MDCodeNode, MDTokenType.Backtick, 2, true)) return true;
  2083. if (this.attemptPair(state, pass, tokens, MDCodeNode, MDTokenType.Backtick, 1, true)) return true;
  2084. }
  2085. }
  2086. class MDSubscriptReader extends MDSimplePairInlineReader {
  2087. readToken(state, line) {
  2088. if (line.startsWith('~')) return new MDToken('~', MDTokenType.Tilde);
  2089. return null;
  2090. }
  2091. preProcess(state) {
  2092. // Causes a conflict
  2093. state.config.strikethroughSingleTildeEnabled = false;
  2094. }
  2095. substituteTokens(state, pass, tokens) {
  2096. return this.attemptPair(state, pass, tokens, MDSubscriptNode, MDTokenType.Tilde);
  2097. }
  2098. compareSubstituteOrdering(other, pass) {
  2099. if (other instanceof MDStrikethroughReader) {
  2100. return -1;
  2101. }
  2102. return 0;
  2103. }
  2104. }
  2105. class MDSuperscriptReader extends MDSimplePairInlineReader {
  2106. readToken(state, line) {
  2107. if (line.startsWith('^')) return new MDToken('^', MDTokenType.Caret);
  2108. return null;
  2109. }
  2110. substituteTokens(state, pass, tokens) {
  2111. return this.attemptPair(state, pass, tokens, MDSuperscriptNode, MDTokenType.Caret);
  2112. }
  2113. }
  2114. class MDHTMLTagReader extends MDReader {
  2115. readToken(state, line) {
  2116. const tag = MDHTMLTag.fromLineStart(line)
  2117. if (tag === null) return null;
  2118. if (!state.root.tagFilter.isValidTagName(tag.tagName)) return null;
  2119. state.root.tagFilter.scrubTag(tag);
  2120. return new MDToken(tag.original, MDTokenType.HTMLTag, null, null, tag)
  2121. }
  2122. substituteTokens(state, pass, tokens) {
  2123. var match;
  2124. if (match = MDToken.findFirstTokens(tokens, [ MDTokenType.HTMLTag ])) {
  2125. const tag = match.tokens[0].tag
  2126. tokens.splice(match.index, match.tokens.length, new MDHTMLTagNode(tag))
  2127. return true;
  2128. }
  2129. return false;
  2130. }
  2131. }
  2132. class MDModifierReader extends MDReader {
  2133. readToken(state, line) {
  2134. var modifier = MDTagModifier.fromStart(line);
  2135. if (modifier) return new MDToken(modifier.original, MDTokenType.Modifier, modifier);
  2136. return null;
  2137. }
  2138. substituteTokens(state, pass, tokens) {
  2139. // Modifiers are applied elsewhere, and if they're not it's fine if they're
  2140. // rendered as the original syntax.
  2141. return false;
  2142. }
  2143. }
  2144. // -- Document nodes --------------------------------------------------------
  2145. class MDNode {
  2146. /**
  2147. * Array of CSS classes to add to the node when rendered as HTML.
  2148. * @type {string[]}
  2149. */
  2150. cssClasses = [];
  2151. /** @type {string|null} */
  2152. cssId = null;
  2153. /**
  2154. * Mapping of CSS attributes to values.
  2155. * @type {object}
  2156. */
  2157. cssStyles = {};
  2158. /**
  2159. * Mapping of arbitrary attributes and values to add to this node's top-level
  2160. * tag when rendered as HTML. For `class`, `id`, and `style` attributes, use
  2161. * `cssClasses`, `cssId`, and `cssStyles` instead.
  2162. * @type {object}
  2163. */
  2164. attributes = {};
  2165. /**
  2166. * All child nodes in this node.
  2167. * @type {MDNode[]}
  2168. */
  2169. children;
  2170. /**
  2171. * @param {MDNode[]} children
  2172. */
  2173. constructor(children=[]) {
  2174. if (children instanceof Array) {
  2175. for (const elem of children) {
  2176. if (!(elem instanceof MDNode)) {
  2177. throw new Error(`${this.constructor.name} expects children of type MDNode[] or MDNode, got array with ${MDUtils.typename(elem)} element`);
  2178. }
  2179. }
  2180. this.children = children;
  2181. } else if (children instanceof MDNode) {
  2182. this.children = [ children ];
  2183. } else {
  2184. throw new Error(`${this.constructor.name} expects children of type MDNode[] or MDNode, got ${MDUtils.typename(children)}`);
  2185. }
  2186. }
  2187. /**
  2188. * Renders this node and any children as an HTML string. If the node has no
  2189. * content an empty string should be returned.
  2190. *
  2191. * @param {MDState} state
  2192. * @returns {string} HTML string
  2193. */
  2194. toHTML(state) {
  2195. return MDNode.toHTML(this.children, state);
  2196. }
  2197. /**
  2198. * Renders this node and any children as a plain text string. The conversion
  2199. * should only render ordinary text, not attempt markdown-like formatting
  2200. * (e.g. list items should not be prefixed with asterisks, only have their
  2201. * content text returned). If the node has no renderable content an empty
  2202. * string should be returned.
  2203. *
  2204. * @param {MDState} state
  2205. * @returns {string} plaintext string
  2206. */
  2207. toPlaintext(state) {
  2208. return MDNode.toPlaintext(this.children, state);
  2209. }
  2210. /**
  2211. * Helper that renders an HTML fragment of the attributes to apply to the
  2212. * root HTML tag representation of this node.
  2213. *
  2214. * Example result with a couple `cssClasses`, a `cssId`, and a custom
  2215. * `attributes` key-value pair:
  2216. *
  2217. * ```
  2218. * class="foo bar" id="baz" lang="en"
  2219. * ```
  2220. *
  2221. * The value includes a leading space if it's non-empty so that it can be
  2222. * concatenated directly after the tag name and before the closing `>`.
  2223. *
  2224. * @returns {string} HTML fragment
  2225. */
  2226. _htmlAttributes() {
  2227. var html = '';
  2228. if (this.cssClasses.length > 0) {
  2229. html += ` class="${this.cssClasses.join(' ')}"`;
  2230. }
  2231. if (this.cssId !== null && this.cssId.length > 0) {
  2232. html += ` id="${this.cssId}"`;
  2233. }
  2234. var styles = [];
  2235. for (const key in this.cssStyles) {
  2236. styles.push(`${key}: ${this.cssStyles[key]};`)
  2237. }
  2238. if (styles.length > 0) {
  2239. html += ` style="${MDUtils.escapeHTML(styles.join(' '))}"`;
  2240. }
  2241. for (const key in this.attributes) {
  2242. if (key == 'class' || key == 'id' || key == 'style') continue;
  2243. const value = `${this.attributes[key]}`;
  2244. const cleanKey = MDUtils.scrubAttributeName(key);
  2245. if (cleanKey.length == 0) continue;
  2246. const cleanValue = MDUtils.escapeHTML(value);
  2247. html += ` ${cleanKey}="${cleanValue}"`;
  2248. }
  2249. return html;
  2250. }
  2251. /**
  2252. * Helper that renders the children of this node to HTML. Mostly for use by
  2253. * subclasses in their `toHTML` implementation.
  2254. *
  2255. * @param {MDState} state
  2256. * @returns {string}
  2257. */
  2258. _childHTML(state) {
  2259. return this.children.map((child) => child.toHTML(state)).join('');
  2260. }
  2261. /**
  2262. * @param {MDState} state
  2263. * @param {string} tagName
  2264. * @param {boolean} innerNewLines
  2265. * @returns {string}
  2266. */
  2267. _simplePairedTagHTML(state, tagName, innerNewLines=false) {
  2268. const openTagSuffix = this.children[0] instanceof MDBlockNode ? '\n' : ''
  2269. const closeTagPrefix = this.children[this.children.length - 1] instanceof MDBlockNode ? '\n' : '';
  2270. const closeTagSuffix = this instanceof MDBlockNode ? '\n' : '';
  2271. return `<${tagName}${this._htmlAttributes()}>${openTagSuffix}${this._childHTML(state)}${closeTagPrefix}</${tagName}>${closeTagSuffix}`;
  2272. }
  2273. /**
  2274. * Calls the given callback function with every child node, recursively.
  2275. * Nodes are visited depth-first.
  2276. *
  2277. * @param {function} fn - callback that takes one `MDNode` argument
  2278. */
  2279. visitChildren(fn) {
  2280. if (this.children === undefined || !Array.isArray(this.children)) {
  2281. return;
  2282. }
  2283. for (const child of this.children) {
  2284. fn(child);
  2285. child.visitChildren(fn);
  2286. }
  2287. }
  2288. /**
  2289. * @param {MDNode[]} nodes
  2290. * @param {MDState} state
  2291. * @returns {string}
  2292. */
  2293. static toHTML(nodes, state) {
  2294. return nodes.map((node) => node.toHTML(state) + (node instanceof MDBlockNode ? '\n' : '')).join('');
  2295. }
  2296. /**
  2297. * @param {MDNode[]} nodes
  2298. * @param {MDState} state
  2299. * @returns {string}
  2300. */
  2301. static toPlaintext(nodes, state) {
  2302. return nodes.map((node) => node.toPlaintext(state)).join('');
  2303. }
  2304. }
  2305. class MDBlockNode extends MDNode {}
  2306. class MDParagraphNode extends MDBlockNode {
  2307. toHTML(state) {
  2308. return this._simplePairedTagHTML(state, 'p');
  2309. }
  2310. }
  2311. class MDHeadingNode extends MDBlockNode {
  2312. /** @type {number} */
  2313. level;
  2314. constructor(level, children) {
  2315. super(children);
  2316. if (typeof level !== 'number' || (level < 1 || level > 6)) {
  2317. throw new Error(`${this.constructor.name} requires heading level 1 to 6`);
  2318. }
  2319. this.level = level;
  2320. }
  2321. toHTML(state) {
  2322. return this._simplePairedTagHTML(state, `h${this.level}`);
  2323. }
  2324. }
  2325. class MDSubtextNode extends MDBlockNode {
  2326. toHTML(state) {
  2327. if (this.cssClasses.indexOf('subtext') < 0) {
  2328. this.cssClasses.push('subtext');
  2329. }
  2330. return this._simplePairedTagHTML(state, 'div');
  2331. }
  2332. }
  2333. class MDHorizontalRuleNode extends MDBlockNode {
  2334. toHTML(state) {
  2335. return `<hr${this._htmlAttributes()}>`;
  2336. }
  2337. }
  2338. class MDBlockquoteNode extends MDBlockNode {
  2339. toHTML(state) {
  2340. return this._simplePairedTagHTML(state, 'blockquote', true);
  2341. }
  2342. }
  2343. class MDUnorderedListNode extends MDBlockNode {
  2344. /** @type {MDListItemNode[]} children */
  2345. /**
  2346. * @param {MDListItemNode[]} children
  2347. */
  2348. constructor(children) {
  2349. super(children);
  2350. }
  2351. toHTML(state) {
  2352. return this._simplePairedTagHTML(state, 'ul', true);
  2353. }
  2354. }
  2355. class MDOrderedListNode extends MDBlockNode {
  2356. /** @type {MDListItemNode[]} children */
  2357. /** @type {number|null} */
  2358. startOrdinal;
  2359. /**
  2360. * @param {MDListItemNode[]} children
  2361. * @param {number|null} startOrdinal
  2362. */
  2363. constructor(children, startOrdinal=null) {
  2364. super(children);
  2365. this.startOrdinal = startOrdinal;
  2366. }
  2367. toHTML(state) {
  2368. if (this.startOrdinal !== null && this.startOrdinal != 1) this.attributes['start'] = this.startOrdinal;
  2369. return this._simplePairedTagHTML(state, 'ol', true);
  2370. }
  2371. }
  2372. class MDListItemNode extends MDBlockNode {
  2373. /** @type {number|null} */
  2374. ordinal;
  2375. /**
  2376. * @param {MDNode|MDNode[]} children
  2377. * @param {number|null} ordinal
  2378. */
  2379. constructor(children, ordinal=null) {
  2380. super(children);
  2381. this.ordinal = ordinal;
  2382. }
  2383. toHTML(state) {
  2384. return this._simplePairedTagHTML(state, 'li');
  2385. }
  2386. }
  2387. class MDCodeBlockNode extends MDBlockNode {
  2388. /** @type {string} */
  2389. text;
  2390. /**
  2391. * @param {string} text
  2392. */
  2393. constructor(text) {
  2394. super([]);
  2395. this.text = text;
  2396. }
  2397. toHTML(state) {
  2398. return `<pre${this._htmlAttributes()}><code>${MDUtils.escapeHTML(this.text)}</code></pre>\n`;
  2399. }
  2400. }
  2401. class MDTableNode extends MDBlockNode {
  2402. /** @param {MDTableRowNode[]} children */
  2403. /** @type {MDTableRowNode} */
  2404. get headerRow() { return this.#headerRow; }
  2405. set headerRow(newValue) {
  2406. this.#headerRow = newValue;
  2407. this.#recalculateChildren();
  2408. }
  2409. #headerRow;
  2410. /** @type {MDTableRowNode[]} */
  2411. get bodyRows() { return this.#bodyRows; }
  2412. set bodyRows(newValue) {
  2413. this.#bodyRows = newValue;
  2414. this.#recalculateChildren();
  2415. }
  2416. #bodyRows;
  2417. /**
  2418. * How to align each column. Columns beyond the length of the array or with
  2419. * corresponding `null` elements will have no alignment set. Values should
  2420. * be valid CSS `text-align` values.
  2421. *
  2422. * @type {string[]}
  2423. */
  2424. columnAlignments = [];
  2425. /**
  2426. * @param {MDTableRowNode} headerRow
  2427. * @param {MDTableRowNode[]} bodyRows
  2428. */
  2429. constructor(headerRow, bodyRows) {
  2430. super([ headerRow, ...bodyRows ]);
  2431. this.#headerRow = headerRow;
  2432. this.#bodyRows = bodyRows;
  2433. }
  2434. #recalculateChildren() {
  2435. this.children = [ this.#headerRow, ...this.#bodyRows ];
  2436. }
  2437. #applyAlignments() {
  2438. this.children.forEach((child) => this.#applyAlignmentsToRow(child));
  2439. }
  2440. /**
  2441. * @param {MDTableRowNode} row
  2442. */
  2443. #applyAlignmentsToRow(row) {
  2444. for (const [columnIndex, cell] of row.children.entries()) {
  2445. const alignment = columnIndex < this.columnAlignments.length ? this.columnAlignments[columnIndex] : null;
  2446. this.#applyAlignmentToCell(cell, alignment);
  2447. }
  2448. }
  2449. /**
  2450. * @param {MDTableCellNode} cell
  2451. * @param {string|null} alignment
  2452. */
  2453. #applyAlignmentToCell(cell, alignment) {
  2454. if (alignment) {
  2455. cell.cssStyles['text-align'] = alignment;
  2456. } else {
  2457. delete cell.cssStyles['text-align'];
  2458. }
  2459. }
  2460. toHTML(state) {
  2461. this.#applyAlignments();
  2462. var html = '';
  2463. html += `<table${this._htmlAttributes()}>\n`;
  2464. html += '<thead>\n';
  2465. html += this.headerRow.toHTML(state) + '\n';
  2466. html += '</thead>\n';
  2467. html += '<tbody>\n';
  2468. html += MDNode.toHTML(this.bodyRows, state) + '\n';
  2469. html += '</tbody>\n';
  2470. html += '</table>\n';
  2471. return html;
  2472. }
  2473. }
  2474. class MDTableRowNode extends MDBlockNode {
  2475. /** @type {MDTableCellNode[]} children */
  2476. toHTML(state) {
  2477. return this._simplePairedTagHTML(state, 'tr', true);
  2478. }
  2479. }
  2480. class MDTableCellNode extends MDBlockNode {
  2481. toHTML(state) {
  2482. return this._simplePairedTagHTML(state, 'td');
  2483. }
  2484. }
  2485. class MDTableHeaderCellNode extends MDBlockNode {
  2486. toHTML(state) {
  2487. return this._simplePairedTagHTML(state, 'th');
  2488. }
  2489. }
  2490. class MDDefinitionListNode extends MDBlockNode {
  2491. toHTML(state) {
  2492. return this._simplePairedTagHTML(state, 'dl', true);
  2493. }
  2494. }
  2495. class MDDefinitionListTermNode extends MDBlockNode {
  2496. toHTML(state) {
  2497. return this._simplePairedTagHTML(state, 'dt');
  2498. }
  2499. }
  2500. class MDDefinitionListDefinitionNode extends MDBlockNode {
  2501. toHTML(state) {
  2502. return this._simplePairedTagHTML(state, 'dd');
  2503. }
  2504. }
  2505. class MDFootnoteListNode extends MDBlockNode {
  2506. toHTML(state) {
  2507. const footnotes = state.footnotes;
  2508. var symbolOrder = Object.keys(footnotes);
  2509. if (Object.keys(footnotes).length == 0) return '';
  2510. const footnoteUniques = state.root.footnoteInstances;
  2511. var html = '';
  2512. html += '<div class="footnotes"><hr/>';
  2513. html += '<ol>';
  2514. for (const symbol of symbolOrder) {
  2515. /** @type {MDNode[]} */
  2516. let content = footnotes[symbol];
  2517. if (!content) continue;
  2518. const contentHTML = MDNode.toHTML(content, state);
  2519. html += `<li value="${symbol}" id="footnote_${symbol}">${contentHTML}`;
  2520. const uniques = footnoteUniques[symbol];
  2521. if (uniques) {
  2522. for (const unique of uniques) {
  2523. html += ` <a href="#footnoteref_${unique}" class="footnote-backref">↩︎</a>`;
  2524. }
  2525. }
  2526. html += `</li>\n`;
  2527. }
  2528. html += '</ol>';
  2529. html += '</div>';
  2530. return html;
  2531. }
  2532. toPlaintext(state) {
  2533. const footnotes = state.footnotes;
  2534. var symbolOrder = Object.keys(footnotes);
  2535. if (Object.keys(footnotes).length == 0) return '';
  2536. var text = '';
  2537. for (const symbol of symbolOrder) {
  2538. let content = footnotes[symbol];
  2539. if (!content) continue;
  2540. text += `${symbol}. ${content.toPlaintext(state)}\n`;
  2541. }
  2542. return text.trim();
  2543. }
  2544. }
  2545. class MDInlineNode extends MDNode {}
  2546. class MDTextNode extends MDInlineNode {
  2547. text;
  2548. constructor(text) {
  2549. super([]);
  2550. this.text = text;
  2551. }
  2552. toHTML(state) {
  2553. return MDUtils.escapeHTML(this.text);
  2554. }
  2555. toPlaintext(state) {
  2556. return this.text;
  2557. }
  2558. }
  2559. class MDObfuscatedTextNode extends MDTextNode {
  2560. toHTML(state) {
  2561. return MDUtils.escapeObfuscated(this.text);
  2562. }
  2563. }
  2564. class MDEmphasisNode extends MDInlineNode {
  2565. toHTML(state) {
  2566. return this._simplePairedTagHTML(state, 'em');
  2567. }
  2568. }
  2569. class MDStrongNode extends MDInlineNode {
  2570. toHTML(state) {
  2571. return this._simplePairedTagHTML(state, 'strong');
  2572. }
  2573. }
  2574. class MDStrikethroughNode extends MDInlineNode {
  2575. toHTML(state) {
  2576. return this._simplePairedTagHTML(state, 's');
  2577. }
  2578. }
  2579. class MDUnderlineNode extends MDInlineNode {
  2580. toHTML(state) {
  2581. return this._simplePairedTagHTML(state, 'u');
  2582. }
  2583. }
  2584. class MDHighlightNode extends MDInlineNode {
  2585. toHTML(state) {
  2586. return this._simplePairedTagHTML(state, 'mark');
  2587. }
  2588. }
  2589. class MDSuperscriptNode extends MDInlineNode {
  2590. toHTML(state) {
  2591. return this._simplePairedTagHTML(state, 'sup');
  2592. }
  2593. }
  2594. class MDSubscriptNode extends MDInlineNode {
  2595. toHTML(state) {
  2596. return this._simplePairedTagHTML(state, 'sub');
  2597. }
  2598. }
  2599. class MDCodeNode extends MDInlineNode {
  2600. /** @type {string} */
  2601. text;
  2602. constructor(text) {
  2603. super([]);
  2604. this.text = text;
  2605. }
  2606. toHTML(state) {
  2607. return `<code${this._htmlAttributes()}>${MDUtils.escapeHTML(this.text)}</code>`;
  2608. }
  2609. }
  2610. class MDFootnoteNode extends MDInlineNode {
  2611. /**
  2612. * Symbol the author used to match up the footnote to its content definition.
  2613. * @type {string}
  2614. */
  2615. symbol;
  2616. /**
  2617. * The superscript symbol rendered in HTML. May be the same or different
  2618. * than `symbol`.
  2619. * @type {string} display symbol
  2620. */
  2621. displaySymbol = null;
  2622. /**
  2623. * Unique ID for the footnote definition.
  2624. * @type {number|null}
  2625. */
  2626. footnoteId = null;
  2627. /**
  2628. * Unique number for backlinking to a footnote occurrence. Populated by
  2629. * `MDFootnoteReader.postProcess`.
  2630. * @type {number|null}
  2631. */
  2632. occurrenceId = null;
  2633. /**
  2634. * @param {string} symbol
  2635. * @param {string|null} title
  2636. */
  2637. constructor(symbol, title=null) {
  2638. super([]);
  2639. this.symbol = symbol;
  2640. if (title) this.attributes['title'] = title;
  2641. }
  2642. toHTML(state) {
  2643. if (this.differentiator !== null) {
  2644. return `<sup id="footnoteref_${this.occurrenceId}"${this._htmlAttributes()}><a href="#footnote_${this.footnoteId}">${MDUtils.escapeHTML(this.displaySymbol ?? this.symbol)}</a></sup>`;
  2645. }
  2646. return `<!--FNREF:{${this.symbol}}-->`;
  2647. }
  2648. }
  2649. class MDLinkNode extends MDInlineNode {
  2650. /** @type {string} */
  2651. href;
  2652. /**
  2653. * @param {string} href
  2654. * @param {MDNode[]|MDNode} children
  2655. */
  2656. constructor(href, children, title=null) {
  2657. super(children);
  2658. this.href = href;
  2659. if (title !== null) this.attributes['title'] = title;
  2660. }
  2661. toHTML(state) {
  2662. var escapedLink;
  2663. if (this.href.startsWith('mailto:')) {
  2664. escapedLink = MDUtils.escapeObfuscated(this.href);
  2665. } else {
  2666. escapedLink = MDUtils.escapeHTML(this.href);
  2667. }
  2668. return `<a href="${escapedLink}"${this._htmlAttributes()}>${this._childHTML(state)}</a>`;
  2669. }
  2670. }
  2671. class MDReferencedLinkNode extends MDLinkNode {
  2672. /** @type {string} */
  2673. reference;
  2674. constructor(reference, children) {
  2675. super('', children);
  2676. this.reference = reference;
  2677. }
  2678. /**
  2679. * @param {MDState} state
  2680. */
  2681. toHTML(state) {
  2682. if (this.href === '') {
  2683. this.href = state.urlForReference(this.reference);
  2684. const title = state.urlTitleForReference(this.reference);
  2685. if (title) this.attributes['title'] = title;
  2686. }
  2687. return super.toHTML(state);
  2688. }
  2689. }
  2690. class MDImageNode extends MDInlineNode {
  2691. /** @type {string} */
  2692. src;
  2693. /** @type {string|null} */
  2694. alt;
  2695. /**
  2696. * @param {string} src
  2697. * @param {string|null} alt
  2698. */
  2699. constructor(src, alt) {
  2700. super([]);
  2701. this.src = src;
  2702. this.alt = alt;
  2703. }
  2704. toHTML(state) {
  2705. var html = `<img src="${MDUtils.escapeHTML(this.src)}"`;
  2706. if (this.alt) html += ` alt="${MDUtils.escapeHTML(this.alt)}"`;
  2707. html += `${this._htmlAttributes()}>`;
  2708. return html;
  2709. }
  2710. }
  2711. class MDReferencedImageNode extends MDImageNode {
  2712. /** @type {string} */
  2713. reference;
  2714. /**
  2715. * @param {string} reference
  2716. * @param {string|null} alt
  2717. */
  2718. constructor(reference, alt='') {
  2719. super('', alt, []);
  2720. this.reference = reference;
  2721. }
  2722. toHTML(state) {
  2723. if (this.src === '') {
  2724. this.src = state.urlForReference(this.reference);
  2725. this.attributes['title'] = state.urlTitleForReference(this.reference);
  2726. }
  2727. return super.toHTML(state);
  2728. }
  2729. }
  2730. class MDAbbreviationNode extends MDInlineNode {
  2731. /** @type {string} */
  2732. abbreviation;
  2733. /** @type {string} */
  2734. get definition() { return this.attributes['title'] ?? null; }
  2735. set definition(newValue) { this.attributes['title'] = newValue; }
  2736. /**
  2737. * @param {string} abbreviation
  2738. * @param {string} definition
  2739. */
  2740. constructor(abbreviation, definition) {
  2741. super([]);
  2742. this.abbreviation = abbreviation;
  2743. this.attributes['title'] = definition;
  2744. }
  2745. toHTML(state) {
  2746. return `<abbr${this._htmlAttributes()}>${MDUtils.escapeHTML(this.abbreviation)}</abbr>`;
  2747. }
  2748. }
  2749. class MDLineBreakNode extends MDInlineNode {
  2750. toHTML(state) {
  2751. return '<br>';
  2752. }
  2753. toPlaintext(state) {
  2754. return '\n';
  2755. }
  2756. }
  2757. class MDHTMLTagNode extends MDInlineNode {
  2758. /** @type {MDHTMLTag} */
  2759. tag;
  2760. constructor(tag) {
  2761. super([]);
  2762. this.tag = tag;
  2763. }
  2764. toHTML(state) {
  2765. return this.tag.toString();
  2766. }
  2767. }
  2768. // -- Other -----------------------------------------------------------------
  2769. /**
  2770. * Helps to reject unapproved HTML, tag attributes, and CSS.
  2771. */
  2772. class MDHTMLFilter {
  2773. /**
  2774. * Mapping of permitted lowercase tag names to objects containing allowable
  2775. * attributes for those tags. Does not need to include those attributes
  2776. * defined in `allowableGlobalAttributes`.
  2777. *
  2778. * Values are objects with allowable lowercase attribute names mapped to
  2779. * allowable value patterns. A `*` means any value is acceptable. Multiple
  2780. * allowable values can be joined together with `|`. These special symbols
  2781. * represent certain kinds of values and can be used in combination or in
  2782. * place of literal values.
  2783. *
  2784. * - `{classlist}`: A list of legal CSS classnames, separated by spaces
  2785. * - `{int}`: An integer
  2786. * - `{none}`: No value (an attribute with no `=` or value, like `checked`)
  2787. * - `{style}`: One or more CSS declarations, separated by semicolons (simple
  2788. * `key: value;` syntax only)
  2789. * - `{url}`: A URL
  2790. * @type {object}
  2791. */
  2792. allowableTags = {
  2793. 'address': {
  2794. 'cite': '{url}',
  2795. },
  2796. 'h1': {},
  2797. 'h2': {},
  2798. 'h3': {},
  2799. 'h4': {},
  2800. 'h5': {},
  2801. 'h6': {},
  2802. 'blockquote': {},
  2803. 'dl': {},
  2804. 'dt': {},
  2805. 'dd': {},
  2806. 'div': {},
  2807. 'hr': {},
  2808. 'ul': {},
  2809. 'ol': {
  2810. 'start': '{int}',
  2811. 'type': 'a|A|i|I|1',
  2812. },
  2813. 'li': {
  2814. 'value': '{int}',
  2815. },
  2816. 'p': {},
  2817. 'pre': {},
  2818. 'table': {},
  2819. 'thead': {},
  2820. 'tbody': {},
  2821. 'tfoot': {},
  2822. 'tr': {},
  2823. 'td': {},
  2824. 'th': {},
  2825. 'a': {
  2826. 'href': '{url}',
  2827. 'target': '*',
  2828. },
  2829. 'abbr': {},
  2830. 'b': {},
  2831. 'br': {},
  2832. 'cite': {},
  2833. 'code': {},
  2834. 'data': {
  2835. 'value': '*',
  2836. },
  2837. 'dfn': {},
  2838. 'em': {},
  2839. 'i': {},
  2840. 'kbd': {},
  2841. 'mark': {},
  2842. 'q': {
  2843. 'cite': '{url}',
  2844. },
  2845. 's': {},
  2846. 'samp': {},
  2847. 'small': {},
  2848. 'span': {},
  2849. 'strong': {},
  2850. 'sub': {},
  2851. 'sup': {},
  2852. 'time': {
  2853. 'datetime': '*',
  2854. },
  2855. 'u': {},
  2856. 'var': {},
  2857. 'wbr': {},
  2858. 'img': {
  2859. 'alt': '*',
  2860. 'href': '{url}',
  2861. },
  2862. 'figure': {},
  2863. 'figcaption': {},
  2864. 'del': {},
  2865. 'ins': {},
  2866. 'details': {},
  2867. 'summary': {},
  2868. };
  2869. /**
  2870. * Mapping of allowable lowercase global attributes to their permitted
  2871. * values. Uses same value pattern syntax as described in `allowableTags`.
  2872. * @type {object}
  2873. */
  2874. allowableGlobalAttributes = {
  2875. 'class': '{classlist}',
  2876. 'data-*': '*',
  2877. 'dir': 'ltr|rtl|auto',
  2878. 'id': '*',
  2879. 'lang': '*',
  2880. 'style': '{style}',
  2881. 'title': '*',
  2882. 'translate': 'yes|no|{none}',
  2883. };
  2884. /**
  2885. * Mapping of allowable CSS style names to their allowable value patterns.
  2886. * @type {object}
  2887. */
  2888. allowableStyleKeys = {
  2889. 'background-color': '{color}',
  2890. 'color': '{color}',
  2891. };
  2892. /**
  2893. * Scrubs all forbidden attributes from an HTML tag.
  2894. *
  2895. * @param {MDHTMLTag} tag - HTML tag
  2896. */
  2897. scrubTag(tag) {
  2898. for (const name of Object.keys(tag.attributes)) {
  2899. if (!this.isValidAttributeName(tag.tagName, name)) {
  2900. delete tag.attributes[name];
  2901. }
  2902. if (!this.isValidAttributeValue(tag.tagName, name, tag.attributes[name])) {
  2903. delete tag.attributes[name];
  2904. }
  2905. }
  2906. }
  2907. /**
  2908. * Scrubs all forbidden attributes from an HTML modifier.
  2909. *
  2910. * @param {MDTagModifier} modifier
  2911. * @param {string|null} tagName - HTML tag name, if known, otherwise only
  2912. * global attributes will be permitted
  2913. */
  2914. scrubModifier(modifier, tagName) {
  2915. if (modifier.cssClasses.length > 0) {
  2916. const classList = modifier.cssClasses.join(' ');
  2917. if (!this.isValidAttributeValue(tagName, 'class', classList)) {
  2918. modifier.cssClasses = [];
  2919. }
  2920. }
  2921. if (modifier.cssId !== null) {
  2922. if (!this.isValidAttributeValue(tagName, 'id', modifier.cssId)) {
  2923. modifier.cssId = null;
  2924. }
  2925. }
  2926. if (!this.isValidAttributeName(tagName, 'style')) {
  2927. modifier.cssStyles = {};
  2928. } else {
  2929. for (const key of Object.keys(modifier.cssStyles)) {
  2930. const val = modifier.cssStyles[key];
  2931. if (!this.isValidStyleValue(key, val)) {
  2932. delete modifier.cssStyles[key];
  2933. }
  2934. }
  2935. }
  2936. for (const key of Object.keys(modifier.attributes)) {
  2937. const val = modifier.attributes[key];
  2938. if (!this.isValidAttributeValue(tagName, key, val)) {
  2939. delete modifier.attributes[key];
  2940. }
  2941. }
  2942. }
  2943. /**
  2944. * Tests if an HTML tag name is permitted.
  2945. *
  2946. * @param {string} tagName
  2947. * @returns {boolean}
  2948. */
  2949. isValidTagName(tagName) {
  2950. return this.allowableTags[tagName.toLowerCase()] !== undefined;
  2951. }
  2952. /**
  2953. * Tests if an HTML attribute name is permitted.
  2954. *
  2955. * @param {string|null} tagName - HTML tag name or null to only check global
  2956. * attributes
  2957. * @param {string} attributeName - attribute name
  2958. * @returns {boolean}
  2959. */
  2960. isValidAttributeName(tagName, attributeName) {
  2961. const lcAttributeName = attributeName.toLowerCase();
  2962. if (this.allowableGlobalAttributes[lcAttributeName] !== undefined) {
  2963. return true;
  2964. }
  2965. for (const pattern in this.allowableGlobalAttributes) {
  2966. if (pattern.endsWith('*') && lcAttributeName.startsWith(pattern.substring(0, pattern.length - 1))) {
  2967. return true;
  2968. }
  2969. }
  2970. if (tagName === null) return false;
  2971. const lcTagName = tagName.toLowerCase();
  2972. const tagAttributes = this.allowableTags[lcTagName];
  2973. if (tagAttributes) {
  2974. return tagAttributes[lcAttributeName] !== undefined;
  2975. }
  2976. return false;
  2977. }
  2978. /**
  2979. * Tests if an attribute value is allowable.
  2980. *
  2981. * @param {string|null} tagName
  2982. * @param {string} attributeName
  2983. * @param {string} attributeValue
  2984. * @returns {boolean}
  2985. */
  2986. isValidAttributeValue(tagName, attributeName, attributeValue) {
  2987. const lcAttributeName = attributeName.toLowerCase();
  2988. const globalPattern = this.allowableGlobalAttributes[attributeName.toLowerCase()];
  2989. if (globalPattern !== undefined) {
  2990. return this.#attributeValueMatchesPattern(attributeValue, globalPattern);
  2991. }
  2992. for (const namePattern in this.allowableGlobalAttributes) {
  2993. if (namePattern.endsWith('*') && lcAttributeName.startsWith(namePattern.substring(0, namePattern.length - 1))) {
  2994. return this.#attributeValueMatchesPattern(attributeValue, this.allowableGlobalAttributes[namePattern]);
  2995. }
  2996. }
  2997. if (tagName === null) return false;
  2998. const lcTagName = tagName.toLowerCase();
  2999. const tagAttributes = this.allowableTags[lcTagName];
  3000. if (tagAttributes === undefined) return false;
  3001. const valuePattern = tagAttributes[lcAttributeName];
  3002. if (valuePattern === undefined) return false;
  3003. return this.#attributeValueMatchesPattern(attributeValue, valuePattern);
  3004. }
  3005. static #permissiveURLRegex = /^\S+$/;
  3006. static #integerRegex = /^[\-]?\d+$/;
  3007. static #classListRegex = /^-?[_a-zA-Z]+[_a-zA-Z0-9-]*(?:\s+-?[_a-zA-Z]+[_a-zA-Z0-9-]*)*$/;
  3008. /**
  3009. * @param {string} value
  3010. * @param {string} pattern
  3011. * @returns {boolean}
  3012. */
  3013. #attributeValueMatchesPattern(value, pattern) {
  3014. const options = pattern.split('|');
  3015. for (const option of options) {
  3016. switch (option) {
  3017. case '*':
  3018. return true;
  3019. case '{classlist}':
  3020. if (MDHTMLFilter.#classListRegex.exec(value)) return true;
  3021. break;
  3022. case '{int}':
  3023. if (MDHTMLFilter.#integerRegex.exec(value)) return true;
  3024. break;
  3025. case '{none}':
  3026. if (value === true) return true;
  3027. break;
  3028. case '{style}':
  3029. if (this.isValidStyleDeclaration(value)) return true;
  3030. break;
  3031. case '{url}':
  3032. if (MDHTMLFilter.#permissiveURLRegex.exec(value)) return true;
  3033. break;
  3034. default:
  3035. if (value === option) return true;
  3036. break;
  3037. }
  3038. }
  3039. return false;
  3040. }
  3041. /**
  3042. * Tests if a string of one or more style `key: value;` declarations is
  3043. * fully allowable.
  3044. *
  3045. * @param {string} styles
  3046. * @returns {boolean}
  3047. */
  3048. isValidStyleDeclaration(styles) {
  3049. const settings = styles.split(';');
  3050. for (const setting of settings) {
  3051. if (setting.trim().length == 0) continue;
  3052. const parts = setting.split(':');
  3053. if (parts.length != 2) return false;
  3054. const name = parts[0].trim();
  3055. if (!this.isValidStyleKey(name)) return false;
  3056. const value = parts[1].trim();
  3057. if (!this.isValidStyleValue(name, value)) return false;
  3058. }
  3059. return true;
  3060. }
  3061. /**
  3062. * Tests if a CSS style key is allowable.
  3063. *
  3064. * @param {string} key - CSS key
  3065. * @returns {boolean}
  3066. */
  3067. isValidStyleKey(key) {
  3068. return this.allowableStyleKeys[key] !== undefined;
  3069. }
  3070. /**
  3071. * Tests if a CSS style value is allowable.
  3072. *
  3073. * @param {string} key
  3074. * @param {string} value
  3075. * @returns {boolean}
  3076. */
  3077. isValidStyleValue(key, value) {
  3078. const pattern = this.allowableStyleKeys[key];
  3079. if (pattern === undefined) return false;
  3080. const options = pattern.split('|');
  3081. for (const option of options) {
  3082. switch (option) {
  3083. case '{color}':
  3084. if (this.#isValidCSSColor(value)) return true;
  3085. default:
  3086. if (value === option) return true;
  3087. }
  3088. }
  3089. return false;
  3090. }
  3091. static #styleColorRegex = /^#[0-9a-f]{3}(?:[0-9a-f]{3})?$|^[a-zA-Z]+$/i;
  3092. #isValidCSSColor(value) {
  3093. return MDHTMLFilter.#styleColorRegex.exec(value) !== null;
  3094. }
  3095. }
  3096. class MDHTMLTag {
  3097. /** @type {string} */
  3098. original;
  3099. /** @type {string} */
  3100. tagName;
  3101. /** @type {boolean} */
  3102. isCloser;
  3103. /** @type {object} */
  3104. attributes;
  3105. /**
  3106. * @param {string} original
  3107. * @param {string} tagName
  3108. * @param {boolean} isCloser
  3109. * @param {object} attributes
  3110. */
  3111. constructor(original, tagName, isCloser, attributes) {
  3112. this.original = original;
  3113. this.tagName = tagName;
  3114. this.isCloser = isCloser;
  3115. this.attributes = attributes;
  3116. }
  3117. toString() {
  3118. var html = '<';
  3119. if (this.isCloser) html += '/';
  3120. html += this.tagName;
  3121. for (const key in this.attributes) {
  3122. const safeName = MDUtils.scrubAttributeName(key);
  3123. const value = this.attributes[key];
  3124. if (value === true) {
  3125. html += ` ${safeName}`;
  3126. } else {
  3127. const escapedValue = MDUtils.escapeHTML(value);
  3128. html += ` ${safeName}="${escapedValue}"`;
  3129. }
  3130. }
  3131. html += '>';
  3132. return html;
  3133. }
  3134. equals(other) {
  3135. if (!(other instanceof MDHTMLTag)) return false;
  3136. if (other.tagName != this.tagName) return false;
  3137. if (other.isCloser != this.isCloser) return false;
  3138. return MDUtils.equal(other.attributes, this.attributes);
  3139. }
  3140. static #htmlTagNameFirstRegex = /[a-z]/i;
  3141. static #htmlTagNameMedialRegex = /[a-z0-9]/i;
  3142. static #htmlAttributeNameFirstRegex = /[a-z]/i;
  3143. static #htmlAttributeNameMedialRegex = /[a-z0-9-]/i;
  3144. static #whitespaceCharRegex = /\s/;
  3145. /**
  3146. * @param {string} line
  3147. * @returns {MDHTMLTag|null} HTML tag if possible
  3148. */
  3149. static fromLineStart(line) {
  3150. let expectOpenBracket = 0;
  3151. let expectCloserOrName = 1;
  3152. let expectName = 2;
  3153. let expectAttributeNameOrEnd = 3;
  3154. let expectEqualsOrAttributeOrEnd = 4;
  3155. let expectAttributeValue = 5;
  3156. let expectCloseBracket = 6;
  3157. var isCloser = false;
  3158. var tagName = '';
  3159. var attributeName = '';
  3160. var attributeValue = '';
  3161. var attributeQuote = null;
  3162. var attributes = {};
  3163. var fullTag = null;
  3164. let endAttribute = function(unescape=false) {
  3165. if (attributeName.length > 0) {
  3166. if (attributeValue.length > 0 || attributeQuote) {
  3167. attributes[attributeName] = unescape ? MDUtils.unescapeHTML(attributeValue) : attributeValue;
  3168. } else {
  3169. attributes[attributeName] = true;
  3170. }
  3171. }
  3172. attributeName = '';
  3173. attributeValue = '';
  3174. attributeQuote = null;
  3175. };
  3176. var expect = expectOpenBracket;
  3177. for (var p = 0; p < line.length && fullTag === null; p++) {
  3178. let ch = line.substring(p, p + 1);
  3179. let isWhitespace = this.#whitespaceCharRegex.exec(ch) !== null;
  3180. switch (expect) {
  3181. case expectOpenBracket:
  3182. if (ch != '<') return null;
  3183. expect = expectCloserOrName;
  3184. break;
  3185. case expectCloserOrName:
  3186. if (ch == '/') {
  3187. isCloser = true;
  3188. } else {
  3189. p--;
  3190. }
  3191. expect = expectName;
  3192. break;
  3193. case expectName:
  3194. if (tagName.length == 0) {
  3195. if (this.#htmlTagNameFirstRegex.exec(ch) === null) return null;
  3196. tagName += ch;
  3197. } else {
  3198. if (this.#htmlTagNameMedialRegex.exec(ch)) {
  3199. tagName += ch;
  3200. } else {
  3201. p--;
  3202. expect = (isCloser) ? expectCloseBracket : expectAttributeNameOrEnd;
  3203. }
  3204. }
  3205. break;
  3206. case expectAttributeNameOrEnd:
  3207. if (attributeName.length == 0) {
  3208. if (isWhitespace) {
  3209. // skip whitespace
  3210. } else if (ch == '/') {
  3211. expect = expectCloseBracket;
  3212. } else if (ch == '>') {
  3213. fullTag = line.substring(0, p + 1);
  3214. break;
  3215. } else if (this.#htmlAttributeNameFirstRegex.exec(ch)) {
  3216. attributeName += ch;
  3217. } else {
  3218. return null;
  3219. }
  3220. } else if (isWhitespace) {
  3221. expect = expectEqualsOrAttributeOrEnd;
  3222. } else if (ch == '/') {
  3223. endAttribute();
  3224. expect = expectCloseBracket;
  3225. } else if (ch == '>') {
  3226. endAttribute();
  3227. fullTag = line.substring(0, p + 1);
  3228. break;
  3229. } else if (ch == '=') {
  3230. expect = expectAttributeValue;
  3231. } else if (this.#htmlAttributeNameMedialRegex.exec(ch)) {
  3232. attributeName += ch;
  3233. } else {
  3234. return null;
  3235. }
  3236. break;
  3237. case expectEqualsOrAttributeOrEnd:
  3238. if (ch == '=') {
  3239. expect = expectAttributeValue;
  3240. } else if (isWhitespace) {
  3241. // skip whitespace
  3242. } else if (ch == '/') {
  3243. expect = expectCloseBracket;
  3244. } else if (ch == '>') {
  3245. fullTag = line.substring(0, p + 1);
  3246. break;
  3247. } else if (this.#htmlAttributeNameFirstRegex.exec(ch)) {
  3248. endAttribute();
  3249. expect = expectAttributeNameOrEnd;
  3250. p--;
  3251. }
  3252. break;
  3253. case expectAttributeValue:
  3254. if (attributeValue.length == 0) {
  3255. if (attributeQuote === null) {
  3256. if (isWhitespace) {
  3257. // skip whitespace
  3258. } else if (ch == '"' || ch == "'") {
  3259. attributeQuote = ch;
  3260. } else {
  3261. attributeQuote = ''; // explicitly unquoted
  3262. p--;
  3263. }
  3264. } else {
  3265. if (ch === attributeQuote) {
  3266. // Empty string
  3267. endAttribute(attributeQuote != '');
  3268. expect = expectAttributeNameOrEnd;
  3269. } else if (attributeQuote === '' && (ch == '/' || ch == '>')) {
  3270. return null;
  3271. } else {
  3272. attributeValue += ch;
  3273. }
  3274. }
  3275. } else {
  3276. if (ch === attributeQuote) {
  3277. endAttribute();
  3278. expect = expectAttributeNameOrEnd;
  3279. } else if (attributeQuote === '' && isWhitespace) {
  3280. endAttribute();
  3281. expect = expectAttributeNameOrEnd;
  3282. } else {
  3283. attributeValue += ch;
  3284. }
  3285. }
  3286. break;
  3287. case expectCloseBracket:
  3288. if (isWhitespace) {
  3289. // ignore whitespace
  3290. } else if (ch == '>') {
  3291. fullTag = line.substring(0, p + 1);
  3292. break;
  3293. }
  3294. break;
  3295. }
  3296. }
  3297. if (fullTag === null) return null;
  3298. endAttribute();
  3299. return new MDHTMLTag(fullTag, tagName, isCloser, attributes);
  3300. }
  3301. }
  3302. class MDTagModifier {
  3303. /** @type {string} */
  3304. original;
  3305. /** @type {string[]} */
  3306. cssClasses = [];
  3307. /** @type {string|null} */
  3308. cssId = null;
  3309. /** @type {object} */
  3310. cssStyles = {};
  3311. /** @type {object} */
  3312. attributes = {};
  3313. static #baseClassRegex = /\.([a-z_\-][a-z0-9_\-]*?)/i;
  3314. static #baseIdRegex = /#([a-z_\-][a-z0-9_\-]*?)/i;
  3315. static #baseAttributeRegex = /([a-z0-9]+?)=([^\s\}]+?)/i;
  3316. static #baseRegex = /\{([^}]+?)}/i;
  3317. static #leadingClassRegex = new RegExp('^' + this.#baseRegex.source, 'i');
  3318. static #trailingClassRegex = new RegExp('^(.*?)\\s*' + this.#baseRegex.source + '\\s*$', 'i');
  3319. static #classRegex = new RegExp('^' + this.#baseClassRegex.source + '$', 'i'); // 1=classname
  3320. static #idRegex = new RegExp('^' + this.#baseIdRegex.source + '$', 'i'); // 1=id
  3321. static #attributeRegex = new RegExp('^' + this.#baseAttributeRegex.source + '$', 'i'); // 1=attribute name, 2=attribute value
  3322. /**
  3323. * @param {MDNode} node
  3324. */
  3325. applyTo(node) {
  3326. if (node instanceof MDNode) {
  3327. node.cssClasses = node.cssClasses.concat(this.cssClasses);
  3328. if (this.cssId) node.cssId = this.cssId;
  3329. for (const name in this.attributes) {
  3330. node.attributes[name] = this.attributes[name];
  3331. }
  3332. for (const name in this.cssStyles) {
  3333. node.cssStyles[name] = this.cssStyles[name];
  3334. }
  3335. }
  3336. }
  3337. equals(other) {
  3338. if (!(other instanceof MDTagModifier)) return false;
  3339. if (!MDUtils.equal(other.cssClasses, this.cssClasses)) return false;
  3340. if (other.cssId !== this.cssId) return false;
  3341. if (!MDUtils.equal(other.attributes, this.attributes)) return false;
  3342. return true;
  3343. }
  3344. toString() {
  3345. return this.original;
  3346. }
  3347. static #styleToObject(styleValue) {
  3348. const pairs = styleValue.split(';');
  3349. var styles = {};
  3350. for (const pair of pairs) {
  3351. const keyAndValue = pair.split(':');
  3352. if (keyAndValue.length != 2) continue;
  3353. styles[keyAndValue[0]] = keyAndValue[1];
  3354. }
  3355. return styles;
  3356. }
  3357. static #fromContents(contents) {
  3358. let modifierTokens = contents.split(/\s+/);
  3359. let mod = new MDTagModifier();
  3360. mod.original = `{${contents}}`;
  3361. var groups;
  3362. for (const token of modifierTokens) {
  3363. if (token.trim() == '') continue;
  3364. if (groups = this.#classRegex.exec(token)) {
  3365. mod.cssClasses.push(groups[1]);
  3366. } else if (groups = this.#idRegex.exec(token)) {
  3367. mod.cssId = groups[1];
  3368. } else if (groups = this.#attributeRegex.exec(token)) {
  3369. if (groups[1] == 'style') {
  3370. mod.cssStyles = this.#styleToObject(groups[2]);
  3371. } else {
  3372. mod.attributes[groups[1]] = groups[2];
  3373. }
  3374. } else {
  3375. return null;
  3376. }
  3377. }
  3378. return mod;
  3379. }
  3380. /**
  3381. * Extracts modifier from line.
  3382. * @param {string} line
  3383. * @returns {Array} Tuple with remaining line and MDTagModifier.
  3384. */
  3385. static fromLine(line) {
  3386. let groups = this.#trailingClassRegex.exec(line);
  3387. if (groups === null) return [ line, null ];
  3388. let bareLine = groups[1];
  3389. let mod = this.#fromContents(groups[2]);
  3390. return [ bareLine, mod ];
  3391. }
  3392. /**
  3393. * Extracts modifier from head of string.
  3394. * @param {string} line
  3395. * @returns {MDTagModifier}
  3396. */
  3397. static fromStart(line) {
  3398. let groups = this.#leadingClassRegex.exec(line);
  3399. if (groups === null) return null;
  3400. return this.#fromContents(groups[1]);
  3401. }
  3402. /**
  3403. * @param {string} line
  3404. * @returns {string}
  3405. */
  3406. static strip(line) {
  3407. let groups = this.#trailingClassRegex.exec(line);
  3408. if (groups === null) return line;
  3409. return groups[1];
  3410. }
  3411. }
  3412. class MDConfig {
  3413. strikethroughSingleTildeEnabled = true;
  3414. }
  3415. class Markdown {
  3416. /**
  3417. * Set of standard readers.
  3418. * @type {MDReader[]}
  3419. */
  3420. static standardReaders = [
  3421. new MDUnderlinedHeadingReader(),
  3422. new MDHashHeadingReader(),
  3423. new MDBlockQuoteReader(),
  3424. new MDHorizontalRuleReader(),
  3425. new MDUnorderedListReader(),
  3426. new MDOrderedListReader(),
  3427. new MDFencedCodeBlockReader(),
  3428. new MDIndentedCodeBlockReader(),
  3429. new MDParagraphReader(),
  3430. new MDStrongReader(),
  3431. new MDEmphasisReader(),
  3432. new MDCodeSpanReader(),
  3433. new MDImageReader(),
  3434. new MDLinkReader(),
  3435. new MDHTMLTagReader(),
  3436. ];
  3437. /**
  3438. * All supported readers.
  3439. * @type {MDReader[]}
  3440. */
  3441. static allReaders = [
  3442. ...this.standardReaders,
  3443. new MDSubtextReader(),
  3444. new MDTableReader(),
  3445. new MDDefinitionListReader(),
  3446. new MDFootnoteReader(),
  3447. new MDAbbreviationReader(),
  3448. new MDUnderlineReader(),
  3449. new MDSubscriptReader(),
  3450. new MDStrikethroughReader(),
  3451. new MDHighlightReader(),
  3452. new MDSuperscriptReader(),
  3453. new MDReferencedImageReader(),
  3454. new MDReferencedLinkReader(),
  3455. new MDModifierReader(),
  3456. ];
  3457. /**
  3458. * Shared instance of a parser with standard syntax.
  3459. */
  3460. static standardParser = new Markdown(this.standardReaders);
  3461. /**
  3462. * Shared instance of a parser with all supported syntax.
  3463. */
  3464. static completeParser = new Markdown(this.allReaders);
  3465. /**
  3466. * @type {MDConfig}
  3467. */
  3468. config;
  3469. /**
  3470. * Filter for what non-markdown HTML is permitted. HTML generated as a
  3471. * result of markdown is unaffected.
  3472. */
  3473. tagFilter = new MDHTMLFilter();
  3474. #readers;
  3475. /** @type {MDReader[]} */
  3476. #readersByBlockPriority;
  3477. /** @type {MDReader[]} */
  3478. #readersByTokenPriority;
  3479. /** @type {Array} */
  3480. #readersBySubstitutePriority;
  3481. /**
  3482. * Creates a Markdown parser with the given syntax readers.
  3483. *
  3484. * @param {MDReader[]} readers
  3485. * @param {MDConfig} config
  3486. */
  3487. constructor(readers=Markdown.allReaders, config=new MDConfig()) {
  3488. this.#readers = readers;
  3489. this.config = config;
  3490. this.#readersByBlockPriority = MDReader.sortReaderForBlocks(readers);
  3491. this.#readersByTokenPriority = MDReader.sortReadersForTokenizing(readers);
  3492. this.#readersBySubstitutePriority = MDReader.sortReadersForSubstitution(readers);
  3493. }
  3494. /**
  3495. * Converts a markdown string to an HTML string.
  3496. *
  3497. * @param {string} markdown
  3498. * @returns {string} HTML
  3499. */
  3500. toHTML(markdown) {
  3501. const lines = markdown.split(/(?:\n|\r|\r\n)/);
  3502. const state = new MDState(lines,
  3503. this.config,
  3504. this.#readersByBlockPriority,
  3505. this.#readersByTokenPriority,
  3506. this.#readersBySubstitutePriority,
  3507. this.tagFilter);
  3508. for (const reader of this.#readers) {
  3509. reader.preProcess(state);
  3510. }
  3511. const nodes = state.readBlocks();
  3512. for (const reader of this.#readers) {
  3513. reader.postProcess(state, nodes);
  3514. }
  3515. return MDNode.toHTML(nodes, state);
  3516. }
  3517. }