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

markdown.js 95KB

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