statum.js 162 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943
  1. /*!
  2. * Statum - client-side state library.
  3. *
  4. * An HTMX-inspired state layer. The library manages signed "snapshots" of
  5. * application state (one per slot type), keeps them fresh via the server's
  6. * `transmit_after` / `invalid_after` timestamps, and binds them to the DOM
  7. * through `stm-*` attributes. See model.md for the full specification.
  8. *
  9. * Distribution: a single IIFE that attaches `window.statum` (no dependencies,
  10. * no build step). Targets modern evergreen browsers (uses fetch,
  11. * BroadcastChannel, queueMicrotask, URLSearchParams, FormData).
  12. */
  13. (function () {
  14. 'use strict';
  15. /** Content-Type used by every Statum directive response. */
  16. var STATUM_CONTENT_TYPE = 'application/vnd.statum+json';
  17. /** Request/response header carrying one slot's reference (or base64 frame). */
  18. var SLOT_HEADER = 'X-Statum-Slot';
  19. /** Request header carrying an action's opaque `private` blob. */
  20. var PRIVATE_HEADER = 'X-Statum-Private';
  21. /** Session-cookie marker used to detect a fresh browser session. */
  22. var SESSION_COOKIE = '_statum_sdc';
  23. /** BroadcastChannel name used to sync session/device state across tabs. */
  24. var CHANNEL_NAME = 'statum';
  25. /** localStorage / sessionStorage key prefix for persisted frames. */
  26. var STORAGE_PREFIX = 'statum:';
  27. // ---------------------------------------------------------------------------
  28. // Configuration
  29. // ---------------------------------------------------------------------------
  30. /**
  31. * Endpoint URLs. These defaults may be overridden per-page with the
  32. * `stm-entrypoint` and `stm-slots` attributes on the `<body>` element.
  33. *
  34. * @type {{entrypointUrl: string, slotsUrl: string}}
  35. */
  36. var config = {
  37. entrypointUrl: '/_statum/entrypoint',
  38. slotsUrl: '/_statum/slots',
  39. channelUrl: '/_statum/channel',
  40. // The realtime worker is embedded in the library as a default resource
  41. // (served at /_statum/resource/statum-worker.js); override with stm-worker.
  42. workerUrl: '/_statum/resource/statum-worker.js'
  43. };
  44. // ---------------------------------------------------------------------------
  45. // State store
  46. // ---------------------------------------------------------------------------
  47. /**
  48. * In-memory store, keyed by slot type. Each value is `{frame, snapshot}`
  49. * where `snapshot` is the parsed `frame.content`. This map is the single
  50. * source of truth for rendering; persistent backends merely hydrate it.
  51. *
  52. * @type {Map<string, {frame: object, snapshot: object}>}
  53. */
  54. var store = new Map();
  55. /** Listeners, keyed by type name. @type {Map<string, Set<Function>>} */
  56. var listeners = new Map();
  57. /**
  58. * Idempotency tracking: slot key -> last-applied frame `signature`. Held in
  59. * memory only (per tab), so a frame already persisted by another tab still
  60. * re-draws a tab whose tracking lags. Entries are dropped on clear so a
  61. * replayed frame re-applies.
  62. * @type {Map<string, string>}
  63. */
  64. var signatures = new Map();
  65. /** Optional UI handlers (set via `statum.on*Handler`). */
  66. var handlers = { onConfirm: null, onNotify: null, onError: null };
  67. /** @type {BroadcastChannel|null} */
  68. var channel = null;
  69. /** Whether {@link init} has already run. */
  70. var initialized = false;
  71. /** Whether the entrypoint has been fetched (guards without pre-entrypoint
  72. * only evaluate after this). */
  73. var entrypointLoaded = false;
  74. /** Set when a guard has triggered a redirect, so further work is skipped. */
  75. var guardRedirected = false;
  76. // ---------------------------------------------------------------------------
  77. // Small utilities
  78. // ---------------------------------------------------------------------------
  79. /**
  80. * Parse an ISO 8601 timestamp to epoch millis, or `undefined`.
  81. * @param {string|undefined} s
  82. * @returns {number|undefined}
  83. */
  84. function parseTime(s) {
  85. if (!s) return undefined;
  86. var t = new Date(s).getTime();
  87. return isNaN(t) ? undefined : t;
  88. }
  89. /**
  90. * Base64-encode a UTF-8 string (safe for frames containing non-Latin1 data).
  91. * @param {string} str
  92. * @returns {string}
  93. */
  94. function b64encode(str) {
  95. var bytes = new TextEncoder().encode(str);
  96. var bin = '';
  97. var chunk = 0x8000;
  98. for (var i = 0; i < bytes.length; i += chunk) {
  99. bin += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
  100. }
  101. return btoa(bin);
  102. }
  103. /** Read a cookie value, or `null` if absent. @param {string} name */
  104. function getCookie(name) {
  105. var escaped = name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1');
  106. var match = document.cookie.match(new RegExp('(?:^|; )' + escaped + '=([^;]*)'));
  107. return match ? decodeURIComponent(match[1]) : null;
  108. }
  109. /** Set a session cookie (no expiry => cleared when the browser closes). */
  110. function setCookie(name, value) {
  111. document.cookie = name + '=' + encodeURIComponent(value) + '; path=/; SameSite=Lax';
  112. }
  113. // ---------------------------------------------------------------------------
  114. // Expression evaluation
  115. // ---------------------------------------------------------------------------
  116. /**
  117. * Compiled-expression cache. Expressions are evaluated as JavaScript against
  118. * a scope object (the page's HTML is trusted, per the model).
  119. * @type {Map<string, Function>}
  120. */
  121. var exprCache = new Map();
  122. /**
  123. * Wrap a scope for expression evaluation so bare identifiers resolve in a
  124. * controlled order: names on the scope (slot types, loop variables,
  125. * `stm-def-*` locals) win; names missing from the scope but present on
  126. * `globalThis` (`Math`, `parseInt`, `String`, …) forward to that global;
  127. * anything else resolves to `undefined` instead of throwing a
  128. * `ReferenceError` or leaking an unintended global. The `has` trap claims
  129. * every name so `with` never falls through around the proxy, and the inner
  130. * try/catch of the compiled expression still turns property access on a
  131. * resolved `undefined`/`null` into `undefined` for that expression alone.
  132. * @param {object} scope
  133. * @returns {Proxy<object>}
  134. */
  135. function evalScope(scope) {
  136. return new Proxy(scope, {
  137. has: function () { return true; },
  138. get: function (target, name) {
  139. if (name in target) return target[name];
  140. if (typeof name === 'string' && name in globalThis) return globalThis[name];
  141. return undefined;
  142. }
  143. });
  144. }
  145. /**
  146. * Compile (and cache) an expression string into a function of `scope`.
  147. * Uses `with` so that arbitrary type names and loop variables are resolved
  148. * as bare identifiers. Returns a function that yields `undefined` on any
  149. * runtime or compile error.
  150. * @param {string} expr
  151. * @returns {Function}
  152. */
  153. function compileExpr(expr) {
  154. var cached = exprCache.get(expr);
  155. if (cached !== undefined) return cached;
  156. var fn;
  157. try {
  158. // Function-constructor bodies are non-strict by default, so `with` is
  159. // permitted. The inner try/catch turns runtime errors (property access
  160. // on an unset slot's `undefined`) into `undefined` so a missing path
  161. // simply yields no value; missing identifiers already resolve to
  162. // `undefined` via the evalScope proxy.
  163. fn = new Function('scope', 'with(scope){try{return (' + expr + ');}catch(e){return undefined;}}');
  164. } catch (e) {
  165. fn = function () { return undefined; };
  166. }
  167. exprCache.set(expr, fn);
  168. return fn;
  169. }
  170. /**
  171. * Evaluate an expression against a scope.
  172. * @param {string} expr
  173. * @param {object} [scope]
  174. * @returns {*} `undefined` if the expression is empty or fails.
  175. */
  176. function evalExpr(expr, scope) {
  177. if (expr == null || expr === '') return undefined;
  178. try {
  179. return compileExpr(expr)(evalScope(scope || {}));
  180. } catch (e) {
  181. return undefined;
  182. }
  183. }
  184. /**
  185. * Build a child scope that inherits the parent (so type bindings remain
  186. * visible) and adds one own loop variable.
  187. * @param {object} parent
  188. * @param {string} name
  189. * @param {*} value
  190. * @returns {object}
  191. */
  192. function childScope(parent, name, value) {
  193. var scope = Object.create(parent || null);
  194. scope[name] = value;
  195. return scope;
  196. }
  197. // ---------------------------------------------------------------------------
  198. // StatumError
  199. // ---------------------------------------------------------------------------
  200. /**
  201. * Error type thrown for HTTP failures and `error` directives.
  202. * @param {string} [message]
  203. * @param {string} [code]
  204. * @param {object} [data]
  205. */
  206. function StatumError(message, code, data) {
  207. this.name = 'StatumError';
  208. this.message = message || '';
  209. this.code = code || undefined;
  210. this.data = data || undefined;
  211. }
  212. StatumError.prototype = Object.create(Error.prototype);
  213. // ---------------------------------------------------------------------------
  214. // UI handlers (confirm / notify / error)
  215. // ---------------------------------------------------------------------------
  216. /**
  217. * Run the `notify` directive's default behaviour or the custom
  218. * `statum.onNotifyHandler`. The default surfaces the message via `alert()`.
  219. * @param {string} [kind] Application-defined category (freeform).
  220. * @param {string} [message]
  221. */
  222. function triggerNotify(kind, message) {
  223. var fn = handlers.onNotify;
  224. try {
  225. if (fn) fn(kind, message);
  226. else if (message != null) window.alert(message);
  227. } catch (e) { console.error('[statum] notify handler error', e); }
  228. }
  229. /**
  230. * Run the `stm-confirm` gate: the custom `statum.onConfirmHandler` if set,
  231. * otherwise `window.confirm`. Supports async handlers (custom modals).
  232. * @param {string} message
  233. * @param {HTMLElement} element
  234. * @returns {Promise<boolean>} truthy if the action should proceed.
  235. */
  236. async function triggerConfirm(message, element) {
  237. var fn = handlers.onConfirm;
  238. try {
  239. if (fn) return !!(await fn(message, element));
  240. return !!window.confirm(message);
  241. } catch (e) {
  242. console.error('[statum] confirm handler error', e);
  243. return false;
  244. }
  245. }
  246. /**
  247. * Run the custom `statum.onErrorHandler`, if set. Returns whether the default
  248. * behaviour (promise rejection + `stm-on-error` classes) should proceed: the
  249. * handler suppresses the default by returning (or resolving) `false`.
  250. * @param {StatumError} error
  251. * @param {HTMLElement|null} element
  252. * @returns {Promise<boolean>}
  253. */
  254. async function triggerError(error, element) {
  255. var fn = handlers.onError;
  256. if (!fn) return true;
  257. try {
  258. var suppress = await fn(error, element);
  259. return suppress !== false;
  260. } catch (e) {
  261. console.error('[statum] error handler error', e);
  262. return true;
  263. }
  264. }
  265. /**
  266. * Surface a failure: run `triggerError`, and if the default is not
  267. * suppressed, apply `stm-on-error` classes (when an origin element exists)
  268. * and re-throw so the originating promise rejects.
  269. * @param {StatumError} error
  270. * @param {HTMLElement|null} element
  271. */
  272. async function reportFailure(error, element) {
  273. if (await triggerError(error, element)) {
  274. if (element) applyErrorClasses(element, true);
  275. throw error;
  276. }
  277. }
  278. // ---------------------------------------------------------------------------
  279. // Persistence
  280. // ---------------------------------------------------------------------------
  281. /**
  282. * Pick the storage backend for a scope.
  283. * @param {string} scope
  284. * @returns {Storage|null} `null` for in-memory scopes.
  285. */
  286. function storageFor(scope) {
  287. if (scope === 'device' || scope === 'session') return localStorage;
  288. if (scope === 'flow') return sessionStorage;
  289. return null; // page / transient
  290. }
  291. /**
  292. * Persist a slot's frame to the backend matching its scope, removing any
  293. * stale copy from the other backend first (in case the scope changed).
  294. * @param {string} type
  295. * @param {{frame: object, snapshot: object}} entry
  296. */
  297. function persistEntry(type, entry) {
  298. var scope = entry.snapshot.slot && entry.snapshot.slot.scope;
  299. removePersisted(type);
  300. var storage = storageFor(scope);
  301. if (!storage) return;
  302. try {
  303. storage.setItem(STORAGE_PREFIX + type, JSON.stringify(entry.frame));
  304. } catch (e) {
  305. /* storage full or unavailable; remain in-memory only */
  306. }
  307. }
  308. /** Remove a persisted frame from both backends. @param {string} type */
  309. function removePersisted(type) {
  310. try { localStorage.removeItem(STORAGE_PREFIX + type); } catch (e) {}
  311. try { sessionStorage.removeItem(STORAGE_PREFIX + type); } catch (e) {}
  312. }
  313. /** Load every persisted frame from a backend into the in-memory store. */
  314. function loadFrom(storage) {
  315. for (var i = 0; i < storage.length; i++) {
  316. var key = storage.key(i);
  317. if (!key || key.indexOf(STORAGE_PREFIX) !== 0) continue;
  318. try {
  319. var frame = JSON.parse(storage.getItem(key));
  320. var snapshot = JSON.parse(frame.content);
  321. var type = snapshot.slot && snapshot.slot.type;
  322. if (type && !store.has(type)) store.set(type, { frame: frame, snapshot: snapshot });
  323. } catch (e) {
  324. /* corrupt entry: ignore */
  325. }
  326. }
  327. }
  328. /** Hydrate the store from localStorage and sessionStorage. */
  329. function hydrate() {
  330. try { loadFrom(localStorage); } catch (e) {}
  331. try { loadFrom(sessionStorage); } catch (e) {}
  332. }
  333. // ---------------------------------------------------------------------------
  334. // Session-cookie initialization
  335. // ---------------------------------------------------------------------------
  336. /**
  337. * Detect a fresh browser session using the `_statum_sdc` session cookie.
  338. * On a fresh session, clear all `session`-scoped persisted frames, then set
  339. * the marker. Session cookies are shared across tabs, so a tab opened
  340. * mid-session sees the marker and leaves session data intact.
  341. */
  342. function initSession() {
  343. if (getCookie(SESSION_COOKIE) !== null) return;
  344. clearSessionSlotsFromStorage();
  345. setCookie(SESSION_COOKIE, '1');
  346. }
  347. /** Remove all `session`-scoped frames from localStorage. */
  348. function clearSessionSlotsFromStorage() {
  349. var toRemove = [];
  350. for (var i = 0; i < localStorage.length; i++) {
  351. var key = localStorage.key(i);
  352. if (!key || key.indexOf(STORAGE_PREFIX) !== 0) continue;
  353. try {
  354. var frame = JSON.parse(localStorage.getItem(key));
  355. var snapshot = JSON.parse(frame.content);
  356. if (snapshot.slot && snapshot.slot.scope === 'session') toRemove.push(key);
  357. } catch (e) {}
  358. }
  359. for (var j = 0; j < toRemove.length; j++) localStorage.removeItem(toRemove[j]);
  360. }
  361. // ---------------------------------------------------------------------------
  362. // Listeners & cross-tab broadcast
  363. // ---------------------------------------------------------------------------
  364. /**
  365. * Fire all listeners for a type. (Renamed from `notify` to avoid confusion
  366. * with the `notify` directive.)
  367. * @param {string} type
  368. * @param {object|null} frame
  369. * @param {object|null} snapshot
  370. */
  371. function fireListeners(type, frame, snapshot) {
  372. var set = listeners.get(type);
  373. if (!set) return;
  374. var event = { type: type, frame: frame, snapshot: snapshot };
  375. set.forEach(function (cb) {
  376. try { cb(event); } catch (e) { console.error('[statum] listener error', e); }
  377. });
  378. }
  379. /**
  380. * Broadcast a state change to other tabs. Only `session`/`device` scopes are
  381. * shared (flow/page/transient are per-tab).
  382. * @param {object} message
  383. * @param {string} [scope]
  384. */
  385. function broadcast(message, scope) {
  386. if (!channel) return;
  387. if (scope !== 'session' && scope !== 'device') return;
  388. try { channel.postMessage(message); } catch (e) {}
  389. }
  390. /**
  391. * Broadcast a message to other tabs regardless of slot scope. Used to relay
  392. * realtime (live) events so they reach every tab, not just session/device
  393. * ones.
  394. */
  395. function broadcastAlways(message) {
  396. if (!channel) return;
  397. try { channel.postMessage(message); } catch (e) {}
  398. }
  399. /** Open the BroadcastChannel and wire inbound messages. */
  400. function setupChannel() {
  401. if (typeof BroadcastChannel === 'undefined') return;
  402. try {
  403. channel = new BroadcastChannel(CHANNEL_NAME);
  404. } catch (e) {
  405. channel = null;
  406. return;
  407. }
  408. channel.onmessage = function (ev) {
  409. var msg = ev.data;
  410. if (!msg) return;
  411. if (msg.kind === 'set' && msg.frame) {
  412. try {
  413. var snapshot = JSON.parse(msg.frame.content);
  414. var type = snapshot.slot && snapshot.slot.type;
  415. if (type && commitSet(msg.frame, snapshot, type, false)) renderAll();
  416. } catch (e) { /* ignore malformed */ }
  417. } else if (msg.kind === 'clear') {
  418. var t = msg.type;
  419. if (!t && msg.key != null) {
  420. store.forEach(function (entry, type) {
  421. if (entry.snapshot.slot && entry.snapshot.slot.key === msg.key) t = type;
  422. });
  423. }
  424. if (t) clearType(t, { broadcast: false });
  425. else if (msg.key != null) signatures.delete(msg.key);
  426. renderAll();
  427. }
  428. };
  429. }
  430. // ---------------------------------------------------------------------------
  431. // State mutation
  432. // ---------------------------------------------------------------------------
  433. /**
  434. * Remove a type from the store, un-persist it, drop its idempotency entry,
  435. * fire listeners, and (optionally) broadcast the clear to other tabs.
  436. * @param {string} type
  437. * @param {{broadcast?: boolean}} [opts]
  438. */
  439. function clearType(type, opts) {
  440. var entry = store.get(type);
  441. if (!entry) return;
  442. var scope = entry.snapshot.slot && entry.snapshot.slot.scope;
  443. var key = entry.snapshot.slot && entry.snapshot.slot.key;
  444. store.delete(type);
  445. removePersisted(type);
  446. if (key) signatures.delete(key);
  447. fireListeners(type, null, null);
  448. if (opts && opts.broadcast) broadcast({ kind: 'clear', key: key, type: type }, scope);
  449. }
  450. /**
  451. * Resolve a slot key to its type and clear it. Used by the `clear` directive
  452. * (keyed) and by live `clear` events.
  453. * @param {string} key
  454. * @param {boolean} [broadcast]
  455. */
  456. function clearByKey(key, broadcast) {
  457. var typeToRemove = null;
  458. store.forEach(function (entry, type) {
  459. if (entry.snapshot.slot && entry.snapshot.slot.key === key) typeToRemove = type;
  460. });
  461. if (typeToRemove) clearType(typeToRemove, { broadcast: !!broadcast });
  462. else if (key) signatures.delete(key);
  463. }
  464. /**
  465. * Resolve a frame signature to its slot type and clear it. Used by the
  466. * `invalidate` directive, which references a signature (not a key) because the
  467. * server reports a bad signature without trusting the frame's content.
  468. * @param {string} signature
  469. * @param {boolean} [broadcast]
  470. */
  471. function clearBySignature(signature, broadcast) {
  472. var typeToRemove = null;
  473. store.forEach(function (entry, type) {
  474. if (entry.frame && entry.frame.signature === signature) typeToRemove = type;
  475. });
  476. if (typeToRemove) clearType(typeToRemove, { broadcast: !!broadcast });
  477. else if (signature) signatures.delete(signature);
  478. }
  479. /**
  480. * Commit a `set` to the in-memory store, persist it, and fire listeners.
  481. * Idempotent: if the frame's `signature` matches the last-applied signature
  482. * for this slot (tracked in {@link signatures}), nothing happens and `false`
  483. * is returned. When `doBroadcast` is true the set is forwarded to other tabs
  484. * via the scope-gated broadcast (session/device only).
  485. * @param {object} frame
  486. * @param {object} snapshot
  487. * @param {string} type
  488. * @param {boolean} doBroadcast
  489. * @returns {boolean} `true` if state changed.
  490. */
  491. function commitSet(frame, snapshot, type, doBroadcast) {
  492. var key = snapshot.slot && snapshot.slot.key;
  493. var sig = frame.signature;
  494. if (key && sig !== undefined && signatures.get(key) === sig) return false;
  495. var entry = { frame: frame, snapshot: snapshot };
  496. store.set(type, entry);
  497. persistEntry(type, entry);
  498. fireListeners(type, frame, snapshot);
  499. if (key && sig !== undefined) signatures.set(key, sig);
  500. if (doBroadcast) {
  501. var scope = snapshot.slot && snapshot.slot.scope;
  502. broadcast({ kind: 'set', frame: frame }, scope);
  503. }
  504. return true;
  505. }
  506. /**
  507. * Apply a `set` directive: store/refresh a slot's snapshot (idempotently),
  508. * broadcasting to other tabs.
  509. * @param {object} directive `{ snapshot_frame }`
  510. */
  511. function applySet(directive) {
  512. var frame = directive.snapshot_frame;
  513. if (!frame || !frame.content) return;
  514. var snapshot;
  515. try { snapshot = JSON.parse(frame.content); } catch (e) { return; }
  516. var type = snapshot.slot && snapshot.slot.type;
  517. if (!type) return;
  518. commitSet(frame, snapshot, type, true);
  519. }
  520. /**
  521. * Apply a `clear` directive. The directive carries a slot key, so the type
  522. * holding that key is resolved and removed.
  523. * @param {object} directive `{ key }`
  524. */
  525. function applyClear(directive) {
  526. if (directive.key == null) return;
  527. clearByKey(directive.key, true);
  528. }
  529. /**
  530. * Apply a live `set` event received over the realtime channel. The frame is
  531. * committed (idempotently) and, when new, broadcast to sibling tabs via the
  532. * scope-agnostic {@link broadcastAlways} so every tab sees it regardless of
  533. * which (or how many) tabs hold a subscription.
  534. * @param {object} frame
  535. */
  536. function applyRemoteSet(frame) {
  537. try {
  538. var snapshot = JSON.parse(frame.content);
  539. var type = snapshot.slot && snapshot.slot.type;
  540. if (!type) return;
  541. if (commitSet(frame, snapshot, type, false)) {
  542. broadcastAlways({ kind: 'set', frame: frame });
  543. renderAll();
  544. }
  545. } catch (e) { /* malformed frame */ }
  546. }
  547. /** Apply a live `clear` event received over the realtime channel. */
  548. function applyRemoteClear(key) {
  549. if (key == null) return;
  550. signatures.delete(key);
  551. clearByKey(key, false);
  552. broadcastAlways({ kind: 'clear', key: key });
  553. renderAll();
  554. }
  555. // ---------------------------------------------------------------------------
  556. // Snapshot freshness & request headers
  557. // ---------------------------------------------------------------------------
  558. /**
  559. * Categorize a held snapshot relative to the current time.
  560. * @returns {'silent'|'always'|'invalid'|'stale'|'fresh'}
  561. * - `silent`: unidirectional (`transmit_after` undefined) - never sent.
  562. * - `always`: `transmit_after === as_at` - send the base64 frame each time.
  563. * - `invalid`: past `invalid_after` - drop and refetch.
  564. * - `stale`: past `transmit_after` but not yet invalid - pre-flight first.
  565. * - `fresh`: still cached server-side - send the `as_at` reference.
  566. */
  567. function entryCategory(entry) {
  568. var snap = entry.snapshot;
  569. var transmit = snap.transmit_after;
  570. if (transmit === undefined || transmit === null) return 'silent';
  571. if (transmit === snap.as_at) return 'always';
  572. var now = Date.now();
  573. var invalidAt = parseTime(snap.invalid_after);
  574. var transmitAt = parseTime(transmit);
  575. if (invalidAt !== undefined && now >= invalidAt) return 'invalid';
  576. if (transmitAt !== undefined && now >= transmitAt) return 'stale';
  577. return 'fresh';
  578. }
  579. /**
  580. * Drop locally-invalid snapshots and pre-flight any stale ones. Pre-flighting
  581. * posts the stale frames to the slot endpoint and applies the returned `set`
  582. * directives, refreshing their `as_at` values before the real request runs.
  583. *
  584. * Single-flight: concurrent callers share one in-flight preparation.
  585. * @returns {Promise<void>}
  586. */
  587. var preparePromise = null;
  588. function prepareSlots() {
  589. if (preparePromise) return preparePromise;
  590. preparePromise = doPrepareSlots().then(function () {
  591. preparePromise = null;
  592. }, function () {
  593. preparePromise = null;
  594. });
  595. return preparePromise;
  596. }
  597. async function doPrepareSlots() {
  598. var stale = [];
  599. store.forEach(function (entry, type) {
  600. var cat = entryCategory(entry);
  601. if (cat === 'invalid') {
  602. clearType(type, { broadcast: false });
  603. } else if (cat === 'stale') {
  604. stale.push(entry.frame);
  605. }
  606. });
  607. if (stale.length) {
  608. var directives = await postSlots(stale);
  609. var pfResult = applyDirectives(directives, { originElement: null });
  610. executeTerminal(pfResult);
  611. }
  612. }
  613. /**
  614. * Build the `X-Statum-Slot` header values for every held slot, using the
  615. * freshness rules. Assumes {@link prepareSlots} has just run.
  616. * @returns {string[]}
  617. */
  618. function buildSlotHeaders() {
  619. var headers = [];
  620. store.forEach(function (entry) {
  621. var snap = entry.snapshot;
  622. var cat = entryCategory(entry);
  623. if (cat === 'silent' || cat === 'invalid') return;
  624. if (cat === 'always') {
  625. headers.push(b64encode(JSON.stringify(entry.frame)));
  626. } else {
  627. headers.push('key=' + snap.slot.key + '; as_at=' + snap.as_at);
  628. }
  629. });
  630. return headers;
  631. }
  632. /**
  633. * Collect the current frames held for the given slot keys, in the order the
  634. * keys are first supplied. Used by the `transmit` directive (which forces a
  635. * slot's frame to be posted as if its `transmit_after` had elapsed). Keys the
  636. * client does not currently hold are skipped.
  637. * @param {string[]} keys
  638. * @returns {object[]}
  639. */
  640. function collectFramesForKeys(keys) {
  641. var keySet = Object.create(null);
  642. for (var i = 0; i < keys.length; i++) keySet[keys[i]] = true;
  643. var frames = [];
  644. store.forEach(function (entry) {
  645. var key = entry.snapshot.slot && entry.snapshot.slot.key;
  646. if (key && keySet[key]) {
  647. frames.push(entry.frame);
  648. delete keySet[key];
  649. }
  650. });
  651. return frames;
  652. }
  653. // ---------------------------------------------------------------------------
  654. // HTTP helpers
  655. // ---------------------------------------------------------------------------
  656. /** True if a response is a Statum directive response. */
  657. function isStatum(res) {
  658. return (res.headers.get('Content-Type') || '').indexOf(STATUM_CONTENT_TYPE) !== -1;
  659. }
  660. /**
  661. * POST an array of frames to the slot endpoint (the pre-flight target) and
  662. * return its directive array. This request intentionally carries no slot
  663. * headers (it is itself the refresh).
  664. * @param {object[]} frames
  665. * @returns {Promise<object[]>}
  666. */
  667. async function postSlots(frames) {
  668. var headers = new Headers();
  669. headers.set('Accept', STATUM_CONTENT_TYPE);
  670. headers.set('Content-Type', 'application/json');
  671. var res = await fetch(config.slotsUrl, {
  672. method: 'POST',
  673. headers: headers,
  674. body: JSON.stringify(frames),
  675. credentials: 'same-origin'
  676. });
  677. if (!isStatum(res)) return [];
  678. try { return await res.json(); } catch (e) { return []; }
  679. }
  680. /**
  681. * Apply the non-terminal directives from a response (set/clear/subscribe/
  682. * unsubscribe/notify/transmit-collection), then `render`.
  683. *
  684. * Terminal directives (`navigate`, `post`), error directives, and any
  685. * transmit/retry intent are collected on the returned result rather than
  686. * executed here: {@link handleResponse} performs the asynchronous
  687. * transmission (and optional retry) and then runs terminal navigation, so
  688. * that a `transmit` with `retry` can re-issue the request before the page is
  689. * navigated away.
  690. * @param {object[]} directives
  691. * @param {{originElement?: HTMLElement}} [ctx]
  692. * @returns {{errors: StatumError[], transmitKeys: string[], retry: boolean, navigate: object|null, post: object|null}}
  693. */
  694. function applyDirectives(directives, ctx) {
  695. var result = { errors: [], transmitKeys: [], retry: false, navigate: null, post: null, formSubmits: [], formResets: [] };
  696. var list = Array.isArray(directives) ? directives : [];
  697. for (var i = 0; i < list.length; i++) {
  698. var d = list[i];
  699. if (!d || typeof d !== 'object') continue;
  700. switch (d.type) {
  701. case 'set': applySet(d); break;
  702. case 'clear': applyClear(d); break;
  703. case 'invalidate':
  704. if (d.signature != null) {
  705. console.warn('[statum] server could not verify slot frame ' + d.signature + '; clearing it');
  706. clearBySignature(d.signature, true);
  707. }
  708. break;
  709. case 'subscribe': channelSubscribe(d.key); break;
  710. case 'unsubscribe': channelUnsubscribe(d.key); break;
  711. case 'notify': triggerNotify(d.kind, d.message); break;
  712. case 'transmit':
  713. if (d.key != null) {
  714. if (result.transmitKeys.indexOf(d.key) === -1) result.transmitKeys.push(d.key);
  715. if (d.retry) result.retry = true;
  716. }
  717. break;
  718. case 'error':
  719. {
  720. var err = new StatumError(d.message, d.code, d.data);
  721. var detail = d.code ? (d.message + ' [' + d.code + ']') : d.message;
  722. console.error('[statum] error directive: ' + (detail || '(no message)'));
  723. result.errors.push(err);
  724. }
  725. break;
  726. case 'navigate': result.navigate = d; break;
  727. case 'post': result.post = d; break;
  728. case 'form-submit': result.formSubmits.push(d); break;
  729. case 'form-reset': result.formResets.push(d); break;
  730. }
  731. }
  732. renderAll();
  733. return result;
  734. }
  735. /** Execute the terminal directives collected by {@link applyDirectives}. */
  736. function executeTerminal(result) {
  737. result.formResets.forEach(doFormReset);
  738. result.formSubmits.forEach(doFormSubmit);
  739. if (result.navigate) doNavigate(result.navigate);
  740. if (result.post) doPost(result.post);
  741. }
  742. /** Execute a `form-submit` directive: optionally override action/method, then submit. */
  743. function doFormSubmit(d) {
  744. var form = document.getElementById(d.form);
  745. if (form) {
  746. if (d.action) form.setAttribute('action', d.action);
  747. if (d.method) form.setAttribute('method', d.method);
  748. form.submit();
  749. }
  750. }
  751. /** Execute a `form-reset` directive. */
  752. function doFormReset(d) {
  753. var form = document.getElementById(d.form);
  754. if (form) form.reset();
  755. }
  756. /** Execute a `navigate` directive as a full page load. */
  757. function doNavigate(d) {
  758. if (d.uri) window.location.href = d.uri;
  759. }
  760. /** Execute a `post` directive as a real (navigating) form POST. */
  761. function doPost(d) {
  762. if (!d.uri) return;
  763. var form = document.createElement('form');
  764. form.method = 'POST';
  765. form.action = d.uri;
  766. form.style.display = 'none';
  767. var data = d.data || {};
  768. Object.keys(data).forEach(function (name) {
  769. var input = document.createElement('input');
  770. input.type = 'hidden';
  771. input.name = name;
  772. input.value = data[name] == null ? '' : String(data[name]);
  773. form.appendChild(input);
  774. });
  775. document.body.appendChild(form);
  776. form.submit();
  777. }
  778. /**
  779. * Handle a fetch response: if it is a Statum response, apply its directives;
  780. * otherwise throw on HTTP errors and no-op on other successful responses.
  781. *
  782. * Directive ordering: non-terminal directives are applied and the page
  783. * rendered; then any `transmit` directives force their slot frames to be
  784. * posted (applying the directives the slot endpoint returns); then, if a
  785. * transmit requested a retry, the originating request (`ctx.retry`) is
  786. * re-issued and this response's terminal directives and errors are skipped.
  787. * Otherwise terminal navigation runs, then error directives are surfaced.
  788. * @param {Response} res
  789. * @param {{originElement?: HTMLElement, retry?: Function}} [ctx]
  790. */
  791. async function handleResponse(res, ctx) {
  792. var el = ctx && ctx.originElement;
  793. if (!isStatum(res)) {
  794. if (!res.ok) await reportFailure(new StatumError('HTTP ' + res.status), el);
  795. return;
  796. }
  797. var directives;
  798. try { directives = await res.json(); } catch (e) { directives = []; }
  799. var result = applyDirectives(directives, ctx);
  800. if (result.transmitKeys && result.transmitKeys.length) {
  801. var frames = collectFramesForKeys(result.transmitKeys);
  802. if (frames.length) {
  803. var txDirectives = await postSlots(frames);
  804. applyDirectives(txDirectives, { originElement: el });
  805. }
  806. }
  807. if (result.retry && ctx && typeof ctx.retry === 'function') {
  808. await ctx.retry();
  809. return;
  810. }
  811. executeTerminal(result);
  812. var errors = result.errors || [];
  813. for (var i = 0; i < errors.length; i++) {
  814. await reportFailure(errors[i], el);
  815. }
  816. }
  817. // ---------------------------------------------------------------------------
  818. // Entrypoint & action requests
  819. // ---------------------------------------------------------------------------
  820. /**
  821. * Fetch the entrypoint for the current URL, applying its directives. Carries
  822. * slot headers (and pre-flights) like any other request. The request is
  823. * wrapped in an `attempt` closure passed as the response `retry` callback, so
  824. * a `transmit` directive with `retry` can re-issue it after transmission.
  825. * @returns {Promise<void>}
  826. */
  827. function loadEntrypoint() {
  828. if (guardRedirected) return Promise.resolve();
  829. // From this point on guards are "post-entrypoint" (all of them evaluate).
  830. entrypointLoaded = true;
  831. var attempt = async function () {
  832. await prepareSlots();
  833. var headers = new Headers();
  834. headers.set('Accept', STATUM_CONTENT_TYPE);
  835. buildSlotHeaders().forEach(function (v) { headers.append(SLOT_HEADER, v); });
  836. var url = config.entrypointUrl + '?uri=' + encodeURIComponent(window.location.href);
  837. var res = await fetch(url, { headers: headers, credentials: 'same-origin' });
  838. await handleResponse(res, { originElement: null, retry: attempt });
  839. };
  840. return attempt();
  841. }
  842. /**
  843. * Perform an action request.
  844. *
  845. * The request is wrapped in an `attempt` closure passed as the response
  846. * `retry` callback, so a `transmit` directive with `retry` can re-issue the
  847. * same action after the transmission (rebuilding headers from current state).
  848. *
  849. * @param {object} action `{ uri, method, private }`
  850. * @param {object} [data] Key/value parameters. Encoded as query string for
  851. * GET/HEAD, otherwise as `application/x-www-form-urlencoded`.
  852. * @param {HTMLElement} [originElement] Element that triggered the action, so
  853. * that `stm-on-error` classes can be applied on an `error` directive.
  854. * @returns {Promise<void>} Resolves once the request completes and its
  855. * directives have run; rejects on HTTP failure or an `error` directive.
  856. */
  857. function performAction(action, data, originElement) {
  858. var attempt = async function () {
  859. await prepareSlots();
  860. var headers = new Headers();
  861. headers.set('Accept', STATUM_CONTENT_TYPE);
  862. buildSlotHeaders().forEach(function (v) { headers.append(SLOT_HEADER, v); });
  863. if (action.private) headers.set(PRIVATE_HEADER, action.private);
  864. var method = (action.method || 'GET').toUpperCase();
  865. var params = data || {};
  866. var init = { method: method, headers: headers, credentials: 'same-origin' };
  867. var url = action.uri;
  868. if (method === 'GET' || method === 'HEAD') {
  869. var qs = new URLSearchParams(params).toString();
  870. if (qs) url += (url.indexOf('?') !== -1 ? '&' : '?') + qs;
  871. } else {
  872. init.body = new URLSearchParams(params).toString();
  873. headers.set('Content-Type', 'application/x-www-form-urlencoded');
  874. }
  875. var res;
  876. try {
  877. res = await fetch(url, init);
  878. } catch (networkErr) {
  879. await reportFailure(new StatumError('Network error'), originElement);
  880. return;
  881. }
  882. await handleResponse(res, { originElement: originElement, retry: attempt });
  883. };
  884. return attempt();
  885. }
  886. // ---------------------------------------------------------------------------
  887. // Realtime channel (subscribe/unsubscribe directives)
  888. // ---------------------------------------------------------------------------
  889. /**
  890. * Per-tab fallback engine, used when `SharedWorker` is unavailable. Owns an
  891. * `EventSource`, reads the channel id from the first `event: channel`,
  892. * reference-counts slot keys within this tab, PATCHes subscription changes,
  893. * applies incoming set/clear events (broadcasting them to siblings), and
  894. * closes the stream once it has no subscriptions.
  895. */
  896. function createFallbackEngine() {
  897. var es = null;
  898. var channelId = null;
  899. var refs = new Map(); // slotKey -> count
  900. var pending = { subscribe: [], unsubscribe: [] };
  901. var patchScheduled = false;
  902. function openStream() {
  903. if (es) return;
  904. try { es = new EventSource(config.channelUrl); }
  905. catch (e) { es = null; return; }
  906. es.addEventListener('channel', function (ev) { channelId = ev.data; flushPatch(); });
  907. es.addEventListener('set', function (ev) {
  908. try { applyRemoteSet(JSON.parse(ev.data)); } catch (e) {}
  909. });
  910. es.addEventListener('clear', function (ev) { applyRemoteClear(ev.data); });
  911. es.onerror = function () { /* EventSource auto-reconnects */ };
  912. }
  913. function closeStream() {
  914. if (es) { try { es.close(); } catch (e) {} es = null; }
  915. channelId = null;
  916. }
  917. function schedulePatch() {
  918. if (patchScheduled) return;
  919. patchScheduled = true;
  920. Promise.resolve().then(function () { patchScheduled = false; flushPatch(); });
  921. }
  922. function flushPatch() {
  923. if (!channelId) return;
  924. var sub = pending.subscribe, uns = pending.unsubscribe;
  925. if (!sub.length && !uns.length) return;
  926. pending = { subscribe: [], unsubscribe: [] };
  927. var body = {};
  928. if (sub.length) body.subscribe = sub;
  929. if (uns.length) body.unsubscribe = uns;
  930. try {
  931. fetch(config.channelUrl + '/' + encodeURIComponent(channelId), {
  932. method: 'PATCH',
  933. headers: { 'Content-Type': 'application/json' },
  934. credentials: 'same-origin',
  935. body: JSON.stringify(body)
  936. }).catch(function () {});
  937. } catch (e) {}
  938. }
  939. return {
  940. subscribe: function (key) {
  941. var c = refs.get(key) || 0;
  942. refs.set(key, c + 1);
  943. if (c === 0) { openStream(); pending.subscribe.push(key); schedulePatch(); }
  944. },
  945. unsubscribe: function (key) {
  946. var c = refs.get(key) || 0;
  947. if (c <= 0) return;
  948. if (c - 1 <= 0) {
  949. refs.delete(key);
  950. pending.unsubscribe.push(key);
  951. schedulePatch();
  952. if (refs.size === 0) setTimeout(function () { if (refs.size === 0) closeStream(); }, 0);
  953. } else {
  954. refs.set(key, c - 1);
  955. }
  956. }
  957. };
  958. }
  959. /**
  960. * Channel manager. Prefers a `SharedWorker` (which owns the single SSE
  961. * connection, reference-counts subscriptions across tabs, and relays events
  962. * through the cross-tab `BroadcastChannel`); falls back to a per-tab
  963. * `EventSource` engine when `SharedWorker` is unavailable or the worker
  964. * script fails to load. Tracks the true per-slot subscription count so it can
  965. * replay active subscriptions into the fallback on a mid-session switch.
  966. */
  967. function createChannelManager() {
  968. var port = null;
  969. var workerObj = null;
  970. var fallback = null;
  971. var mode = null; // 'worker' | 'fallback' | null
  972. var counts = new Map(); // slotKey -> count (true subscription state)
  973. function switchToFallback() {
  974. if (mode === 'fallback') return;
  975. mode = 'fallback';
  976. port = null;
  977. workerObj = null;
  978. fallback = createFallbackEngine();
  979. counts.forEach(function (c, key) {
  980. for (var i = 0; i < c; i++) fallback.subscribe(key);
  981. });
  982. }
  983. function ensure() {
  984. if (mode !== null) return;
  985. if (typeof SharedWorker !== 'undefined') {
  986. try {
  987. workerObj = new SharedWorker(config.workerUrl);
  988. port = workerObj.port;
  989. port.start();
  990. port.postMessage({ op: 'init', url: config.channelUrl });
  991. // The worker delivers events via the BroadcastChannel, so nothing is
  992. // required on this port beyond command messages.
  993. port.onmessage = function () {};
  994. workerObj.onerror = function () {
  995. console.warn('[statum] realtime worker failed; falling back to per-tab channel');
  996. switchToFallback();
  997. };
  998. window.addEventListener('pagehide', function () {
  999. if (mode === 'worker' && port) {
  1000. try { port.postMessage({ op: 'disconnect' }); } catch (e) {}
  1001. }
  1002. });
  1003. mode = 'worker';
  1004. return;
  1005. } catch (e) { workerObj = null; port = null; }
  1006. }
  1007. switchToFallback();
  1008. }
  1009. return {
  1010. subscribe: function (key) {
  1011. if (key == null) return;
  1012. ensure();
  1013. counts.set(key, (counts.get(key) || 0) + 1);
  1014. if (mode === 'worker') { try { port.postMessage({ op: 'subscribe', key: key }); } catch (e) {} }
  1015. else fallback.subscribe(key);
  1016. },
  1017. unsubscribe: function (key) {
  1018. if (key == null) return;
  1019. var c = (counts.get(key) || 0) - 1;
  1020. if (c <= 0) counts.delete(key); else counts.set(key, c);
  1021. ensure();
  1022. if (mode === 'worker') { try { port.postMessage({ op: 'unsubscribe', key: key }); } catch (e) {} }
  1023. else fallback.unsubscribe(key);
  1024. }
  1025. };
  1026. }
  1027. var channelManager = createChannelManager();
  1028. function channelSubscribe(key) { channelManager.subscribe(key); }
  1029. function channelUnsubscribe(key) { channelManager.unsubscribe(key); }
  1030. // ---------------------------------------------------------------------------
  1031. // Rendering: loops, conditionals, bindings
  1032. // ---------------------------------------------------------------------------
  1033. /** Templates captured from loop holders. @type {WeakMap<Element, Element>} */
  1034. var templates = new WeakMap();
  1035. /** Instance nodes managed by a loop holder. @type {WeakMap<Element, Element[]>} */
  1036. var instancesByHolder = new WeakMap();
  1037. /** Loop instance nodes; skipped by the render walk (owned by their holder). */
  1038. var instanceNodes = new WeakSet();
  1039. /** Elements whose `stm-action` has been wired, to avoid double-binding. */
  1040. var wired = new WeakSet();
  1041. /**
  1042. * Find a `stm-for-{name}-in` attribute on an element.
  1043. * @returns {{varName: string, expr: string}|null}
  1044. */
  1045. function getStmFor(el) {
  1046. var attrs = el.attributes;
  1047. for (var i = 0; i < attrs.length; i++) {
  1048. var match = attrs[i].name.match(/^stm-for-(.+)-in$/);
  1049. if (match) return { varName: match[1], expr: attrs[i].value };
  1050. }
  1051. return null;
  1052. }
  1053. /** Remove any `stm-for-*` and `stm-key` attributes from an element. */
  1054. function stripLoopAttrs(el) {
  1055. var toRemove = [];
  1056. for (var i = 0; i < el.attributes.length; i++) {
  1057. var name = el.attributes[i].name;
  1058. if (/^stm-for-(.+)-in$/.test(name) || name === 'stm-key') toRemove.push(name);
  1059. }
  1060. toRemove.forEach(function (n) { el.removeAttribute(n); });
  1061. }
  1062. /**
  1063. * Evaluate an `stm-if` / `stm-else-if` / `stm-else` chain beginning at `el`,
  1064. * showing the matching branch and hiding the rest.
  1065. *
  1066. * Branches are toggled with the `hidden` attribute rather than detached, so
  1067. * every branch stays in the DOM and is re-evaluated on each render. (Detaching
  1068. * the `stm-if` element would remove it from the render walk, leaving an
  1069. * already-attached `stm-else` branch skipped and never refreshed.)
  1070. * @returns {Element|null} The active branch element (or `null`).
  1071. */
  1072. function handleIfChain(el, scope) {
  1073. var branches = [{ el: el, expr: el.getAttribute('stm-if') }];
  1074. var cur = el.nextElementSibling;
  1075. while (cur) {
  1076. if (cur.hasAttribute('stm-else-if')) {
  1077. branches.push({ el: cur, expr: cur.getAttribute('stm-else-if') });
  1078. cur = cur.nextElementSibling;
  1079. } else if (cur.hasAttribute('stm-else')) {
  1080. branches.push({ el: cur, expr: null });
  1081. break;
  1082. } else {
  1083. break;
  1084. }
  1085. }
  1086. var activeEl = null;
  1087. for (var i = 0; i < branches.length; i++) {
  1088. var b = branches[i];
  1089. if (b.expr === null || !!evalExpr(b.expr, scope)) { activeEl = b.el; break; }
  1090. }
  1091. branches.forEach(function (b) {
  1092. if (b.el === activeEl) b.el.removeAttribute('hidden');
  1093. else b.el.setAttribute('hidden', '');
  1094. });
  1095. return activeEl;
  1096. }
  1097. /** Toggle `stm-class.{name}` classes from their predicate expressions. */
  1098. function handleClass(el, scope) {
  1099. var attrs = el.attributes;
  1100. for (var i = 0; i < attrs.length; i++) {
  1101. var attr = attrs[i];
  1102. if (attr.name.indexOf('stm-class.') === 0) {
  1103. var cls = attr.name.slice('stm-class.'.length);
  1104. if (cls && !!evalExpr(attr.value, scope)) el.classList.add(cls);
  1105. else el.classList.remove(cls);
  1106. }
  1107. }
  1108. }
  1109. /**
  1110. * Bind `stm-attribute.{name}`: a boolean result toggles the attribute, any
  1111. * other defined value sets it, and `undefined` removes it.
  1112. *
  1113. * For booleans, prefer the element's IDL property when one exists (e.g.
  1114. * `checked`, `disabled`, `hidden`) — setting the content attribute is
  1115. * unreliable for these once the control is "dirty".
  1116. */
  1117. function handleAttribute(el, scope) {
  1118. var attrs = el.attributes;
  1119. for (var i = 0; i < attrs.length; i++) {
  1120. var attr = attrs[i];
  1121. if (attr.name.indexOf('stm-attribute.') === 0) {
  1122. var name = attr.name.slice('stm-attribute.'.length);
  1123. if (!name) continue;
  1124. var value = evalExpr(attr.value, scope);
  1125. if (value === undefined) {
  1126. el.removeAttribute(name);
  1127. } else if (typeof value === 'boolean') {
  1128. if (name in el && typeof el[name] === 'boolean') el[name] = value;
  1129. else if (value) el.setAttribute(name, '');
  1130. else el.removeAttribute(name);
  1131. } else {
  1132. el.setAttribute(name, value === null ? '' : String(value));
  1133. }
  1134. }
  1135. }
  1136. }
  1137. /**
  1138. * Apply `stm-disabled`: when the predicate is truthy, disable the element and
  1139. * its descendant form controls; otherwise enable them.
  1140. */
  1141. function handleDisabled(el, scope) {
  1142. if (!el.hasAttribute('stm-disabled')) return;
  1143. setDisabledCascade(el, !!evalExpr(el.getAttribute('stm-disabled'), scope));
  1144. }
  1145. /** Set the `disabled` attribute on an element and its descendant controls. */
  1146. function setDisabledCascade(el, on) {
  1147. var nodes = [el].concat(Array.prototype.slice.call(
  1148. el.querySelectorAll('button,input,select,textarea,fieldset')));
  1149. nodes.forEach(function (n) {
  1150. if (on) n.setAttribute('disabled', ''); else n.removeAttribute('disabled');
  1151. });
  1152. }
  1153. // ---------------------------------------------------------------------------
  1154. // Markdown rendering (CommonMark 0.31.2)
  1155. // ---------------------------------------------------------------------------
  1156. //
  1157. // A two-pass CommonMark engine: block structure first (paragraphs, ATX and
  1158. // setext headings, thematic breaks, indented and fenced code blocks, block
  1159. // quotes with lazy continuation, lists with tight/loose rendering and link
  1160. // reference definitions), then inline parsing inside the leaf blocks
  1161. // (delimiter-run emphasis, links and images, code spans, autolinks,
  1162. // backslash escapes, entity references, hard/soft line breaks).
  1163. //
  1164. // SECURITY: the markdown source is untrusted text. Raw HTML is never passed
  1165. // through — inline HTML and HTML-block-looking constructs render as escaped
  1166. // text, which intentionally diverges from the CommonMark HTML output for
  1167. // those examples. Link and image destinations are scheme-filtered by
  1168. // safeMarkdownUrl() and everything is escaped before it reaches the output.
  1169. function safeMarkdownUrl(url) {
  1170. var u = url.trim();
  1171. var m = u.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
  1172. if (!m) return u;
  1173. var scheme = m[1].toLowerCase();
  1174. if (scheme === 'http' || scheme === 'https' || scheme === 'mailto' || scheme === 'ftp') return u;
  1175. return '#';
  1176. }
  1177. /**
  1178. * Packed HTML5 named-entity table (WHATWG `entities.json`, 2125 entries).
  1179. * Each record is "name cp[,cp...]" on one line; multi-codepoint entities
  1180. * carry comma-separated codepoints. Line breaks are escaped so the whole
  1181. * table stays one string literal. Parsed lazily into a name -> text map the
  1182. * first time an entity reference is looked up.
  1183. */
  1184. var MD_ENTITIES_PACKED = ''
  1185. + "AElig 198\nAMP 38\nAacute 193\nAbreve 258\nAcirc 194\nAcy 1040\nAfr 120068\nAgrave 192\n"
  1186. + "Alpha 913\nAmacr 256\nAnd 10835\nAogon 260\nAopf 120120\nApplyFunction 8289\nAring 197\n"
  1187. + "Ascr 119964\nAssign 8788\nAtilde 195\nAuml 196\nBackslash 8726\nBarv 10983\nBarwed 8966\n"
  1188. + "Bcy 1041\nBecause 8757\nBernoullis 8492\nBeta 914\nBfr 120069\nBopf 120121\nBreve 728\n"
  1189. + "Bscr 8492\nBumpeq 8782\nCHcy 1063\nCOPY 169\nCacute 262\nCap 8914\n"
  1190. + "CapitalDifferentialD 8517\nCayleys 8493\nCcaron 268\nCcedil 199\nCcirc 264\nCconint 8752\n"
  1191. + "Cdot 266\nCedilla 184\nCenterDot 183\nCfr 8493\nChi 935\nCircleDot 8857\n"
  1192. + "CircleMinus 8854\nCirclePlus 8853\nCircleTimes 8855\nClockwiseContourIntegral 8754\n"
  1193. + "CloseCurlyDoubleQuote 8221\nCloseCurlyQuote 8217\nColon 8759\nColone 10868\n"
  1194. + "Congruent 8801\nConint 8751\nContourIntegral 8750\nCopf 8450\nCoproduct 8720\n"
  1195. + "CounterClockwiseContourIntegral 8755\nCross 10799\nCscr 119966\nCup 8915\nCupCap 8781\n"
  1196. + "DD 8517\nDDotrahd 10513\nDJcy 1026\nDScy 1029\nDZcy 1039\nDagger 8225\nDarr 8609\n"
  1197. + "Dashv 10980\nDcaron 270\nDcy 1044\nDel 8711\nDelta 916\nDfr 120071\nDiacriticalAcute 180\n"
  1198. + "DiacriticalDot 729\nDiacriticalDoubleAcute 733\nDiacriticalGrave 96\n"
  1199. + "DiacriticalTilde 732\nDiamond 8900\nDifferentialD 8518\nDopf 120123\nDot 168\n"
  1200. + "DotDot 8412\nDotEqual 8784\nDoubleContourIntegral 8751\nDoubleDot 168\n"
  1201. + "DoubleDownArrow 8659\nDoubleLeftArrow 8656\nDoubleLeftRightArrow 8660\n"
  1202. + "DoubleLeftTee 10980\nDoubleLongLeftArrow 10232\nDoubleLongLeftRightArrow 10234\n"
  1203. + "DoubleLongRightArrow 10233\nDoubleRightArrow 8658\nDoubleRightTee 8872\n"
  1204. + "DoubleUpArrow 8657\nDoubleUpDownArrow 8661\nDoubleVerticalBar 8741\nDownArrow 8595\n"
  1205. + "DownArrowBar 10515\nDownArrowUpArrow 8693\nDownBreve 785\nDownLeftRightVector 10576\n"
  1206. + "DownLeftTeeVector 10590\nDownLeftVector 8637\nDownLeftVectorBar 10582\n"
  1207. + "DownRightTeeVector 10591\nDownRightVector 8641\nDownRightVectorBar 10583\nDownTee 8868\n"
  1208. + "DownTeeArrow 8615\nDownarrow 8659\nDscr 119967\nDstrok 272\nENG 330\nETH 208\nEacute 201\n"
  1209. + "Ecaron 282\nEcirc 202\nEcy 1069\nEdot 278\nEfr 120072\nEgrave 200\nElement 8712\n"
  1210. + "Emacr 274\nEmptySmallSquare 9723\nEmptyVerySmallSquare 9643\nEogon 280\nEopf 120124\n"
  1211. + "Epsilon 917\nEqual 10869\nEqualTilde 8770\nEquilibrium 8652\nEscr 8496\nEsim 10867\n"
  1212. + "Eta 919\nEuml 203\nExists 8707\nExponentialE 8519\nFcy 1060\nFfr 120073\n"
  1213. + "FilledSmallSquare 9724\nFilledVerySmallSquare 9642\nFopf 120125\nForAll 8704\n"
  1214. + "Fouriertrf 8497\nFscr 8497\nGJcy 1027\nGT 62\nGamma 915\nGammad 988\nGbreve 286\n"
  1215. + "Gcedil 290\nGcirc 284\nGcy 1043\nGdot 288\nGfr 120074\nGg 8921\nGopf 120126\n"
  1216. + "GreaterEqual 8805\nGreaterEqualLess 8923\nGreaterFullEqual 8807\nGreaterGreater 10914\n"
  1217. + "GreaterLess 8823\nGreaterSlantEqual 10878\nGreaterTilde 8819\nGscr 119970\nGt 8811\n"
  1218. + "HARDcy 1066\nHacek 711\nHat 94\nHcirc 292\nHfr 8460\nHilbertSpace 8459\nHopf 8461\n"
  1219. + "HorizontalLine 9472\nHscr 8459\nHstrok 294\nHumpDownHump 8782\nHumpEqual 8783\nIEcy 1045\n"
  1220. + "IJlig 306\nIOcy 1025\nIacute 205\nIcirc 206\nIcy 1048\nIdot 304\nIfr 8465\nIgrave 204\n"
  1221. + "Im 8465\nImacr 298\nImaginaryI 8520\nImplies 8658\nInt 8748\nIntegral 8747\n"
  1222. + "Intersection 8898\nInvisibleComma 8291\nInvisibleTimes 8290\nIogon 302\nIopf 120128\n"
  1223. + "Iota 921\nIscr 8464\nItilde 296\nIukcy 1030\nIuml 207\nJcirc 308\nJcy 1049\nJfr 120077\n"
  1224. + "Jopf 120129\nJscr 119973\nJsercy 1032\nJukcy 1028\nKHcy 1061\nKJcy 1036\nKappa 922\n"
  1225. + "Kcedil 310\nKcy 1050\nKfr 120078\nKopf 120130\nKscr 119974\nLJcy 1033\nLT 60\nLacute 313\n"
  1226. + "Lambda 923\nLang 10218\nLaplacetrf 8466\nLarr 8606\nLcaron 317\nLcedil 315\nLcy 1051\n"
  1227. + "LeftAngleBracket 10216\nLeftArrow 8592\nLeftArrowBar 8676\nLeftArrowRightArrow 8646\n"
  1228. + "LeftCeiling 8968\nLeftDoubleBracket 10214\nLeftDownTeeVector 10593\nLeftDownVector 8643\n"
  1229. + "LeftDownVectorBar 10585\nLeftFloor 8970\nLeftRightArrow 8596\nLeftRightVector 10574\n"
  1230. + "LeftTee 8867\nLeftTeeArrow 8612\nLeftTeeVector 10586\nLeftTriangle 8882\n"
  1231. + "LeftTriangleBar 10703\nLeftTriangleEqual 8884\nLeftUpDownVector 10577\n"
  1232. + "LeftUpTeeVector 10592\nLeftUpVector 8639\nLeftUpVectorBar 10584\nLeftVector 8636\n"
  1233. + "LeftVectorBar 10578\nLeftarrow 8656\nLeftrightarrow 8660\nLessEqualGreater 8922\n"
  1234. + "LessFullEqual 8806\nLessGreater 8822\nLessLess 10913\nLessSlantEqual 10877\n"
  1235. + "LessTilde 8818\nLfr 120079\nLl 8920\nLleftarrow 8666\nLmidot 319\nLongLeftArrow 10229\n"
  1236. + "LongLeftRightArrow 10231\nLongRightArrow 10230\nLongleftarrow 10232\n"
  1237. + "Longleftrightarrow 10234\nLongrightarrow 10233\nLopf 120131\nLowerLeftArrow 8601\n"
  1238. + "LowerRightArrow 8600\nLscr 8466\nLsh 8624\nLstrok 321\nLt 8810\nMap 10501\nMcy 1052\n"
  1239. + "MediumSpace 8287\nMellintrf 8499\nMfr 120080\nMinusPlus 8723\nMopf 120132\nMscr 8499\n"
  1240. + "Mu 924\nNJcy 1034\nNacute 323\nNcaron 327\nNcedil 325\nNcy 1053\n"
  1241. + "NegativeMediumSpace 8203\nNegativeThickSpace 8203\nNegativeThinSpace 8203\n"
  1242. + "NegativeVeryThinSpace 8203\nNestedGreaterGreater 8811\nNestedLessLess 8810\nNewLine 10\n"
  1243. + "Nfr 120081\nNoBreak 8288\nNonBreakingSpace 160\nNopf 8469\nNot 10988\nNotCongruent 8802\n"
  1244. + "NotCupCap 8813\nNotDoubleVerticalBar 8742\nNotElement 8713\nNotEqual 8800\n"
  1245. + "NotEqualTilde 8770,824\nNotExists 8708\nNotGreater 8815\nNotGreaterEqual 8817\n"
  1246. + "NotGreaterFullEqual 8807,824\nNotGreaterGreater 8811,824\nNotGreaterLess 8825\n"
  1247. + "NotGreaterSlantEqual 10878,824\nNotGreaterTilde 8821\nNotHumpDownHump 8782,824\n"
  1248. + "NotHumpEqual 8783,824\nNotLeftTriangle 8938\nNotLeftTriangleBar 10703,824\n"
  1249. + "NotLeftTriangleEqual 8940\nNotLess 8814\nNotLessEqual 8816\nNotLessGreater 8824\n"
  1250. + "NotLessLess 8810,824\nNotLessSlantEqual 10877,824\nNotLessTilde 8820\n"
  1251. + "NotNestedGreaterGreater 10914,824\nNotNestedLessLess 10913,824\nNotPrecedes 8832\n"
  1252. + "NotPrecedesEqual 10927,824\nNotPrecedesSlantEqual 8928\nNotReverseElement 8716\n"
  1253. + "NotRightTriangle 8939\nNotRightTriangleBar 10704,824\nNotRightTriangleEqual 8941\n"
  1254. + "NotSquareSubset 8847,824\nNotSquareSubsetEqual 8930\nNotSquareSuperset 8848,824\n"
  1255. + "NotSquareSupersetEqual 8931\nNotSubset 8834,8402\nNotSubsetEqual 8840\nNotSucceeds 8833\n"
  1256. + "NotSucceedsEqual 10928,824\nNotSucceedsSlantEqual 8929\nNotSucceedsTilde 8831,824\n"
  1257. + "NotSuperset 8835,8402\nNotSupersetEqual 8841\nNotTilde 8769\nNotTildeEqual 8772\n"
  1258. + "NotTildeFullEqual 8775\nNotTildeTilde 8777\nNotVerticalBar 8740\nNscr 119977\nNtilde 209\n"
  1259. + "Nu 925\nOElig 338\nOacute 211\nOcirc 212\nOcy 1054\nOdblac 336\nOfr 120082\nOgrave 210\n"
  1260. + "Omacr 332\nOmega 937\nOmicron 927\nOopf 120134\nOpenCurlyDoubleQuote 8220\n"
  1261. + "OpenCurlyQuote 8216\nOr 10836\nOscr 119978\nOslash 216\nOtilde 213\nOtimes 10807\n"
  1262. + "Ouml 214\nOverBar 8254\nOverBrace 9182\nOverBracket 9140\nOverParenthesis 9180\n"
  1263. + "PartialD 8706\nPcy 1055\nPfr 120083\nPhi 934\nPi 928\nPlusMinus 177\nPoincareplane 8460\n"
  1264. + "Popf 8473\nPr 10939\nPrecedes 8826\nPrecedesEqual 10927\nPrecedesSlantEqual 8828\n"
  1265. + "PrecedesTilde 8830\nPrime 8243\nProduct 8719\nProportion 8759\nProportional 8733\n"
  1266. + "Pscr 119979\nPsi 936\nQUOT 34\nQfr 120084\nQopf 8474\nQscr 119980\nRBarr 10512\nREG 174\n"
  1267. + "Racute 340\nRang 10219\nRarr 8608\nRarrtl 10518\nRcaron 344\nRcedil 342\nRcy 1056\n"
  1268. + "Re 8476\nReverseElement 8715\nReverseEquilibrium 8651\nReverseUpEquilibrium 10607\n"
  1269. + "Rfr 8476\nRho 929\nRightAngleBracket 10217\nRightArrow 8594\nRightArrowBar 8677\n"
  1270. + "RightArrowLeftArrow 8644\nRightCeiling 8969\nRightDoubleBracket 10215\n"
  1271. + "RightDownTeeVector 10589\nRightDownVector 8642\nRightDownVectorBar 10581\n"
  1272. + "RightFloor 8971\nRightTee 8866\nRightTeeArrow 8614\nRightTeeVector 10587\n"
  1273. + "RightTriangle 8883\nRightTriangleBar 10704\nRightTriangleEqual 8885\n"
  1274. + "RightUpDownVector 10575\nRightUpTeeVector 10588\nRightUpVector 8638\n"
  1275. + "RightUpVectorBar 10580\nRightVector 8640\nRightVectorBar 10579\nRightarrow 8658\n"
  1276. + "Ropf 8477\nRoundImplies 10608\nRrightarrow 8667\nRscr 8475\nRsh 8625\nRuleDelayed 10740\n"
  1277. + "SHCHcy 1065\nSHcy 1064\nSOFTcy 1068\nSacute 346\nSc 10940\nScaron 352\nScedil 350\n"
  1278. + "Scirc 348\nScy 1057\nSfr 120086\nShortDownArrow 8595\nShortLeftArrow 8592\n"
  1279. + "ShortRightArrow 8594\nShortUpArrow 8593\nSigma 931\nSmallCircle 8728\nSopf 120138\n"
  1280. + "Sqrt 8730\nSquare 9633\nSquareIntersection 8851\nSquareSubset 8847\n"
  1281. + "SquareSubsetEqual 8849\nSquareSuperset 8848\nSquareSupersetEqual 8850\nSquareUnion 8852\n"
  1282. + "Sscr 119982\nStar 8902\nSub 8912\nSubset 8912\nSubsetEqual 8838\nSucceeds 8827\n"
  1283. + "SucceedsEqual 10928\nSucceedsSlantEqual 8829\nSucceedsTilde 8831\nSuchThat 8715\n"
  1284. + "Sum 8721\nSup 8913\nSuperset 8835\nSupersetEqual 8839\nSupset 8913\nTHORN 222\n"
  1285. + "TRADE 8482\nTSHcy 1035\nTScy 1062\nTab 9\nTau 932\nTcaron 356\nTcedil 354\nTcy 1058\n"
  1286. + "Tfr 120087\nTherefore 8756\nTheta 920\nThickSpace 8287,8202\nThinSpace 8201\nTilde 8764\n"
  1287. + "TildeEqual 8771\nTildeFullEqual 8773\nTildeTilde 8776\nTopf 120139\nTripleDot 8411\n"
  1288. + "Tscr 119983\nTstrok 358\nUacute 218\nUarr 8607\nUarrocir 10569\nUbrcy 1038\nUbreve 364\n"
  1289. + "Ucirc 219\nUcy 1059\nUdblac 368\nUfr 120088\nUgrave 217\nUmacr 362\nUnderBar 95\n"
  1290. + "UnderBrace 9183\nUnderBracket 9141\nUnderParenthesis 9181\nUnion 8899\nUnionPlus 8846\n"
  1291. + "Uogon 370\nUopf 120140\nUpArrow 8593\nUpArrowBar 10514\nUpArrowDownArrow 8645\n"
  1292. + "UpDownArrow 8597\nUpEquilibrium 10606\nUpTee 8869\nUpTeeArrow 8613\nUparrow 8657\n"
  1293. + "Updownarrow 8661\nUpperLeftArrow 8598\nUpperRightArrow 8599\nUpsi 978\nUpsilon 933\n"
  1294. + "Uring 366\nUscr 119984\nUtilde 360\nUuml 220\nVDash 8875\nVbar 10987\nVcy 1042\n"
  1295. + "Vdash 8873\nVdashl 10982\nVee 8897\nVerbar 8214\nVert 8214\nVerticalBar 8739\n"
  1296. + "VerticalLine 124\nVerticalSeparator 10072\nVerticalTilde 8768\nVeryThinSpace 8202\n"
  1297. + "Vfr 120089\nVopf 120141\nVscr 119985\nVvdash 8874\nWcirc 372\nWedge 8896\nWfr 120090\n"
  1298. + "Wopf 120142\nWscr 119986\nXfr 120091\nXi 926\nXopf 120143\nXscr 119987\nYAcy 1071\n"
  1299. + "YIcy 1031\nYUcy 1070\nYacute 221\nYcirc 374\nYcy 1067\nYfr 120092\nYopf 120144\n"
  1300. + "Yscr 119988\nYuml 376\nZHcy 1046\nZacute 377\nZcaron 381\nZcy 1047\nZdot 379\n"
  1301. + "ZeroWidthSpace 8203\nZeta 918\nZfr 8488\nZopf 8484\nZscr 119989\naacute 225\nabreve 259\n"
  1302. + "ac 8766\nacE 8766,819\nacd 8767\nacirc 226\nacute 180\nacy 1072\naelig 230\naf 8289\n"
  1303. + "afr 120094\nagrave 224\nalefsym 8501\naleph 8501\nalpha 945\namacr 257\namalg 10815\n"
  1304. + "amp 38\nand 8743\nandand 10837\nandd 10844\nandslope 10840\nandv 10842\nang 8736\n"
  1305. + "ange 10660\nangle 8736\nangmsd 8737\nangmsdaa 10664\nangmsdab 10665\nangmsdac 10666\n"
  1306. + "angmsdad 10667\nangmsdae 10668\nangmsdaf 10669\nangmsdag 10670\nangmsdah 10671\n"
  1307. + "angrt 8735\nangrtvb 8894\nangrtvbd 10653\nangsph 8738\nangst 197\nangzarr 9084\n"
  1308. + "aogon 261\naopf 120146\nap 8776\napE 10864\napacir 10863\nape 8778\napid 8779\napos 39\n"
  1309. + "approx 8776\napproxeq 8778\naring 229\nascr 119990\nast 42\nasymp 8776\nasympeq 8781\n"
  1310. + "atilde 227\nauml 228\nawconint 8755\nawint 10769\nbNot 10989\nbackcong 8780\n"
  1311. + "backepsilon 1014\nbackprime 8245\nbacksim 8765\nbacksimeq 8909\nbarvee 8893\nbarwed 8965\n"
  1312. + "barwedge 8965\nbbrk 9141\nbbrktbrk 9142\nbcong 8780\nbcy 1073\nbdquo 8222\nbecaus 8757\n"
  1313. + "because 8757\nbemptyv 10672\nbepsi 1014\nbernou 8492\nbeta 946\nbeth 8502\nbetween 8812\n"
  1314. + "bfr 120095\nbigcap 8898\nbigcirc 9711\nbigcup 8899\nbigodot 10752\nbigoplus 10753\n"
  1315. + "bigotimes 10754\nbigsqcup 10758\nbigstar 9733\nbigtriangledown 9661\nbigtriangleup 9651\n"
  1316. + "biguplus 10756\nbigvee 8897\nbigwedge 8896\nbkarow 10509\nblacklozenge 10731\n"
  1317. + "blacksquare 9642\nblacktriangle 9652\nblacktriangledown 9662\nblacktriangleleft 9666\n"
  1318. + "blacktriangleright 9656\nblank 9251\nblk12 9618\nblk14 9617\nblk34 9619\nblock 9608\n"
  1319. + "bne 61,8421\nbnequiv 8801,8421\nbnot 8976\nbopf 120147\nbot 8869\nbottom 8869\n"
  1320. + "bowtie 8904\nboxDL 9559\nboxDR 9556\nboxDl 9558\nboxDr 9555\nboxH 9552\nboxHD 9574\n"
  1321. + "boxHU 9577\nboxHd 9572\nboxHu 9575\nboxUL 9565\nboxUR 9562\nboxUl 9564\nboxUr 9561\n"
  1322. + "boxV 9553\nboxVH 9580\nboxVL 9571\nboxVR 9568\nboxVh 9579\nboxVl 9570\nboxVr 9567\n"
  1323. + "boxbox 10697\nboxdL 9557\nboxdR 9554\nboxdl 9488\nboxdr 9484\nboxh 9472\nboxhD 9573\n"
  1324. + "boxhU 9576\nboxhd 9516\nboxhu 9524\nboxminus 8863\nboxplus 8862\nboxtimes 8864\n"
  1325. + "boxuL 9563\nboxuR 9560\nboxul 9496\nboxur 9492\nboxv 9474\nboxvH 9578\nboxvL 9569\n"
  1326. + "boxvR 9566\nboxvh 9532\nboxvl 9508\nboxvr 9500\nbprime 8245\nbreve 728\nbrvbar 166\n"
  1327. + "bscr 119991\nbsemi 8271\nbsim 8765\nbsime 8909\nbsol 92\nbsolb 10693\nbsolhsub 10184\n"
  1328. + "bull 8226\nbullet 8226\nbump 8782\nbumpE 10926\nbumpe 8783\nbumpeq 8783\ncacute 263\n"
  1329. + "cap 8745\ncapand 10820\ncapbrcup 10825\ncapcap 10827\ncapcup 10823\ncapdot 10816\n"
  1330. + "caps 8745,65024\ncaret 8257\ncaron 711\nccaps 10829\nccaron 269\nccedil 231\nccirc 265\n"
  1331. + "ccups 10828\nccupssm 10832\ncdot 267\ncedil 184\ncemptyv 10674\ncent 162\ncenterdot 183\n"
  1332. + "cfr 120096\nchcy 1095\ncheck 10003\ncheckmark 10003\nchi 967\ncir 9675\ncirE 10691\n"
  1333. + "circ 710\ncirceq 8791\ncirclearrowleft 8634\ncirclearrowright 8635\ncircledR 174\n"
  1334. + "circledS 9416\ncircledast 8859\ncircledcirc 8858\ncircleddash 8861\ncire 8791\n"
  1335. + "cirfnint 10768\ncirmid 10991\ncirscir 10690\nclubs 9827\nclubsuit 9827\ncolon 58\n"
  1336. + "colone 8788\ncoloneq 8788\ncomma 44\ncommat 64\ncomp 8705\ncompfn 8728\ncomplement 8705\n"
  1337. + "complexes 8450\ncong 8773\ncongdot 10861\nconint 8750\ncopf 120148\ncoprod 8720\n"
  1338. + "copy 169\ncopysr 8471\ncrarr 8629\ncross 10007\ncscr 119992\ncsub 10959\ncsube 10961\n"
  1339. + "csup 10960\ncsupe 10962\nctdot 8943\ncudarrl 10552\ncudarrr 10549\ncuepr 8926\n"
  1340. + "cuesc 8927\ncularr 8630\ncularrp 10557\ncup 8746\ncupbrcap 10824\ncupcap 10822\n"
  1341. + "cupcup 10826\ncupdot 8845\ncupor 10821\ncups 8746,65024\ncurarr 8631\ncurarrm 10556\n"
  1342. + "curlyeqprec 8926\ncurlyeqsucc 8927\ncurlyvee 8910\ncurlywedge 8911\ncurren 164\n"
  1343. + "curvearrowleft 8630\ncurvearrowright 8631\ncuvee 8910\ncuwed 8911\ncwconint 8754\n"
  1344. + "cwint 8753\ncylcty 9005\ndArr 8659\ndHar 10597\ndagger 8224\ndaleth 8504\ndarr 8595\n"
  1345. + "dash 8208\ndashv 8867\ndbkarow 10511\ndblac 733\ndcaron 271\ndcy 1076\ndd 8518\n"
  1346. + "ddagger 8225\nddarr 8650\nddotseq 10871\ndeg 176\ndelta 948\ndemptyv 10673\ndfisht 10623\n"
  1347. + "dfr 120097\ndharl 8643\ndharr 8642\ndiam 8900\ndiamond 8900\ndiamondsuit 9830\n"
  1348. + "diams 9830\ndie 168\ndigamma 989\ndisin 8946\ndiv 247\ndivide 247\ndivideontimes 8903\n"
  1349. + "divonx 8903\ndjcy 1106\ndlcorn 8990\ndlcrop 8973\ndollar 36\ndopf 120149\ndot 729\n"
  1350. + "doteq 8784\ndoteqdot 8785\ndotminus 8760\ndotplus 8724\ndotsquare 8865\n"
  1351. + "doublebarwedge 8966\ndownarrow 8595\ndowndownarrows 8650\ndownharpoonleft 8643\n"
  1352. + "downharpoonright 8642\ndrbkarow 10512\ndrcorn 8991\ndrcrop 8972\ndscr 119993\ndscy 1109\n"
  1353. + "dsol 10742\ndstrok 273\ndtdot 8945\ndtri 9663\ndtrif 9662\nduarr 8693\nduhar 10607\n"
  1354. + "dwangle 10662\ndzcy 1119\ndzigrarr 10239\neDDot 10871\neDot 8785\neacute 233\n"
  1355. + "easter 10862\necaron 283\necir 8790\necirc 234\necolon 8789\necy 1101\nedot 279\nee 8519\n"
  1356. + "efDot 8786\nefr 120098\neg 10906\negrave 232\negs 10902\negsdot 10904\nel 10905\n"
  1357. + "elinters 9191\nell 8467\nels 10901\nelsdot 10903\nemacr 275\nempty 8709\nemptyset 8709\n"
  1358. + "emptyv 8709\nemsp 8195\nemsp13 8196\nemsp14 8197\neng 331\nensp 8194\neogon 281\n"
  1359. + "eopf 120150\nepar 8917\neparsl 10723\neplus 10865\nepsi 949\nepsilon 949\nepsiv 1013\n"
  1360. + "eqcirc 8790\neqcolon 8789\neqsim 8770\neqslantgtr 10902\neqslantless 10901\nequals 61\n"
  1361. + "equest 8799\nequiv 8801\nequivDD 10872\neqvparsl 10725\nerDot 8787\nerarr 10609\n"
  1362. + "escr 8495\nesdot 8784\nesim 8770\neta 951\neth 240\neuml 235\neuro 8364\nexcl 33\n"
  1363. + "exist 8707\nexpectation 8496\nexponentiale 8519\nfallingdotseq 8786\nfcy 1092\n"
  1364. + "female 9792\nffilig 64259\nfflig 64256\nffllig 64260\nffr 120099\nfilig 64257\n"
  1365. + "fjlig 102,106\nflat 9837\nfllig 64258\nfltns 9649\nfnof 402\nfopf 120151\nforall 8704\n"
  1366. + "fork 8916\nforkv 10969\nfpartint 10765\nfrac12 189\nfrac13 8531\nfrac14 188\nfrac15 8533\n"
  1367. + "frac16 8537\nfrac18 8539\nfrac23 8532\nfrac25 8534\nfrac34 190\nfrac35 8535\nfrac38 8540\n"
  1368. + "frac45 8536\nfrac56 8538\nfrac58 8541\nfrac78 8542\nfrasl 8260\nfrown 8994\nfscr 119995\n"
  1369. + "gE 8807\ngEl 10892\ngacute 501\ngamma 947\ngammad 989\ngap 10886\ngbreve 287\ngcirc 285\n"
  1370. + "gcy 1075\ngdot 289\nge 8805\ngel 8923\ngeq 8805\ngeqq 8807\ngeqslant 10878\nges 10878\n"
  1371. + "gescc 10921\ngesdot 10880\ngesdoto 10882\ngesdotol 10884\ngesl 8923,65024\ngesles 10900\n"
  1372. + "gfr 120100\ngg 8811\nggg 8921\ngimel 8503\ngjcy 1107\ngl 8823\nglE 10898\ngla 10917\n"
  1373. + "glj 10916\ngnE 8809\ngnap 10890\ngnapprox 10890\ngne 10888\ngneq 10888\ngneqq 8809\n"
  1374. + "gnsim 8935\ngopf 120152\ngrave 96\ngscr 8458\ngsim 8819\ngsime 10894\ngsiml 10896\ngt 62\n"
  1375. + "gtcc 10919\ngtcir 10874\ngtdot 8919\ngtlPar 10645\ngtquest 10876\ngtrapprox 10886\n"
  1376. + "gtrarr 10616\ngtrdot 8919\ngtreqless 8923\ngtreqqless 10892\ngtrless 8823\ngtrsim 8819\n"
  1377. + "gvertneqq 8809,65024\ngvnE 8809,65024\nhArr 8660\nhairsp 8202\nhalf 189\nhamilt 8459\n"
  1378. + "hardcy 1098\nharr 8596\nharrcir 10568\nharrw 8621\nhbar 8463\nhcirc 293\nhearts 9829\n"
  1379. + "heartsuit 9829\nhellip 8230\nhercon 8889\nhfr 120101\nhksearow 10533\nhkswarow 10534\n"
  1380. + "hoarr 8703\nhomtht 8763\nhookleftarrow 8617\nhookrightarrow 8618\nhopf 120153\n"
  1381. + "horbar 8213\nhscr 119997\nhslash 8463\nhstrok 295\nhybull 8259\nhyphen 8208\niacute 237\n"
  1382. + "ic 8291\nicirc 238\nicy 1080\niecy 1077\niexcl 161\niff 8660\nifr 120102\nigrave 236\n"
  1383. + "ii 8520\niiiint 10764\niiint 8749\niinfin 10716\niiota 8489\nijlig 307\nimacr 299\n"
  1384. + "image 8465\nimagline 8464\nimagpart 8465\nimath 305\nimof 8887\nimped 437\nin 8712\n"
  1385. + "incare 8453\ninfin 8734\ninfintie 10717\ninodot 305\nint 8747\nintcal 8890\n"
  1386. + "integers 8484\nintercal 8890\nintlarhk 10775\nintprod 10812\niocy 1105\niogon 303\n"
  1387. + "iopf 120154\niota 953\niprod 10812\niquest 191\niscr 119998\nisin 8712\nisinE 8953\n"
  1388. + "isindot 8949\nisins 8948\nisinsv 8947\nisinv 8712\nit 8290\nitilde 297\niukcy 1110\n"
  1389. + "iuml 239\njcirc 309\njcy 1081\njfr 120103\njmath 567\njopf 120155\njscr 119999\n"
  1390. + "jsercy 1112\njukcy 1108\nkappa 954\nkappav 1008\nkcedil 311\nkcy 1082\nkfr 120104\n"
  1391. + "kgreen 312\nkhcy 1093\nkjcy 1116\nkopf 120156\nkscr 120000\nlAarr 8666\nlArr 8656\n"
  1392. + "lAtail 10523\nlBarr 10510\nlE 8806\nlEg 10891\nlHar 10594\nlacute 314\nlaemptyv 10676\n"
  1393. + "lagran 8466\nlambda 955\nlang 10216\nlangd 10641\nlangle 10216\nlap 10885\nlaquo 171\n"
  1394. + "larr 8592\nlarrb 8676\nlarrbfs 10527\nlarrfs 10525\nlarrhk 8617\nlarrlp 8619\n"
  1395. + "larrpl 10553\nlarrsim 10611\nlarrtl 8610\nlat 10923\nlatail 10521\nlate 10925\n"
  1396. + "lates 10925,65024\nlbarr 10508\nlbbrk 10098\nlbrace 123\nlbrack 91\nlbrke 10635\n"
  1397. + "lbrksld 10639\nlbrkslu 10637\nlcaron 318\nlcedil 316\nlceil 8968\nlcub 123\nlcy 1083\n"
  1398. + "ldca 10550\nldquo 8220\nldquor 8222\nldrdhar 10599\nldrushar 10571\nldsh 8626\nle 8804\n"
  1399. + "leftarrow 8592\nleftarrowtail 8610\nleftharpoondown 8637\nleftharpoonup 8636\n"
  1400. + "leftleftarrows 8647\nleftrightarrow 8596\nleftrightarrows 8646\nleftrightharpoons 8651\n"
  1401. + "leftrightsquigarrow 8621\nleftthreetimes 8907\nleg 8922\nleq 8804\nleqq 8806\n"
  1402. + "leqslant 10877\nles 10877\nlescc 10920\nlesdot 10879\nlesdoto 10881\nlesdotor 10883\n"
  1403. + "lesg 8922,65024\nlesges 10899\nlessapprox 10885\nlessdot 8918\nlesseqgtr 8922\n"
  1404. + "lesseqqgtr 10891\nlessgtr 8822\nlesssim 8818\nlfisht 10620\nlfloor 8970\nlfr 120105\n"
  1405. + "lg 8822\nlgE 10897\nlhard 8637\nlharu 8636\nlharul 10602\nlhblk 9604\nljcy 1113\nll 8810\n"
  1406. + "llarr 8647\nllcorner 8990\nllhard 10603\nlltri 9722\nlmidot 320\nlmoust 9136\n"
  1407. + "lmoustache 9136\nlnE 8808\nlnap 10889\nlnapprox 10889\nlne 10887\nlneq 10887\nlneqq 8808\n"
  1408. + "lnsim 8934\nloang 10220\nloarr 8701\nlobrk 10214\nlongleftarrow 10229\n"
  1409. + "longleftrightarrow 10231\nlongmapsto 10236\nlongrightarrow 10230\nlooparrowleft 8619\n"
  1410. + "looparrowright 8620\nlopar 10629\nlopf 120157\nloplus 10797\nlotimes 10804\nlowast 8727\n"
  1411. + "lowbar 95\nloz 9674\nlozenge 9674\nlozf 10731\nlpar 40\nlparlt 10643\nlrarr 8646\n"
  1412. + "lrcorner 8991\nlrhar 8651\nlrhard 10605\nlrm 8206\nlrtri 8895\nlsaquo 8249\nlscr 120001\n"
  1413. + "lsh 8624\nlsim 8818\nlsime 10893\nlsimg 10895\nlsqb 91\nlsquo 8216\nlsquor 8218\n"
  1414. + "lstrok 322\nlt 60\nltcc 10918\nltcir 10873\nltdot 8918\nlthree 8907\nltimes 8905\n"
  1415. + "ltlarr 10614\nltquest 10875\nltrPar 10646\nltri 9667\nltrie 8884\nltrif 9666\n"
  1416. + "lurdshar 10570\nluruhar 10598\nlvertneqq 8808,65024\nlvnE 8808,65024\nmDDot 8762\n"
  1417. + "macr 175\nmale 9794\nmalt 10016\nmaltese 10016\nmap 8614\nmapsto 8614\nmapstodown 8615\n"
  1418. + "mapstoleft 8612\nmapstoup 8613\nmarker 9646\nmcomma 10793\nmcy 1084\nmdash 8212\n"
  1419. + "measuredangle 8737\nmfr 120106\nmho 8487\nmicro 181\nmid 8739\nmidast 42\nmidcir 10992\n"
  1420. + "middot 183\nminus 8722\nminusb 8863\nminusd 8760\nminusdu 10794\nmlcp 10971\nmldr 8230\n"
  1421. + "mnplus 8723\nmodels 8871\nmopf 120158\nmp 8723\nmscr 120002\nmstpos 8766\nmu 956\n"
  1422. + "multimap 8888\nmumap 8888\nnGg 8921,824\nnGt 8811,8402\nnGtv 8811,824\nnLeftarrow 8653\n"
  1423. + "nLeftrightarrow 8654\nnLl 8920,824\nnLt 8810,8402\nnLtv 8810,824\nnRightarrow 8655\n"
  1424. + "nVDash 8879\nnVdash 8878\nnabla 8711\nnacute 324\nnang 8736,8402\nnap 8777\n"
  1425. + "napE 10864,824\nnapid 8779,824\nnapos 329\nnapprox 8777\nnatur 9838\nnatural 9838\n"
  1426. + "naturals 8469\nnbsp 160\nnbump 8782,824\nnbumpe 8783,824\nncap 10819\nncaron 328\n"
  1427. + "ncedil 326\nncong 8775\nncongdot 10861,824\nncup 10818\nncy 1085\nndash 8211\nne 8800\n"
  1428. + "neArr 8663\nnearhk 10532\nnearr 8599\nnearrow 8599\nnedot 8784,824\nnequiv 8802\n"
  1429. + "nesear 10536\nnesim 8770,824\nnexist 8708\nnexists 8708\nnfr 120107\nngE 8807,824\n"
  1430. + "nge 8817\nngeq 8817\nngeqq 8807,824\nngeqslant 10878,824\nnges 10878,824\nngsim 8821\n"
  1431. + "ngt 8815\nngtr 8815\nnhArr 8654\nnharr 8622\nnhpar 10994\nni 8715\nnis 8956\nnisd 8954\n"
  1432. + "niv 8715\nnjcy 1114\nnlArr 8653\nnlE 8806,824\nnlarr 8602\nnldr 8229\nnle 8816\n"
  1433. + "nleftarrow 8602\nnleftrightarrow 8622\nnleq 8816\nnleqq 8806,824\nnleqslant 10877,824\n"
  1434. + "nles 10877,824\nnless 8814\nnlsim 8820\nnlt 8814\nnltri 8938\nnltrie 8940\nnmid 8740\n"
  1435. + "nopf 120159\nnot 172\nnotin 8713\nnotinE 8953,824\nnotindot 8949,824\nnotinva 8713\n"
  1436. + "notinvb 8951\nnotinvc 8950\nnotni 8716\nnotniva 8716\nnotnivb 8958\nnotnivc 8957\n"
  1437. + "npar 8742\nnparallel 8742\nnparsl 11005,8421\nnpart 8706,824\nnpolint 10772\nnpr 8832\n"
  1438. + "nprcue 8928\nnpre 10927,824\nnprec 8832\nnpreceq 10927,824\nnrArr 8655\nnrarr 8603\n"
  1439. + "nrarrc 10547,824\nnrarrw 8605,824\nnrightarrow 8603\nnrtri 8939\nnrtrie 8941\nnsc 8833\n"
  1440. + "nsccue 8929\nnsce 10928,824\nnscr 120003\nnshortmid 8740\nnshortparallel 8742\nnsim 8769\n"
  1441. + "nsime 8772\nnsimeq 8772\nnsmid 8740\nnspar 8742\nnsqsube 8930\nnsqsupe 8931\nnsub 8836\n"
  1442. + "nsubE 10949,824\nnsube 8840\nnsubset 8834,8402\nnsubseteq 8840\nnsubseteqq 10949,824\n"
  1443. + "nsucc 8833\nnsucceq 10928,824\nnsup 8837\nnsupE 10950,824\nnsupe 8841\nnsupset 8835,8402\n"
  1444. + "nsupseteq 8841\nnsupseteqq 10950,824\nntgl 8825\nntilde 241\nntlg 8824\n"
  1445. + "ntriangleleft 8938\nntrianglelefteq 8940\nntriangleright 8939\nntrianglerighteq 8941\n"
  1446. + "nu 957\nnum 35\nnumero 8470\nnumsp 8199\nnvDash 8877\nnvHarr 10500\nnvap 8781,8402\n"
  1447. + "nvdash 8876\nnvge 8805,8402\nnvgt 62,8402\nnvinfin 10718\nnvlArr 10498\nnvle 8804,8402\n"
  1448. + "nvlt 60,8402\nnvltrie 8884,8402\nnvrArr 10499\nnvrtrie 8885,8402\nnvsim 8764,8402\n"
  1449. + "nwArr 8662\nnwarhk 10531\nnwarr 8598\nnwarrow 8598\nnwnear 10535\noS 9416\noacute 243\n"
  1450. + "oast 8859\nocir 8858\nocirc 244\nocy 1086\nodash 8861\nodblac 337\nodiv 10808\nodot 8857\n"
  1451. + "odsold 10684\noelig 339\nofcir 10687\nofr 120108\nogon 731\nograve 242\nogt 10689\n"
  1452. + "ohbar 10677\nohm 937\noint 8750\nolarr 8634\nolcir 10686\nolcross 10683\noline 8254\n"
  1453. + "olt 10688\nomacr 333\nomega 969\nomicron 959\nomid 10678\nominus 8854\noopf 120160\n"
  1454. + "opar 10679\noperp 10681\noplus 8853\nor 8744\norarr 8635\nord 10845\norder 8500\n"
  1455. + "orderof 8500\nordf 170\nordm 186\norigof 8886\noror 10838\norslope 10839\norv 10843\n"
  1456. + "oscr 8500\noslash 248\nosol 8856\notilde 245\notimes 8855\notimesas 10806\nouml 246\n"
  1457. + "ovbar 9021\npar 8741\npara 182\nparallel 8741\nparsim 10995\nparsl 11005\npart 8706\n"
  1458. + "pcy 1087\npercnt 37\nperiod 46\npermil 8240\nperp 8869\npertenk 8241\npfr 120109\n"
  1459. + "phi 966\nphiv 981\nphmmat 8499\nphone 9742\npi 960\npitchfork 8916\npiv 982\nplanck 8463\n"
  1460. + "planckh 8462\nplankv 8463\nplus 43\nplusacir 10787\nplusb 8862\npluscir 10786\n"
  1461. + "plusdo 8724\nplusdu 10789\npluse 10866\nplusmn 177\nplussim 10790\nplustwo 10791\npm 177\n"
  1462. + "pointint 10773\npopf 120161\npound 163\npr 8826\nprE 10931\nprap 10935\nprcue 8828\n"
  1463. + "pre 10927\nprec 8826\nprecapprox 10935\npreccurlyeq 8828\npreceq 10927\n"
  1464. + "precnapprox 10937\nprecneqq 10933\nprecnsim 8936\nprecsim 8830\nprime 8242\nprimes 8473\n"
  1465. + "prnE 10933\nprnap 10937\nprnsim 8936\nprod 8719\nprofalar 9006\nprofline 8978\n"
  1466. + "profsurf 8979\nprop 8733\npropto 8733\nprsim 8830\nprurel 8880\npscr 120005\npsi 968\n"
  1467. + "puncsp 8200\nqfr 120110\nqint 10764\nqopf 120162\nqprime 8279\nqscr 120006\n"
  1468. + "quaternions 8461\nquatint 10774\nquest 63\nquesteq 8799\nquot 34\nrAarr 8667\nrArr 8658\n"
  1469. + "rAtail 10524\nrBarr 10511\nrHar 10596\nrace 8765,817\nracute 341\nradic 8730\n"
  1470. + "raemptyv 10675\nrang 10217\nrangd 10642\nrange 10661\nrangle 10217\nraquo 187\nrarr 8594\n"
  1471. + "rarrap 10613\nrarrb 8677\nrarrbfs 10528\nrarrc 10547\nrarrfs 10526\nrarrhk 8618\n"
  1472. + "rarrlp 8620\nrarrpl 10565\nrarrsim 10612\nrarrtl 8611\nrarrw 8605\nratail 10522\n"
  1473. + "ratio 8758\nrationals 8474\nrbarr 10509\nrbbrk 10099\nrbrace 125\nrbrack 93\nrbrke 10636\n"
  1474. + "rbrksld 10638\nrbrkslu 10640\nrcaron 345\nrcedil 343\nrceil 8969\nrcub 125\nrcy 1088\n"
  1475. + "rdca 10551\nrdldhar 10601\nrdquo 8221\nrdquor 8221\nrdsh 8627\nreal 8476\nrealine 8475\n"
  1476. + "realpart 8476\nreals 8477\nrect 9645\nreg 174\nrfisht 10621\nrfloor 8971\nrfr 120111\n"
  1477. + "rhard 8641\nrharu 8640\nrharul 10604\nrho 961\nrhov 1009\nrightarrow 8594\n"
  1478. + "rightarrowtail 8611\nrightharpoondown 8641\nrightharpoonup 8640\nrightleftarrows 8644\n"
  1479. + "rightleftharpoons 8652\nrightrightarrows 8649\nrightsquigarrow 8605\n"
  1480. + "rightthreetimes 8908\nring 730\nrisingdotseq 8787\nrlarr 8644\nrlhar 8652\nrlm 8207\n"
  1481. + "rmoust 9137\nrmoustache 9137\nrnmid 10990\nroang 10221\nroarr 8702\nrobrk 10215\n"
  1482. + "ropar 10630\nropf 120163\nroplus 10798\nrotimes 10805\nrpar 41\nrpargt 10644\n"
  1483. + "rppolint 10770\nrrarr 8649\nrsaquo 8250\nrscr 120007\nrsh 8625\nrsqb 93\nrsquo 8217\n"
  1484. + "rsquor 8217\nrthree 8908\nrtimes 8906\nrtri 9657\nrtrie 8885\nrtrif 9656\nrtriltri 10702\n"
  1485. + "ruluhar 10600\nrx 8478\nsacute 347\nsbquo 8218\nsc 8827\nscE 10932\nscap 10936\n"
  1486. + "scaron 353\nsccue 8829\nsce 10928\nscedil 351\nscirc 349\nscnE 10934\nscnap 10938\n"
  1487. + "scnsim 8937\nscpolint 10771\nscsim 8831\nscy 1089\nsdot 8901\nsdotb 8865\nsdote 10854\n"
  1488. + "seArr 8664\nsearhk 10533\nsearr 8600\nsearrow 8600\nsect 167\nsemi 59\nseswar 10537\n"
  1489. + "setminus 8726\nsetmn 8726\nsext 10038\nsfr 120112\nsfrown 8994\nsharp 9839\nshchcy 1097\n"
  1490. + "shcy 1096\nshortmid 8739\nshortparallel 8741\nshy 173\nsigma 963\nsigmaf 962\nsigmav 962\n"
  1491. + "sim 8764\nsimdot 10858\nsime 8771\nsimeq 8771\nsimg 10910\nsimgE 10912\nsiml 10909\n"
  1492. + "simlE 10911\nsimne 8774\nsimplus 10788\nsimrarr 10610\nslarr 8592\nsmallsetminus 8726\n"
  1493. + "smashp 10803\nsmeparsl 10724\nsmid 8739\nsmile 8995\nsmt 10922\nsmte 10924\n"
  1494. + "smtes 10924,65024\nsoftcy 1100\nsol 47\nsolb 10692\nsolbar 9023\nsopf 120164\n"
  1495. + "spades 9824\nspadesuit 9824\nspar 8741\nsqcap 8851\nsqcaps 8851,65024\nsqcup 8852\n"
  1496. + "sqcups 8852,65024\nsqsub 8847\nsqsube 8849\nsqsubset 8847\nsqsubseteq 8849\nsqsup 8848\n"
  1497. + "sqsupe 8850\nsqsupset 8848\nsqsupseteq 8850\nsqu 9633\nsquare 9633\nsquarf 9642\n"
  1498. + "squf 9642\nsrarr 8594\nsscr 120008\nssetmn 8726\nssmile 8995\nsstarf 8902\nstar 9734\n"
  1499. + "starf 9733\nstraightepsilon 1013\nstraightphi 981\nstrns 175\nsub 8834\nsubE 10949\n"
  1500. + "subdot 10941\nsube 8838\nsubedot 10947\nsubmult 10945\nsubnE 10955\nsubne 8842\n"
  1501. + "subplus 10943\nsubrarr 10617\nsubset 8834\nsubseteq 8838\nsubseteqq 10949\n"
  1502. + "subsetneq 8842\nsubsetneqq 10955\nsubsim 10951\nsubsub 10965\nsubsup 10963\nsucc 8827\n"
  1503. + "succapprox 10936\nsucccurlyeq 8829\nsucceq 10928\nsuccnapprox 10938\nsuccneqq 10934\n"
  1504. + "succnsim 8937\nsuccsim 8831\nsum 8721\nsung 9834\nsup 8835\nsup1 185\nsup2 178\nsup3 179\n"
  1505. + "supE 10950\nsupdot 10942\nsupdsub 10968\nsupe 8839\nsupedot 10948\nsuphsol 10185\n"
  1506. + "suphsub 10967\nsuplarr 10619\nsupmult 10946\nsupnE 10956\nsupne 8843\nsupplus 10944\n"
  1507. + "supset 8835\nsupseteq 8839\nsupseteqq 10950\nsupsetneq 8843\nsupsetneqq 10956\n"
  1508. + "supsim 10952\nsupsub 10964\nsupsup 10966\nswArr 8665\nswarhk 10534\nswarr 8601\n"
  1509. + "swarrow 8601\nswnwar 10538\nszlig 223\ntarget 8982\ntau 964\ntbrk 9140\ntcaron 357\n"
  1510. + "tcedil 355\ntcy 1090\ntdot 8411\ntelrec 8981\ntfr 120113\nthere4 8756\ntherefore 8756\n"
  1511. + "theta 952\nthetasym 977\nthetav 977\nthickapprox 8776\nthicksim 8764\nthinsp 8201\n"
  1512. + "thkap 8776\nthksim 8764\nthorn 254\ntilde 732\ntimes 215\ntimesb 8864\ntimesbar 10801\n"
  1513. + "timesd 10800\ntint 8749\ntoea 10536\ntop 8868\ntopbot 9014\ntopcir 10993\ntopf 120165\n"
  1514. + "topfork 10970\ntosa 10537\ntprime 8244\ntrade 8482\ntriangle 9653\ntriangledown 9663\n"
  1515. + "triangleleft 9667\ntrianglelefteq 8884\ntriangleq 8796\ntriangleright 9657\n"
  1516. + "trianglerighteq 8885\ntridot 9708\ntrie 8796\ntriminus 10810\ntriplus 10809\ntrisb 10701\n"
  1517. + "tritime 10811\ntrpezium 9186\ntscr 120009\ntscy 1094\ntshcy 1115\ntstrok 359\ntwixt 8812\n"
  1518. + "twoheadleftarrow 8606\ntwoheadrightarrow 8608\nuArr 8657\nuHar 10595\nuacute 250\n"
  1519. + "uarr 8593\nubrcy 1118\nubreve 365\nucirc 251\nucy 1091\nudarr 8645\nudblac 369\n"
  1520. + "udhar 10606\nufisht 10622\nufr 120114\nugrave 249\nuharl 8639\nuharr 8638\nuhblk 9600\n"
  1521. + "ulcorn 8988\nulcorner 8988\nulcrop 8975\nultri 9720\numacr 363\numl 168\nuogon 371\n"
  1522. + "uopf 120166\nuparrow 8593\nupdownarrow 8597\nupharpoonleft 8639\nupharpoonright 8638\n"
  1523. + "uplus 8846\nupsi 965\nupsih 978\nupsilon 965\nupuparrows 8648\nurcorn 8989\n"
  1524. + "urcorner 8989\nurcrop 8974\nuring 367\nurtri 9721\nuscr 120010\nutdot 8944\nutilde 361\n"
  1525. + "utri 9653\nutrif 9652\nuuarr 8648\nuuml 252\nuwangle 10663\nvArr 8661\nvBar 10984\n"
  1526. + "vBarv 10985\nvDash 8872\nvangrt 10652\nvarepsilon 1013\nvarkappa 1008\nvarnothing 8709\n"
  1527. + "varphi 981\nvarpi 982\nvarpropto 8733\nvarr 8597\nvarrho 1009\nvarsigma 962\n"
  1528. + "varsubsetneq 8842,65024\nvarsubsetneqq 10955,65024\nvarsupsetneq 8843,65024\n"
  1529. + "varsupsetneqq 10956,65024\nvartheta 977\nvartriangleleft 8882\nvartriangleright 8883\n"
  1530. + "vcy 1074\nvdash 8866\nvee 8744\nveebar 8891\nveeeq 8794\nvellip 8942\nverbar 124\n"
  1531. + "vert 124\nvfr 120115\nvltri 8882\nvnsub 8834,8402\nvnsup 8835,8402\nvopf 120167\n"
  1532. + "vprop 8733\nvrtri 8883\nvscr 120011\nvsubnE 10955,65024\nvsubne 8842,65024\n"
  1533. + "vsupnE 10956,65024\nvsupne 8843,65024\nvzigzag 10650\nwcirc 373\nwedbar 10847\n"
  1534. + "wedge 8743\nwedgeq 8793\nweierp 8472\nwfr 120116\nwopf 120168\nwp 8472\nwr 8768\n"
  1535. + "wreath 8768\nwscr 120012\nxcap 8898\nxcirc 9711\nxcup 8899\nxdtri 9661\nxfr 120117\n"
  1536. + "xhArr 10234\nxharr 10231\nxi 958\nxlArr 10232\nxlarr 10229\nxmap 10236\nxnis 8955\n"
  1537. + "xodot 10752\nxopf 120169\nxoplus 10753\nxotime 10754\nxrArr 10233\nxrarr 10230\n"
  1538. + "xscr 120013\nxsqcup 10758\nxuplus 10756\nxutri 9651\nxvee 8897\nxwedge 8896\nyacute 253\n"
  1539. + "yacy 1103\nycirc 375\nycy 1099\nyen 165\nyfr 120118\nyicy 1111\nyopf 120170\nyscr 120014\n"
  1540. + "yucy 1102\nyuml 255\nzacute 378\nzcaron 382\nzcy 1079\nzdot 380\nzeetrf 8488\nzeta 950\n"
  1541. + "zfr 120119\nzhcy 1078\nzigrarr 8669\nzopf 120171\nzscr 120015\nzwj 8205\nzwnj 8204\n"
  1542. ;
  1543. // ---------------------------------------------------------------------------
  1544. // Markdown block structure (CommonMark 0.31.2, section 3)
  1545. // ---------------------------------------------------------------------------
  1546. //
  1547. // A cmark-style line scanner: each line is matched against the stack of
  1548. // open "container" blocks (document > blockquote > list > item > ...),
  1549. // consuming their prefixes, then new blocks are attempted in order
  1550. // (blockquote, ATX heading, fenced code, setext underline, thematic break,
  1551. // list item, indented code). Whatever remains is appended to the deepest
  1552. // open leaf, or becomes a lazy paragraph continuation.
  1553. //
  1554. // We do not implement HTML blocks as containers: raw HTML is a security
  1555. // hazard for `innerHTML`, so HTML-block-looking paragraphs stay paragraphs
  1556. // and get escaped by the inline pass like any other text. That also means
  1557. // `<script>` cannot interrupt a paragraph the way the spec allows.
  1558. /** Number of columns that count as an indented code block. */
  1559. var MD_CODE_INDENT = 4;
  1560. /** Characters that can begin a non-paragraph block (fast bail-out). */
  1561. var MD_MAYBE_SPECIAL_RE = /^[#`~*+_=<>0-9\-]/;
  1562. var MD_THEMATIC_RE = /^(?:\*[ \t]*){3,}$|^(?:_[ \t]*){3,}$|^(?:-[ \t]*){3,}$/;
  1563. var MD_BULLET_RE = /^[*+\-]/;
  1564. var MD_ORDERED_RE = /^(\d{1,9})([.)])/;
  1565. var MD_ATX_RE = /^#{1,6}(?:[ \t]+|$)/;
  1566. var MD_FENCE_RE = /^`{3,}(?!.*`)|^~{3,}/;
  1567. var MD_CLOSE_FENCE_RE = /^(?:`{3,}|~{3,})(?=[ \t]*$)/;
  1568. var MD_SETEXT_RE = /^(?:=+|-+)[ \t]*$/;
  1569. var MD_NON_SPACE_RE = /[^ \t\f\v\r\n]/;
  1570. /**
  1571. * Create a block node of the given type.
  1572. * @param {string} type document|blockquote|list|item|paragraph|heading|code|thematic
  1573. */
  1574. function mdNode(type) {
  1575. return {
  1576. type: type, parent: null, children: [], strings: [],
  1577. open: true, info: '', literal: '',
  1578. fenced: false, fenceChar: '', fenceLength: 0, fenceOffset: 0,
  1579. level: 1, listData: null, tight: true, headingSetext: false,
  1580. sourceLine: 0
  1581. };
  1582. }
  1583. function mdParser() {
  1584. var doc = mdNode('document');
  1585. return {
  1586. doc: doc, tip: doc, oldtip: doc,
  1587. offset: 0, column: 0, nextNonspace: 0, nextNonspaceColumn: 0,
  1588. indent: 0, indented: false, blank: false, partiallyConsumedTab: false,
  1589. allClosed: true, lastMatchedContainer: doc,
  1590. currentLine: '', lineNumber: 0, lastLineLength: 0,
  1591. refmap: {}, lineOffsets: []
  1592. };
  1593. }
  1594. /** Normalise endings and split into lines (trailing blank from final \n dropped). */
  1595. function mdSplitInput(raw) {
  1596. var text = String(raw).replace(/\r\n|\r(?!\n)/g, '\n').replace(/\r/g, '\n');
  1597. var out = text.split('\n');
  1598. if (out.length && out[out.length - 1] === '') out.pop();
  1599. return out;
  1600. }
  1601. // --- parser cursor helpers ------------------------------------------------
  1602. /** Find the next non-space/tab char; sets blank/indent/indented on p. */
  1603. function mdFindNextNonspace(p) {
  1604. var ln = p.currentLine, i = p.offset, cols = p.column, c;
  1605. while ((c = ln.charAt(i)) !== '') {
  1606. if (c === ' ') { i++; cols++; }
  1607. else if (c === '\t') { i++; cols += 4 - (cols % 4); }
  1608. else break;
  1609. }
  1610. p.blank = c === '' ;
  1611. p.nextNonspace = i;
  1612. p.nextNonspaceColumn = cols;
  1613. p.indent = p.nextNonspaceColumn - p.column;
  1614. p.indented = p.indent >= MD_CODE_INDENT;
  1615. }
  1616. /**
  1617. * Advance the cursor by `count` columns (or characters when `columns` is
  1618. * false). Handles partially consumed tabs, which stay in the line as the
  1619. * equivalent spaces.
  1620. */
  1621. function mdAdvanceOffset(p, count, columns) {
  1622. var ln = p.currentLine, charsToTab, advance, c;
  1623. while (count > 0 && (c = ln.charAt(p.offset)) !== '') {
  1624. if (c === '\t') {
  1625. charsToTab = 4 - (p.column % 4);
  1626. if (columns) {
  1627. p.partiallyConsumedTab = charsToTab > count;
  1628. advance = charsToTab > count ? count : charsToTab;
  1629. p.column += advance;
  1630. if (!p.partiallyConsumedTab) p.offset += 1;
  1631. count -= advance;
  1632. } else {
  1633. p.partiallyConsumedTab = false;
  1634. p.column += charsToTab;
  1635. p.offset += 1;
  1636. count -= 1;
  1637. }
  1638. } else {
  1639. p.partiallyConsumedTab = false;
  1640. p.offset += 1;
  1641. p.column += 1;
  1642. count -= 1;
  1643. }
  1644. }
  1645. }
  1646. function mdAdvanceNextNonspace(p) {
  1647. p.offset = p.nextNonspace;
  1648. p.column = p.nextNonspaceColumn;
  1649. p.partiallyConsumedTab = false;
  1650. }
  1651. /**
  1652. * Append the rest of the current line (from p.offset) to the tip. When a
  1653. * tab was only partially consumed, the surviving columns are re-emitted as
  1654. * spaces, exactly like cmark.
  1655. */
  1656. function mdAddLine(p) {
  1657. var s = '';
  1658. if (p.partiallyConsumedTab) {
  1659. p.offset += 1; // skip the tab character itself
  1660. var charsToTab = 4 - (p.column % 4);
  1661. while (s.length < charsToTab) s += ' ';
  1662. }
  1663. p.tip.strings.push(s + p.currentLine.slice(p.offset));
  1664. }
  1665. function mdCanContain(type, child) {
  1666. if (type === 'item' || type === 'blockquote' ||
  1667. type === 'document' || type === 'list') {
  1668. if (type === 'list') return child === 'item';
  1669. return child !== 'item';
  1670. }
  1671. return false;
  1672. }
  1673. function mdAcceptsLines(type) {
  1674. return type === 'paragraph' || type === 'code';
  1675. }
  1676. /** Close every block from tip down to (excluding) `lastMatched`. */
  1677. function mdCloseUnmatched(p) {
  1678. if (p.allClosed) return;
  1679. while (p.oldtip !== p.lastMatchedContainer) {
  1680. mdFinalize(p, p.oldtip, p.lineNumber - 1);
  1681. p.oldtip = p.oldtip.parent;
  1682. }
  1683. p.allClosed = true;
  1684. }
  1685. /**
  1686. * Add a child block of `tag` under the tip, finalising any tip that cannot
  1687. * contain it first.
  1688. */
  1689. function mdAddChild(p, tag) {
  1690. while (!mdCanContain(p.tip.type, tag)) mdFinalize(p, p.tip, p.lineNumber - 1);
  1691. var node = mdNode(tag);
  1692. node.parent = p.tip;
  1693. node.sourceLine = p.lineNumber;
  1694. p.tip.children.push(node);
  1695. p.tip = node;
  1696. return node;
  1697. }
  1698. // --- container continue checks -------------------------------------------
  1699. // 0 = matched, 1 = not matched, 2 = line fully handled (fence close)
  1700. function mdContinueBlock(p, container) {
  1701. var ln = p.currentLine, t = container.type;
  1702. if (t === 'document' || t === 'list') return 0;
  1703. if (t === 'blockquote') {
  1704. if (!p.indented && ln.charAt(p.nextNonspace) === '>') {
  1705. mdAdvanceNextNonspace(p);
  1706. mdAdvanceOffset(p, 1, false);
  1707. if (ln.charAt(p.offset) === ' ' || ln.charAt(p.offset) === '\t') {
  1708. mdAdvanceOffset(p, 1, true);
  1709. }
  1710. return 0;
  1711. }
  1712. return 1;
  1713. }
  1714. if (t === 'item') {
  1715. if (p.blank) {
  1716. if (!container.children.length) return 1; // blank after empty item
  1717. mdAdvanceNextNonspace(p);
  1718. return 0;
  1719. }
  1720. var need = container.listData.markerOffset + container.listData.padding;
  1721. if (p.indent >= need) {
  1722. mdAdvanceOffset(p, need, true);
  1723. return 0;
  1724. }
  1725. return 1;
  1726. }
  1727. if (t === 'code') {
  1728. if (container.fenced) {
  1729. var rest = ln.slice(p.nextNonspace);
  1730. if (p.indent <= 3 && ln.charAt(p.nextNonspace) === container.fenceChar &&
  1731. MD_CLOSE_FENCE_RE.test(rest) && rest.match(MD_CLOSE_FENCE_RE)[0].length >= container.fenceLength) {
  1732. mdFinalize(p, container, p.lineNumber);
  1733. return 2;
  1734. }
  1735. // skip up to fenceOffset leading spaces
  1736. var i = container.fenceOffset;
  1737. while (i > 0 && (ln.charAt(p.offset) === ' ' || ln.charAt(p.offset) === '\t')) {
  1738. mdAdvanceOffset(p, 1, true);
  1739. i--;
  1740. }
  1741. return 0;
  1742. }
  1743. if (p.indent >= MD_CODE_INDENT) { mdAdvanceOffset(p, MD_CODE_INDENT, true); return 0; }
  1744. if (p.blank) { mdAdvanceNextNonspace(p); return 0; }
  1745. return 1;
  1746. }
  1747. if (t === 'heading' || t === 'thematic') return 1;
  1748. if (t === 'paragraph') return p.blank ? 1 : 0;
  1749. return 1;
  1750. }
  1751. /**
  1752. * True when a blank line separates `node` from its following sibling (used
  1753. * for the tight/loose list decision).
  1754. */
  1755. function mdEndsWithBlank(node) {
  1756. if (!node.parent) return false;
  1757. var idx = node.parent.children.indexOf(node);
  1758. var next = node.parent.children[idx + 1];
  1759. if (!next) return false;
  1760. return mdNodeEndLine(node) !== next.sourceLine - 1;
  1761. }
  1762. /** Last source line number consumed by a node (tracked during the scan). */
  1763. function mdNodeEndLine(node) {
  1764. return node.endLine == null ? node.sourceLine : node.endLine;
  1765. }
  1766. // --- block starts ----------------------------------------------------------
  1767. /**
  1768. * Parse a list marker at the cursor. Returns the marker data or null.
  1769. * Mirrors cmark: the marker itself, then 1-4 spaces (5+ or a blank rest
  1770. * means "content starts one space after the marker").
  1771. */
  1772. function mdParseListMarker(p, container) {
  1773. var rest = p.currentLine.slice(p.nextNonspace);
  1774. var data = { type: null, tight: true, bulletChar: null, start: null,
  1775. delimiter: null, padding: null, markerOffset: p.indent };
  1776. var m, nextc;
  1777. if (p.indent >= 4) return null;
  1778. if ((m = rest.match(MD_BULLET_RE))) {
  1779. data.type = 'bullet';
  1780. data.bulletChar = m[0].charAt(0);
  1781. } else if ((m = rest.match(MD_ORDERED_RE)) &&
  1782. (container.type !== 'paragraph' || Number(m[1]) === 1)) {
  1783. data.type = 'ordered';
  1784. data.start = parseInt(m[1], 10);
  1785. data.delimiter = m[2];
  1786. } else {
  1787. return null;
  1788. }
  1789. nextc = p.currentLine.charAt(p.nextNonspace + m[0].length);
  1790. if (!(nextc === '' || nextc === ' ' || nextc === '\t')) return null;
  1791. if (container.type === 'paragraph' &&
  1792. !MD_NON_SPACE_RE.test(p.currentLine.slice(p.nextNonspace + m[0].length))) {
  1793. return null;
  1794. }
  1795. mdAdvanceNextNonspace(p);
  1796. mdAdvanceOffset(p, m[0].length, true);
  1797. var spacesStartCol = p.column, spacesStartOffset = p.offset;
  1798. do {
  1799. mdAdvanceOffset(p, 1, true);
  1800. nextc = p.currentLine.charAt(p.offset);
  1801. } while (p.column - spacesStartCol < 5 && (nextc === ' ' || nextc === '\t'));
  1802. var blankItem = p.currentLine.charAt(p.offset) === '' && p.offset >= p.currentLine.length;
  1803. var spacesAfterMarker = p.column - spacesStartCol;
  1804. if (spacesAfterMarker >= 5 || spacesAfterMarker < 1 || blankItem) {
  1805. data.padding = m[0].length + 1;
  1806. p.column = spacesStartCol;
  1807. p.offset = spacesStartOffset;
  1808. var c2 = p.currentLine.charAt(p.offset);
  1809. if (c2 === ' ' || c2 === '\t') mdAdvanceOffset(p, 1, true);
  1810. } else {
  1811. data.padding = m[0].length + spacesAfterMarker;
  1812. }
  1813. return data;
  1814. }
  1815. function mdListsMatch(a, b) {
  1816. return a.type === b.type && a.delimiter === b.delimiter &&
  1817. a.bulletChar === b.bulletChar;
  1818. }
  1819. /**
  1820. * Try to start a new block at the cursor. Returns 0 (no match), 1 (opened a
  1821. * container, keep trying), or 2 (opened a leaf; the line is spent).
  1822. */
  1823. function mdTryBlockStart(p, container) {
  1824. var ln = p.currentLine, m, node;
  1825. // block quote
  1826. if (!p.indented && ln.charAt(p.nextNonspace) === '>') {
  1827. mdAdvanceNextNonspace(p);
  1828. mdAdvanceOffset(p, 1, false);
  1829. if (ln.charAt(p.offset) === ' ' || ln.charAt(p.offset) === '\t') {
  1830. mdAdvanceOffset(p, 1, true);
  1831. }
  1832. mdCloseUnmatched(p);
  1833. mdAddChild(p, 'blockquote');
  1834. return 1;
  1835. }
  1836. // ATX heading
  1837. if (!p.indented && (m = ln.slice(p.nextNonspace).match(MD_ATX_RE))) {
  1838. mdAdvanceNextNonspace(p);
  1839. mdAdvanceOffset(p, m[0].length, false);
  1840. mdCloseUnmatched(p);
  1841. node = mdAddChild(p, 'heading');
  1842. node.level = m[0].replace(/[ \t]/g, '').length;
  1843. node.strings.push(
  1844. ln.slice(p.offset)
  1845. .replace(/^[ \t]*#+[ \t]*$/, '')
  1846. .replace(/[ \t]+#+[ \t]*$/, ''));
  1847. mdAdvanceOffset(p, ln.length - p.offset, false);
  1848. return 2;
  1849. }
  1850. // fenced code block
  1851. if (!p.indented && (m = ln.slice(p.nextNonspace).match(MD_FENCE_RE))) {
  1852. mdCloseUnmatched(p);
  1853. node = mdAddChild(p, 'code');
  1854. node.fenced = true;
  1855. node.fenceLength = m[0].length;
  1856. node.fenceChar = m[0].charAt(0);
  1857. node.fenceOffset = p.indent;
  1858. mdAdvanceNextNonspace(p);
  1859. mdAdvanceOffset(p, m[0].length, false);
  1860. return 2;
  1861. }
  1862. // setext heading underline
  1863. if (!p.indented && container.type === 'paragraph' &&
  1864. (m = ln.slice(p.nextNonspace).match(MD_SETEXT_RE))) {
  1865. mdCloseUnmatched(p);
  1866. var pos;
  1867. var joined = container.strings.join('\n');
  1868. while (joined.charAt(0) === '[' && (pos = mdParseReference(joined, p.refmap)) > 0) {
  1869. joined = joined.slice(pos);
  1870. }
  1871. container.strings = joined.length ? [joined] : [];
  1872. var content = container.strings.join('\n');
  1873. if (content.length > 0) {
  1874. node = mdNode('heading');
  1875. node.level = m[0].charAt(0) === '=' ? 1 : 2;
  1876. node.headingSetext = true;
  1877. node.strings = container.strings.slice();
  1878. node.sourceLine = container.sourceLine;
  1879. node.endLine = p.lineNumber;
  1880. node.parent = container.parent;
  1881. var ci = container.parent.children.indexOf(container);
  1882. container.parent.children[ci] = node;
  1883. p.tip = node;
  1884. mdAdvanceOffset(p, ln.length - p.offset, false);
  1885. return 2;
  1886. }
  1887. return 0;
  1888. }
  1889. // thematic break
  1890. if (!p.indented && MD_THEMATIC_RE.test(ln.slice(p.nextNonspace))) {
  1891. mdCloseUnmatched(p);
  1892. mdAddChild(p, 'thematic');
  1893. mdAdvanceOffset(p, ln.length - p.offset, false);
  1894. return 2;
  1895. }
  1896. // list item
  1897. var data;
  1898. if ((!p.indented || container.type === 'list') && (data = mdParseListMarker(p, container))) {
  1899. mdCloseUnmatched(p);
  1900. if (p.tip.type !== 'list' || !mdListsMatch(container.listData, data)) {
  1901. var list = mdAddChild(p, 'list');
  1902. list.listData = data;
  1903. container = list;
  1904. }
  1905. var item = mdAddChild(p, 'item');
  1906. item.listData = data;
  1907. return 1;
  1908. }
  1909. // indented code block
  1910. if (p.indented && p.tip.type !== 'paragraph' && !p.blank) {
  1911. mdAdvanceOffset(p, MD_CODE_INDENT, true);
  1912. mdCloseUnmatched(p);
  1913. mdAddChild(p, 'code');
  1914. return 2;
  1915. }
  1916. return 0;
  1917. }
  1918. // --- line incorporation and finalisation -----------------------------------
  1919. /**
  1920. * Process one source line into the open block tree: match container
  1921. * prefixes, try to open new blocks, then hand the remainder to a leaf.
  1922. */
  1923. function mdIncorporateLine(p, ln) {
  1924. var container = p.doc, lastChild, allMatched = true, res;
  1925. p.oldtip = p.tip;
  1926. p.offset = 0;
  1927. p.column = 0;
  1928. p.blank = false;
  1929. p.partiallyConsumedTab = false;
  1930. p.lineNumber += 1;
  1931. p.currentLine = ln.indexOf('\u0000') >= 0 ? ln.replace(/\u0000/g, '\uFFFD') : ln;
  1932. // 1. Match the open containers' prefixes, deepest first.
  1933. while (container.children.length) {
  1934. lastChild = container.children[container.children.length - 1];
  1935. if (!lastChild.open) break;
  1936. container = lastChild;
  1937. mdFindNextNonspace(p);
  1938. res = mdContinueBlock(p, container);
  1939. if (res === 2) { p.lastLineLength = ln.length; return; }
  1940. if (res === 1) { allMatched = false; container = container.parent; break; }
  1941. }
  1942. p.allClosed = container === p.oldtip;
  1943. p.lastMatchedContainer = container;
  1944. // 2. Unless we already sit in a leaf that accepts lines, try block starts.
  1945. var matchedLeaf = container.type !== 'paragraph' && mdAcceptsLines(container.type);
  1946. while (!matchedLeaf) {
  1947. mdFindNextNonspace(p);
  1948. if (!p.indented && !MD_MAYBE_SPECIAL_RE.test(p.currentLine.slice(p.nextNonspace))) {
  1949. mdAdvanceNextNonspace(p);
  1950. break;
  1951. }
  1952. res = mdTryBlockStart(p, container);
  1953. if (res === 1) { container = p.tip; continue; }
  1954. if (res === 2) { container = p.tip; matchedLeaf = true; break; }
  1955. mdAdvanceNextNonspace(p);
  1956. break;
  1957. }
  1958. // 3. The remainder is line content for the tip.
  1959. if (!p.allClosed && !p.blank && p.tip.type === 'paragraph') {
  1960. mdAddLine(p); // lazy paragraph continuation
  1961. } else {
  1962. mdCloseUnmatched(p);
  1963. if (mdAcceptsLines(container.type)) {
  1964. mdAddLine(p);
  1965. } else if (p.offset < p.currentLine.length && !p.blank) {
  1966. mdAddChild(p, 'paragraph');
  1967. mdAdvanceNextNonspace(p);
  1968. mdAddLine(p);
  1969. }
  1970. }
  1971. p.lastLineLength = ln.length;
  1972. }
  1973. /**
  1974. * Close a block: extract fenced info strings / indented-code literals, work
  1975. * out list tightness, peel link reference definitions off paragraphs and
  1976. * record which source line the block ended on.
  1977. */
  1978. function mdFinalize(p, block, endLine) {
  1979. var above = block.parent;
  1980. block.open = false;
  1981. block.endLine = endLine == null ? p.lineNumber : endLine;
  1982. if (block.type === 'code') {
  1983. if (block.fenced) {
  1984. var content = block.strings.join('\n');
  1985. var nl = content.indexOf('\n');
  1986. var firstLine = nl >= 0 ? content.slice(0, nl) : content;
  1987. var rest = nl >= 0 ? content.slice(nl + 1) : '';
  1988. block.info = mdUnescapeString(firstLine.replace(/^[ \t]+/, '').replace(/[ \t]+$/, ''));
  1989. // cmark keeps the final newline of the block's content
  1990. block.literal = rest.length ? rest + '\n' : (block.strings.length > 1 ? '\n' : '');
  1991. } else {
  1992. var lines = block.strings.slice();
  1993. while (lines.length && !MD_NON_SPACE_RE.test(lines[lines.length - 1])) lines.pop();
  1994. block.literal = lines.join('\n') + '\n';
  1995. }
  1996. block.strings = [];
  1997. }
  1998. if (block.type === 'paragraph') {
  1999. // Reference definitions live at the start of paragraphs only; each
  2000. // consumed definition must advance or we stop.
  2001. var txt = block.strings.join('\n');
  2002. var pos;
  2003. while (txt.charAt(0) === '[' && (pos = mdParseReference(txt, p.refmap)) > 0) {
  2004. txt = txt.slice(pos);
  2005. }
  2006. block.strings = txt.length ? [txt] : [];
  2007. if (!txt.length) mdRemoveFromParent(block);
  2008. }
  2009. if ((block.type === 'item' || block.type === 'list') && block.children.length) {
  2010. // items and lists end where their last child ends (a trailing blank
  2011. // line is not part of them) — this drives the tight/loose rule.
  2012. // Blockquotes deliberately keep the line their `>` marker last
  2013. // appeared on: ` >` is not a blank line for these purposes.
  2014. block.endLine = block.children[block.children.length - 1].endLine;
  2015. }
  2016. if (block.type === 'list') {
  2017. block.tight = true;
  2018. for (var i = 0; i < block.children.length; i++) {
  2019. var item = block.children[i];
  2020. if (i < block.children.length - 1 && mdEndsWithBlank(item)) { block.tight = false; break; }
  2021. for (var j = 0; j < item.children.length; j++) {
  2022. if (j < item.children.length - 1 && mdEndsWithBlank(item.children[j])) { block.tight = false; break; }
  2023. }
  2024. if (!block.tight) break;
  2025. }
  2026. }
  2027. p.tip = above;
  2028. }
  2029. /** Detach a node (used when a paragraph turns out to be all references). */
  2030. function mdRemoveFromParent(node) {
  2031. if (!node.parent) return;
  2032. var i = node.parent.children.indexOf(node);
  2033. if (i >= 0) node.parent.children.splice(i, 1);
  2034. }
  2035. /** Parse the whole input into a block tree. */
  2036. function mdParseDocument(raw) {
  2037. var p = mdParser();
  2038. var lines = mdSplitInput(raw);
  2039. for (var i = 0; i < lines.length; i++) mdIncorporateLine(p, lines[i]);
  2040. while (p.tip && p.tip.type !== 'document') mdFinalize(p, p.tip, lines.length);
  2041. return p;
  2042. }
  2043. // --- shared inline regexes / URI handling -----------------------------------
  2044. var MD_LINK_TITLE_RE = /(?:")([^"]*)(?:")|(?:')([^']*)(?:')/;
  2045. /**
  2046. * Scan a link title at `subject[pos]` without regex backtracking: a quoted
  2047. * run (with `\"`-style escapes) or a parenthesised run. Returns
  2048. * `{ text, len }` or null.
  2049. * @param {string} subject
  2050. * @param {number} pos
  2051. * @returns {?{text: string, len: number}}
  2052. */
  2053. function mdScanLinkTitle(subject, pos) {
  2054. var start = pos, q = subject.charAt(pos);
  2055. if (q !== '"' && q !== "'" && q !== '(') return null;
  2056. var close = q === '(' ? ')' : q;
  2057. var i = pos + 1;
  2058. while (i < subject.length) {
  2059. var ch = subject.charAt(i);
  2060. if (ch === '\\' && subject.charAt(i + 1) !== '' &&
  2061. MD_ESCAPABLE.indexOf(subject.charAt(i + 1)) >= 0) { i += 2; continue; }
  2062. if (ch === close) {
  2063. return { text: subject.slice(start + 1, i), len: i + 1 - start };
  2064. }
  2065. if (ch === '\n' && subject.charAt(i + 1) === '\n') return null; // blank line ends it
  2066. i++;
  2067. }
  2068. return null;
  2069. }
  2070. var MD_LINK_DEST_BRACES_RE = /^<(?:[^<>\n\\\x00]|\\.)*>/;
  2071. /**
  2072. * End bound for scanning a construct that cannot contain whitespace, '<' or
  2073. * '>' from `pos` (autolinks; raw HTML tags scan themselves linearly).
  2074. * Slicing to this index instead of the end of the subject keeps the inline
  2075. * parser linear on pathological input (many unmatched '<' or '&'): the
  2076. * anchored construct regexes cannot match past the bound anyway.
  2077. * @param {string} subject
  2078. * @param {number} pos
  2079. * @returns {number}
  2080. */
  2081. function mdBoundScan(subject, pos) {
  2082. var i = pos + 1, c;
  2083. while (i < subject.length) {
  2084. c = subject.charCodeAt(i);
  2085. if (c <= 0x20 || c === 0x3C || c === 0x3E) break;
  2086. i++;
  2087. }
  2088. return i < subject.length ? i + 1 : subject.length;
  2089. }
  2090. /**
  2091. * Match a raw HTML open/close tag at subject[pos] using a linear scan
  2092. * (the equivalent regex backtracks catastrophically on malformed input).
  2093. * Returns the matched text or null. SECURITY: callers render this escaped.
  2094. * @param {string} subject
  2095. * @param {number} pos
  2096. * @returns {?string}
  2097. */
  2098. function mdScanHtmlTag(subject, pos) {
  2099. var i = pos;
  2100. if (subject.charAt(i) !== '<') return null;
  2101. i++;
  2102. var closing = subject.charAt(i) === '/';
  2103. if (closing) i++;
  2104. if (!/[A-Za-z]/.test(subject.charAt(i))) return null;
  2105. var start = i;
  2106. while (i < subject.length && /[A-Za-z0-9-]/.test(subject.charAt(i))) i++;
  2107. if (i === start) return null;
  2108. if (closing) {
  2109. while (i < subject.length && /[ \t\n]/.test(subject.charAt(i))) i++;
  2110. if (subject.charAt(i) === '>') return subject.slice(pos, i + 1);
  2111. return null;
  2112. }
  2113. // attributes
  2114. while (i < subject.length) {
  2115. var wsStart = i;
  2116. while (i < subject.length && /[ \t\n]/.test(subject.charAt(i))) i++;
  2117. if (subject.charAt(i) === '>') return subject.slice(pos, i + 1);
  2118. if (subject.charAt(i) === '/' && subject.charAt(i + 1) === '>') return subject.slice(pos, i + 2);
  2119. if (i === wsStart) return null; // attribute must be preceded by space
  2120. var an = i;
  2121. while (i < subject.length && !/[\s/=><]/.test(subject.charAt(i))) i++;
  2122. if (i === an) return null;
  2123. var eq = i;
  2124. while (i < subject.length && /[ \t\n]/.test(subject.charAt(i))) i++;
  2125. if (subject.charAt(i) !== '=') { i = eq; continue; }
  2126. i++;
  2127. while (i < subject.length && /[ \t\n]/.test(subject.charAt(i))) i++;
  2128. var q = subject.charAt(i);
  2129. if (q === '"' || q === "'") {
  2130. i++;
  2131. var endq = subject.indexOf(q, i);
  2132. if (endq < 0) return null;
  2133. i = endq + 1;
  2134. } else {
  2135. var v = i;
  2136. while (i < subject.length && !/[\s>]/.test(subject.charAt(i))) i++;
  2137. if (i === v) return null;
  2138. }
  2139. }
  2140. return null;
  2141. }
  2142. /**
  2143. * CommonMark's URL normalisation: percent-encode every character that is
  2144. * not in the RFC 3986 "unreserved + reserved + '#' escape" set. Non-ASCII
  2145. * characters become UTF-8 percent-escapes (like encodeURIComponent).
  2146. * @param {string} uri
  2147. * @returns {string}
  2148. */
  2149. function mdNormalizeUri(uri) {
  2150. if (!uri) return '';
  2151. var out = '', ch, i;
  2152. for (i = 0; i < uri.length; i++) {
  2153. ch = uri.charAt(i);
  2154. if (MD_URI_SAFE_RE.test(ch)) { out += ch; continue; }
  2155. // encode one code point (surrogate pairs together)
  2156. var cp = uri.charCodeAt(i);
  2157. var str = ch;
  2158. if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < uri.length) {
  2159. str += uri.charAt(i + 1);
  2160. i++;
  2161. }
  2162. out += encodeURIComponent(str);
  2163. }
  2164. return out;
  2165. }
  2166. /** URI characters kept literally (unreserved, reserved and '%'). */
  2167. var MD_URI_SAFE_RE = /^[A-Za-z0-9;,\/?:@&=+\$\-_\.!~\*'\(\)%#]$/;
  2168. /**
  2169. * Normalise a link label for reference lookup: strip brackets, trim,
  2170. * collapse internal whitespace, case-fold.
  2171. * @param {string} label raw `[...]` including brackets
  2172. * @returns {string}
  2173. */
  2174. function mdNormalizeReference(label) {
  2175. return label.slice(1, label.length - 1).trim()
  2176. .replace(/[ \t\r\n]+/g, ' ')
  2177. .toLowerCase().toUpperCase();
  2178. }
  2179. /**
  2180. * Parse one link reference definition from the start of `s`. Returns the
  2181. * number of characters consumed, or 0.
  2182. * @param {string} s
  2183. * @param {Object} refmap
  2184. * @returns {number}
  2185. */
  2186. function mdParseReference(s, refmap) {
  2187. var subject = s, pos = 0, rawlabel, dest, title = null, startpos = 0;
  2188. function peek2() { return pos < subject.length ? subject.charAt(pos) : ''; }
  2189. function match2(re) {
  2190. var m = re.exec(subject.slice(pos));
  2191. if (m) pos += m[0].length;
  2192. return m;
  2193. }
  2194. function spnl2() { match2(/^ *(?:\n *)?/); }
  2195. // label: one bracketed run, no nested brackets, no blank line
  2196. var depth = 0, i = 0, ch;
  2197. while (i < subject.length) {
  2198. ch = subject.charAt(i);
  2199. if (ch === '\\') { i += 2; continue; }
  2200. if (ch === '[') { depth++; if (depth > 1) return 0; }
  2201. else if (ch === ']') break;
  2202. i++;
  2203. }
  2204. if (depth !== 1 || subject.charAt(i) !== ']') return 0;
  2205. rawlabel = subject.slice(0, i + 1);
  2206. if (rawlabel.length > 1001) return 0;
  2207. pos = i + 1;
  2208. if (peek2() !== ':') return 0;
  2209. pos++;
  2210. spnl2();
  2211. // destination
  2212. var m2 = match2(MD_LINK_DEST_BRACES_RE);
  2213. if (m2) {
  2214. dest = mdNormalizeUri(mdUnescapeString(m2[0].slice(1, -1)));
  2215. } else {
  2216. var save = pos, openparens = 0, c;
  2217. while ((c = peek2()) !== '') {
  2218. if (c === '\\' && subject.charAt(pos + 1) !== '' &&
  2219. MD_ESCAPABLE.indexOf(subject.charAt(pos + 1)) >= 0) {
  2220. pos += 1;
  2221. if (peek2() !== '') pos += 1;
  2222. } else if (c === '(') { pos++; openparens++; }
  2223. else if (c === ')') { if (openparens < 1) break; pos++; openparens--; }
  2224. else if (MD_WS_CHAR_RE.test(c)) break;
  2225. else pos++;
  2226. }
  2227. if (pos === save && c !== ')') return 0;
  2228. if (openparens !== 0) return 0;
  2229. dest = mdNormalizeUri(mdUnescapeString(subject.slice(save, pos)));
  2230. }
  2231. var beforetitle = pos;
  2232. spnl2();
  2233. if (pos !== beforetitle) {
  2234. var t = mdScanLinkTitle(subject, pos);
  2235. if (t) { title = mdUnescapeString(t.text); pos += t.len; }
  2236. }
  2237. if (title === null) pos = beforetitle;
  2238. // must be at end of line
  2239. var atLineEnd = match2(/^ *(?:\n|$)/) !== null;
  2240. if (!atLineEnd) {
  2241. if (title !== null) {
  2242. title = null;
  2243. pos = beforetitle;
  2244. atLineEnd = match2(/^ *(?:\n|$)/) !== null;
  2245. }
  2246. if (!atLineEnd) return 0;
  2247. }
  2248. var normlabel = mdNormalizeReference(rawlabel);
  2249. if (normlabel === '') return 0;
  2250. if (!refmap[normlabel]) {
  2251. refmap[normlabel] = { destination: dest, title: title == null ? '' : title };
  2252. }
  2253. return pos;
  2254. }
  2255. // ---------------------------------------------------------------------------
  2256. // Markdown inline parsing (CommonMark 0.31.2, section 6)
  2257. // ---------------------------------------------------------------------------
  2258. //
  2259. // A hand-rolled scanner over paragraph/heading content: plain text runs are
  2260. // copied until a special character (\n ` [ ] \ ! < & * _), then that
  2261. // character's handler runs. Emphasis is resolved afterwards with the
  2262. // delimiter-run algorithm from the spec: openers are searched bottom-up
  2263. // with the "rule of 3" and openers_bottom optimisation; leftover delimiter
  2264. // text stays literal. Links and images share a bracket stack; links may
  2265. // not contain links.
  2266. //
  2267. // Inline nodes form a doubly-linked list (firstChild/lastChild/next/prev on
  2268. // the parent) exactly like the block tree: process_emphasis has to splice
  2269. // nodes out of the middle, which is painful with arrays.
  2270. var MD_ESCAPABLE = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';
  2271. var MD_ASCII_PUNCT_TEST_RE = /^[!-/:-@[-`{-~]$/;
  2272. var MD_ENTITY_HERE_RE = /^&(?:#x[a-f0-9]{1,6}|#[0-9]{1,7}|[a-z][a-z0-9]{1,31});/i;
  2273. var MD_TICKS_HERE_RE = /^`+/;
  2274. var MD_WS_CHAR_RE = /[ \t\n\x0b\x0c\x0d]/;
  2275. var MD_FINAL_SPACE_RE = / *$/;
  2276. /**
  2277. * Unicode punctuation/symbol code points (categories P* and S*) above the
  2278. * ASCII range, as "lo-hi" hex ranges (binary-searched). Needed for the
  2279. * left/right-flanking rules on non-ASCII text; ES5 has no \p{P}.
  2280. */
  2281. var MD_UNICODE_PUNCT_RANGES = [
  2282. 'A1-A9','AB-AC','AE-B1','B4','B6-B8','BB','BF','D7','F7','2C2-2C5','2D2-2DF','2E5-2EB',
  2283. '2ED','2EF-2FF','375','37E','384-385','387','3F6','482','55A-55F','589-58A','58D-58F','5BE',
  2284. '5C0','5C3','5C6','5F3-5F4','606-60F','61B','61D-61F','66A-66D','6D4','6DE','6E9','6FD-6FE',
  2285. '700-70D','7F6-7F9','7FE-7FF','830-83E','85E','888','964-965','970','9F2-9F3','9FA-9FB',
  2286. '9FD','A76','AF0-AF1','B70','BF3-BFA','C77','C7F','C84','D4F','D79','DF4','E3F','E4F',
  2287. 'E5A-E5B','F01-F17','F1A-F1F','F34','F36','F38','F3A-F3D','F85','FBE-FC5','FC7-FCC',
  2288. 'FCE-FDA','104A-104F','109E-109F','10FB','1360-1368','1390-1399','1400','166D-166E',
  2289. '169B-169C','16EB-16ED','1735-1736','17D4-17D6','17D8-17DB','1800-180A','1940','1944-1945',
  2290. '19DE-19FF','1A1E-1A1F','1AA0-1AA6','1AA8-1AAD','1B4E-1B4F','1B5A-1B6A','1B74-1B7F',
  2291. '1BFC-1BFF','1C3B-1C3F','1C7E-1C7F','1CC0-1CC7','1CD3','1FBD','1FBF-1FC1','1FCD-1FCF',
  2292. '1FDD-1FDF','1FED-1FEF','1FFD-1FFE','2010-2027','2030-205E','207A-207E','208A-208E',
  2293. '20A0-20C0','2100-2101','2103-2106','2108-2109','2114','2116-2118','211E-2123','2125',
  2294. '2127','2129','212E','213A-213B','2140-2144','214A-214D','214F','218A-218B','2190-2429',
  2295. '2440-244A','249C-24E9','2500-2775','2794-2B73','2B76-2B95','2B97-2BFF','2CE5-2CEA',
  2296. '2CF9-2CFC','2CFE-2CFF','2D70','2E00-2E2E','2E30-2E5D','2E80-2E99','2E9B-2EF3','2F00-2FD5',
  2297. '2FF0-2FFF','3001-3004','3008-3020','3030','3036-3037','303D-303F','309B-309C','30A0',
  2298. '30FB','3190-3191','3196-319F','31C0-31E5','31EF','3200-321E','322A-3247','3250',
  2299. '3260-327F','328A-32B0','32C0-33FF','4DC0-4DFF','A490-A4C6','A4FE-A4FF','A60D-A60F','A673',
  2300. 'A67E','A6F2-A6F7','A700-A716','A720-A721','A789-A78A','A828-A82B','A836-A839','A874-A877',
  2301. 'A8CE-A8CF','A8F8-A8FA','A8FC','A92E-A92F','A95F','A9C1-A9CD','A9DE-A9DF','AA5C-AA5F',
  2302. 'AA77-AA79','AADE-AADF','AAF0-AAF1','AB5B','AB6A-AB6B','ABEB','FB29','FBB2-FBC2',
  2303. 'FD3E-FD4F','FDCF','FDFC-FDFF','FE10-FE19','FE30-FE52','FE54-FE66','FE68-FE6B','FF01-FF0F',
  2304. 'FF1A-FF20','FF3B-FF40','FF5B-FF65','FFE0-FFE6','FFE8-FFEE','FFFC-FFFD','10100-10102',
  2305. '10137-1013F','10179-10189','1018C-1018E','10190-1019C','101A0','101D0-101FC','1039F',
  2306. '103D0','1056F','10857','10877-10878','1091F','1093F','10A50-10A58','10A7F','10AC8',
  2307. '10AF0-10AF6','10B39-10B3F','10B99-10B9C','10D6E','10D8E-10D8F','10EAD','10F55-10F59',
  2308. '10F86-10F89','11047-1104D','110BB-110BC','110BE-110C1','11140-11143','11174-11175',
  2309. '111C5-111C8','111CD','111DB','111DD-111DF','11238-1123D','112A9','113D4-113D5',
  2310. '113D7-113D8','1144B-1144F','1145A-1145B','1145D','114C6','115C1-115D7','11641-11643',
  2311. '11660-1166C','116B9','1173C-1173F','1183B','11944-11946','119E2','11A3F-11A46',
  2312. '11A9A-11A9C','11A9E-11AA2','11B00-11B09','11BE1','11C41-11C45','11C70-11C71','11EF7-11EF8',
  2313. '11F43-11F4F','11FD5-11FF1','11FFF','12470-12474','12FF1-12FF2','16A6E-16A6F','16AF5',
  2314. '16B37-16B3F','16B44-16B45','16D6D-16D6F','16E97-16E9A','16FE2','1BC9C','1BC9F',
  2315. '1CC00-1CCEF','1CD00-1CEB3','1CF50-1CFC3','1D000-1D0F5','1D100-1D126','1D129-1D164',
  2316. '1D16A-1D16C','1D183-1D184','1D18C-1D1A9','1D1AE-1D1EA','1D200-1D241','1D245','1D300-1D356',
  2317. '1D6C1','1D6DB','1D6FB','1D715','1D735','1D74F','1D76F','1D789','1D7A9','1D7C3',
  2318. '1D800-1D9FF','1DA37-1DA3A','1DA6D-1DA74','1DA76-1DA83','1DA85-1DA8B','1E14F','1E2FF',
  2319. '1E5FF','1E95E-1E95F','1ECAC','1ECB0','1ED2E','1EEF0-1EEF1','1F000-1F02B','1F030-1F093',
  2320. '1F0A0-1F0AE','1F0B1-1F0BF','1F0C1-1F0CF','1F0D1-1F0F5','1F10D-1F1AD','1F1E6-1F202',
  2321. '1F210-1F23B','1F240-1F248','1F250-1F251','1F260-1F265','1F300-1F6D7','1F6DC-1F6EC',
  2322. '1F6F0-1F6FC','1F700-1F776','1F77B-1F7D9','1F7E0-1F7EB','1F7F0','1F800-1F80B','1F810-1F847',
  2323. '1F850-1F859','1F860-1F887','1F890-1F8AD','1F8B0-1F8BB','1F8C0-1F8C1','1F900-1FA53',
  2324. '1FA60-1FA6D','1FA70-1FA7C','1FA80-1FA89','1FA8F-1FAC6','1FACE-1FADC','1FADF-1FAE9',
  2325. '1FAF0-1FAF8','1FB00-1FB92','1FB94-1FBEF',
  2326. ];
  2327. function mdIsUnicodePunct(cp) {
  2328. var lo = 0, hi = MD_UNICODE_PUNCT_RANGES.length - 1;
  2329. while (lo <= hi) {
  2330. var mid = (lo + hi) >> 1;
  2331. var dash = MD_UNICODE_PUNCT_RANGES[mid].indexOf('-');
  2332. var a, b;
  2333. if (dash < 0) { a = b = parseInt(MD_UNICODE_PUNCT_RANGES[mid], 16); }
  2334. else {
  2335. a = parseInt(MD_UNICODE_PUNCT_RANGES[mid].slice(0, dash), 16);
  2336. b = parseInt(MD_UNICODE_PUNCT_RANGES[mid].slice(dash + 1), 16);
  2337. }
  2338. if (cp < a) hi = mid - 1;
  2339. else if (cp > b) lo = mid + 1;
  2340. else return true;
  2341. }
  2342. return false;
  2343. }
  2344. function mdIsPunct(ch) {
  2345. if (ch === '' || ch == null) return false;
  2346. if (MD_ASCII_PUNCT_TEST_RE.test(ch)) return true;
  2347. var cp = ch.charCodeAt(0);
  2348. if (cp < 0xA1) return false;
  2349. // combine a surrogate pair into one code point
  2350. if (cp >= 0xD800 && cp <= 0xDBFF && ch.length > 1) {
  2351. var lo = ch.charCodeAt(1);
  2352. if (lo >= 0xDC00 && lo <= 0xDFFF) cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
  2353. }
  2354. return mdIsUnicodePunct(cp);
  2355. }
  2356. /**
  2357. * String.fromCharCode with surrogate-pair support.
  2358. * @param {number} cp
  2359. * @returns {string}
  2360. */
  2361. function mdFromCodePoint(cp) {
  2362. if (cp == null || cp < 0 || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) return '\uFFFD';
  2363. if (cp <= 0xFFFF) return String.fromCharCode(cp);
  2364. var c = cp - 0x10000;
  2365. return String.fromCharCode(0xD800 + (c >> 10), 0xDC00 + (c & 0x3FF));
  2366. }
  2367. var mdEntityMap = null;
  2368. /** Parse MD_ENTITIES_PACKED into a name -> string map (once). */
  2369. function mdGetEntityMap() {
  2370. if (mdEntityMap) return mdEntityMap;
  2371. var map = {}, recs = MD_ENTITIES_PACKED.split('\n');
  2372. for (var i = 0; i < recs.length; i++) {
  2373. var sp = recs[i].indexOf(' ');
  2374. if (sp < 0) continue;
  2375. var cps = recs[i].slice(sp + 1).split(',');
  2376. var s = '';
  2377. for (var j = 0; j < cps.length; j++) s += mdFromCodePoint(parseInt(cps[j], 10));
  2378. map[recs[i].slice(0, sp)] = s;
  2379. }
  2380. mdEntityMap = map;
  2381. return map;
  2382. }
  2383. /**
  2384. * Resolve an entity reference at the start of `s`; null when invalid.
  2385. * @param {string} s
  2386. * @returns {?string}
  2387. */
  2388. function mdReplaceEntity(s) {
  2389. var m = s.match(MD_ENTITY_HERE_RE);
  2390. if (!m) return null;
  2391. var body = m[0].slice(1, -1);
  2392. if (body.charAt(0) === '#') {
  2393. var num = body.charAt(1) === 'x' || body.charAt(1) === 'X'
  2394. ? parseInt(body.slice(2), 16)
  2395. : parseInt(body.slice(1), 10);
  2396. if (isNaN(num) || num === 0 || num > 0x10FFFF) return '\uFFFD';
  2397. return mdFromCodePoint(num);
  2398. }
  2399. var map = mdGetEntityMap();
  2400. return Object.prototype.hasOwnProperty.call(map, body) ? map[body] : null;
  2401. }
  2402. /**
  2403. * Replace backslash escapes and entity references (spec 2.2/2.3). Used on
  2404. * link destinations, titles, labels and code info strings.
  2405. * @param {string} s
  2406. * @returns {string}
  2407. */
  2408. function mdUnescapeString(s) {
  2409. if (s.indexOf('\\') < 0 && s.indexOf('&') < 0) return s;
  2410. var out = '', i = 0, ch, rep, m;
  2411. while (i < s.length) {
  2412. ch = s.charAt(i);
  2413. if (ch === '\\' && MD_ESCAPABLE.indexOf(s.charAt(i + 1)) >= 0) {
  2414. out += s.charAt(i + 1);
  2415. i += 2;
  2416. continue;
  2417. }
  2418. if (ch === '&') {
  2419. // entity references are at most 34 characters long (spec 2.3), so a
  2420. // bounded window is enough and keeps this loop linear in s.length
  2421. rep = mdReplaceEntity(s.slice(i, i + 34));
  2422. if (rep !== null) {
  2423. m = s.slice(i, i + 34).match(MD_ENTITY_HERE_RE);
  2424. out += rep;
  2425. i += m[0].length;
  2426. continue;
  2427. }
  2428. }
  2429. out += ch;
  2430. i++;
  2431. }
  2432. return out;
  2433. }
  2434. // --- inline nodes -----------------------------------------------------------
  2435. /**
  2436. * Create an inline node. Types: text, softbreak, code, emph, strong,
  2437. * link, image, html (escaped text).
  2438. */
  2439. function mdInlineNode(type, literal) {
  2440. return { type: type, literal: literal == null ? '' : literal,
  2441. parent: null, next: null, prev: null,
  2442. firstChild: null, lastChild: null,
  2443. destination: null, title: null, hardBreak: false };
  2444. }
  2445. function mdAppendChild(parent, node) {
  2446. node.parent = parent;
  2447. node.prev = parent.lastChild;
  2448. if (parent.lastChild) parent.lastChild.next = node;
  2449. else parent.firstChild = node;
  2450. parent.lastChild = node;
  2451. return node;
  2452. }
  2453. function mdUnlink(node) {
  2454. if (node.prev) node.prev.next = node.next;
  2455. else if (node.parent) node.parent.firstChild = node.next;
  2456. if (node.next) node.next.prev = node.prev;
  2457. else if (node.parent) node.parent.lastChild = node.prev;
  2458. node.parent = null;
  2459. node.next = null;
  2460. node.prev = null;
  2461. }
  2462. function mdInsertAfter(node, sibling) {
  2463. sibling.parent = node.parent;
  2464. sibling.next = node.next;
  2465. sibling.prev = node;
  2466. if (node.next) node.next.prev = sibling;
  2467. else if (node.parent) node.parent.lastChild = sibling;
  2468. node.next = sibling;
  2469. }
  2470. /** Collect a node's children into an array (for rendering). */
  2471. function mdChildren(node) {
  2472. var out = [], c = node.firstChild;
  2473. while (c) { out.push(c); c = c.next; }
  2474. return out;
  2475. }
  2476. // --- the subject scanner ----------------------------------------------------
  2477. /**
  2478. * Parse the inline content of one leaf block.
  2479. * @param {string} subject already-trimmed paragraph/heading text
  2480. * @param {Object} refmap normalised-label -> { destination, title }
  2481. * @returns {Object} a synthetic parent whose children are the inlines
  2482. */
  2483. function mdParseInlines(subject, refmap) {
  2484. var block = { parent: null, next: null, prev: null,
  2485. firstChild: null, lastChild: null, type: 'root' };
  2486. var pos = 0;
  2487. var delimiters = null; // linked list, newest first
  2488. var brackets = null; // linked list, newest first
  2489. function text(s) { return mdAppendChild(block, mdInlineNode('text', s)); }
  2490. function peek() { return pos < subject.length ? subject.charAt(pos) : ''; }
  2491. function match(re) {
  2492. var m = re.exec(subject.slice(pos));
  2493. if (m) pos += m[0].length;
  2494. return m;
  2495. }
  2496. function spnl() {
  2497. match(/^ *(?:\n *)?/);
  2498. return true;
  2499. }
  2500. // -- emphasis delimiter bookkeeping --
  2501. function scanDelims(cc) {
  2502. var numdelims = 0, startpos = pos;
  2503. while (peek() === cc) { numdelims++; pos++; }
  2504. // adjacent characters, taking surrogate pairs as one character
  2505. var charBefore = startpos === 0 ? '\n'
  2506. : (mdIsHighSurrogate(subject.charAt(startpos - 1))
  2507. ? subject.slice(startpos - 2, startpos)
  2508. : subject.charAt(startpos - 1));
  2509. var nextCh = peek();
  2510. var charAfter = nextCh === '' ? '\n'
  2511. : (mdIsHighSurrogate(nextCh) ? subject.substr(pos, 2) : nextCh);
  2512. var afterWs = /^\s/.test(charAfter);
  2513. var beforeWs = /^\s/.test(charBefore);
  2514. var afterPunct = mdIsPunct(charAfter);
  2515. var beforePunct = mdIsPunct(charBefore);
  2516. var leftFlanking = !afterWs && (!afterPunct || beforeWs || beforePunct);
  2517. var rightFlanking = !beforeWs && (!beforePunct || afterWs || afterPunct);
  2518. var canOpen, canClose;
  2519. if (cc === '_') {
  2520. canOpen = leftFlanking && (!rightFlanking || beforePunct);
  2521. canClose = rightFlanking && (!leftFlanking || afterPunct);
  2522. } else {
  2523. canOpen = leftFlanking;
  2524. canClose = rightFlanking;
  2525. }
  2526. pos = startpos;
  2527. return { numdelims: numdelims, canOpen: canOpen, canClose: canClose };
  2528. }
  2529. function handleDelim(cc) {
  2530. var res = scanDelims(cc);
  2531. if (!res || res.numdelims === 0) return false;
  2532. var node = text(subject.slice(pos, pos + res.numdelims));
  2533. pos += res.numdelims;
  2534. if (res.canOpen || res.canClose) {
  2535. var d = { cc: cc, numdelims: res.numdelims, origdelims: res.numdelims,
  2536. node: node, previous: delimiters, next: null,
  2537. canOpen: res.canOpen, canClose: res.canClose };
  2538. if (delimiters) delimiters.next = d;
  2539. delimiters = d;
  2540. }
  2541. return true;
  2542. }
  2543. function removeDelimiter(d) {
  2544. if (d.previous) d.previous.next = d.next;
  2545. if (d.next === null) delimiters = d.previous;
  2546. else d.next.previous = d.previous;
  2547. }
  2548. function removeDelimitersBetween(bottom, top) {
  2549. if (bottom.next !== top) { bottom.next = top; top.previous = bottom; }
  2550. }
  2551. /** True for the high half of a UTF-16 surrogate pair. */
  2552. function mdIsHighSurrogate(ch) {
  2553. var c = ch.charCodeAt(0);
  2554. return c >= 0xD800 && c <= 0xDBFF;
  2555. }
  2556. /** Emphasis resolution over the delimiter stack (spec 6.2). */
  2557. function processEmphasis(stackBottom) {
  2558. var openersBottom = new Array(8);
  2559. for (var i = 0; i < 8; i++) openersBottom[i] = stackBottom;
  2560. var closer = delimiters;
  2561. while (closer !== null && closer.previous !== stackBottom) closer = closer.previous;
  2562. var opener, openerInl, closerInl, useDelims, tmp, next, openerFound, oddMatch;
  2563. while (closer !== null) {
  2564. if (!closer.canClose) { closer = closer.next; continue; }
  2565. opener = closer.previous;
  2566. openerFound = false;
  2567. // openers_bottom index: 0..3 for '_', 4..7 for '*' — see rule of 3
  2568. var idx = (closer.cc === '_' ? 0 : 4) + (closer.canOpen ? 2 : 0) + (closer.origdelims % 3 === 0 ? 0 : 1);
  2569. while (opener !== null && opener !== stackBottom && opener !== openersBottom[idx]) {
  2570. oddMatch = (closer.canOpen || opener.canClose) &&
  2571. closer.origdelims % 3 !== 0 &&
  2572. (opener.origdelims + closer.origdelims) % 3 === 0;
  2573. if (opener.cc === closer.cc && opener.canOpen && !oddMatch) { openerFound = true; break; }
  2574. opener = opener.previous;
  2575. }
  2576. var oldCloser = closer;
  2577. if (!openerFound) {
  2578. openersBottom[idx] = oldCloser.previous;
  2579. if (!oldCloser.canOpen) removeDelimiter(oldCloser);
  2580. closer = closer.next;
  2581. continue;
  2582. }
  2583. useDelims = closer.numdelims >= 2 && opener.numdelims >= 2 ? 2 : 1;
  2584. openerInl = opener.node;
  2585. closerInl = closer.node;
  2586. opener.numdelims -= useDelims;
  2587. closer.numdelims -= useDelims;
  2588. openerInl.literal = openerInl.literal.slice(0, openerInl.literal.length - useDelims);
  2589. closerInl.literal = closerInl.literal.slice(0, closerInl.literal.length - useDelims);
  2590. var emph = mdInlineNode(useDelims === 1 ? 'emph' : 'strong');
  2591. tmp = openerInl.next;
  2592. while (tmp && tmp !== closerInl) {
  2593. next = tmp.next;
  2594. mdUnlink(tmp);
  2595. mdAppendChild(emph, tmp);
  2596. tmp = next;
  2597. }
  2598. mdInsertAfter(openerInl, emph);
  2599. removeDelimitersBetween(opener, closer);
  2600. if (opener.numdelims === 0) { mdUnlink(openerInl); removeDelimiter(opener); }
  2601. if (closer.numdelims === 0) {
  2602. mdUnlink(closerInl);
  2603. var after = closer.next;
  2604. removeDelimiter(closer);
  2605. closer = after;
  2606. }
  2607. // when the closer still has delimiters left, stay on it: the next
  2608. // iteration searches for another opener further left
  2609. }
  2610. while (delimiters !== null && delimiters !== stackBottom) removeDelimiter(delimiters);
  2611. }
  2612. // -- link / image machinery --
  2613. /**
  2614. * Parse a link title at pos; returns the unescaped title or null.
  2615. * Uses the linear scanner (no regex backtracking).
  2616. */
  2617. function parseLinkTitle() {
  2618. var res = mdScanLinkTitle(subject, pos);
  2619. if (!res) return null;
  2620. pos += res.len;
  2621. return mdUnescapeString(res.text);
  2622. }
  2623. /** Parse `<dest>` or a bare destination; returns normalised URI or null. */
  2624. function parseLinkDestination() {
  2625. var m = match(MD_LINK_DEST_BRACES_RE);
  2626. if (m) return mdNormalizeUri(mdUnescapeString(m[0].slice(1, -1)));
  2627. if (peek() === '<') return null;
  2628. var savepos = pos, openparens = 0, c;
  2629. while ((c = peek()) !== '') {
  2630. if (c === '\\' && subject.charAt(pos + 1) !== '' &&
  2631. MD_ESCAPABLE.indexOf(subject.charAt(pos + 1)) >= 0) {
  2632. pos += 1;
  2633. if (peek() !== '') pos += 1;
  2634. } else if (c === '(') { pos++; openparens++; }
  2635. else if (c === ')') { if (openparens < 1) break; pos++; openparens--; }
  2636. else if (MD_WS_CHAR_RE.test(c)) break;
  2637. else pos++;
  2638. }
  2639. if (pos === savepos && c !== ')') return null;
  2640. if (openparens !== 0) { pos = savepos; return null; }
  2641. return mdNormalizeUri(mdUnescapeString(subject.slice(savepos, pos)));
  2642. }
  2643. /**
  2644. * Parse `[...]` at pos; returns the consumed length (including both
  2645. * brackets), or 0 when there is no well-formed label. A label may not
  2646. * contain a blank line and is capped at 1000 characters.
  2647. */
  2648. function parseLinkLabel() {
  2649. if (subject.charAt(pos) !== '[') return 0;
  2650. var i = pos + 1, ch;
  2651. while (i < subject.length) {
  2652. ch = subject.charAt(i);
  2653. if (ch === '\\') { i += 2; continue; }
  2654. if (ch === '[') return 0; // nested brackets are not labels
  2655. if (ch === ']') break;
  2656. i++;
  2657. }
  2658. if (subject.charAt(i) !== ']') return 0;
  2659. var len = i + 1 - pos;
  2660. var label = subject.slice(pos, i + 1);
  2661. if (len > 1001) return 0;
  2662. if (/\n[ \t]*\n/.test(label)) return 0; // no blank lines inside
  2663. return len;
  2664. }
  2665. function addBracket(node, index, image) {
  2666. if (brackets) brackets.bracketAfter = true;
  2667. brackets = { node: node, previous: brackets, previousDelimiter: delimiters,
  2668. index: index, image: image, active: true, bracketAfter: false };
  2669. }
  2670. function removeBracket() { brackets = brackets.previous; }
  2671. function parseCloseBracket() {
  2672. var startpos = pos, isImage, dest, title, matched = false, reflabel;
  2673. pos += 1;
  2674. var opener = brackets;
  2675. if (opener === null) { text(']'); return true; }
  2676. if (!opener.active) { text(']'); removeBracket(); return true; }
  2677. isImage = opener.image;
  2678. var savepos = pos;
  2679. if (peek() === '(') {
  2680. pos++;
  2681. if (spnl() && (dest = parseLinkDestination()) !== null) {
  2682. var beforeTitle = pos;
  2683. spnl();
  2684. // A title is only allowed when whitespace separated it from the
  2685. // destination (spec: "optional... preceded by a space").
  2686. if (pos !== beforeTitle) {
  2687. var tryTitle = parseLinkTitle();
  2688. if (tryTitle !== null) {
  2689. title = tryTitle;
  2690. spnl();
  2691. } else {
  2692. pos = beforeTitle;
  2693. }
  2694. }
  2695. if (peek() === ')') {
  2696. pos += 1;
  2697. matched = true;
  2698. } else {
  2699. pos = savepos;
  2700. }
  2701. } else {
  2702. pos = savepos;
  2703. }
  2704. }
  2705. if (!matched) {
  2706. var beforelabel = pos;
  2707. var n = parseLinkLabel();
  2708. if (n > 2) {
  2709. // full reference: [text][label]
  2710. reflabel = subject.slice(beforelabel, beforelabel + n);
  2711. pos = beforelabel + n;
  2712. } else if (n === 2 && !opener.bracketAfter) {
  2713. // collapsed reference: [text][] — consume the empty label
  2714. reflabel = '[' + subject.slice(opener.index, startpos) + ']';
  2715. pos = beforelabel + n;
  2716. } else if (!opener.bracketAfter) {
  2717. // shortcut reference: [text]
  2718. reflabel = '[' + subject.slice(opener.index, startpos) + ']';
  2719. }
  2720. if (n === 0) pos = savepos;
  2721. if (reflabel) {
  2722. var link = refmap[mdNormalizeReference(reflabel)];
  2723. if (link) { dest = link.destination; title = link.title; matched = true; }
  2724. }
  2725. }
  2726. if (matched) {
  2727. var node = mdInlineNode(isImage ? 'image' : 'link');
  2728. node.destination = dest == null ? '' : dest;
  2729. node.title = title == null ? '' : title;
  2730. var tmp = opener.node.next, next;
  2731. while (tmp) {
  2732. next = tmp.next;
  2733. mdUnlink(tmp);
  2734. mdAppendChild(node, tmp);
  2735. tmp = next;
  2736. }
  2737. // the link takes the opener's position in the parent's child list
  2738. mdInsertAfter(opener.node, node);
  2739. processEmphasis(opener.previousDelimiter);
  2740. removeBracket();
  2741. mdUnlink(opener.node);
  2742. if (!isImage) {
  2743. var b = brackets;
  2744. while (b !== null) { if (!b.image) b.active = false; b = b.previous; }
  2745. }
  2746. return true;
  2747. }
  2748. removeBracket();
  2749. // NB: `startpos` was captured before consuming ']', so pos now sits
  2750. // just after it — do not rewind onto the bracket again.
  2751. pos = startpos + 1;
  2752. text(']');
  2753. return true;
  2754. }
  2755. // -- autolinks and raw HTML (neutralised) --
  2756. var MD_EMAIL_AUTOLINK_RE = /^<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/;
  2757. var MD_AUTOLINK_RE = /^<[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*>/i;
  2758. /**
  2759. * Try an autolink at pos. Returns the node or null.
  2760. */
  2761. function parseAutolink() {
  2762. // Autolinks cannot contain whitespace or '<'/'>' (MD_AUTOLINK_RE), so
  2763. // slicing to the next such character bounds the scan without changing
  2764. // what can match. Without this bound the slice+match was quadratic in
  2765. // pathological input (thousands of unmatched '<').
  2766. var rest = subject.slice(pos, mdBoundScan(subject, pos));
  2767. var m = rest.match(MD_AUTOLINK_RE);
  2768. var node;
  2769. if (m) {
  2770. node = mdInlineNode('link');
  2771. node.destination = mdNormalizeUri(m[0].slice(1, -1));
  2772. node.autolinkText = m[0].slice(1, -1);
  2773. mdAppendChild(block, node);
  2774. mdAppendChild(node, mdInlineNode('text', m[0].slice(1, -1)));
  2775. pos += m[0].length;
  2776. return node;
  2777. }
  2778. m = rest.match(MD_EMAIL_AUTOLINK_RE);
  2779. if (m) {
  2780. node = mdInlineNode('link');
  2781. node.destination = 'mailto:' + m[1];
  2782. node.autolinkText = m[1];
  2783. mdAppendChild(block, node);
  2784. mdAppendChild(node, mdInlineNode('text', m[1]));
  2785. pos += m[0].length;
  2786. return node;
  2787. }
  2788. return null;
  2789. }
  2790. /**
  2791. * Match a raw HTML tag at pos. SECURITY: the matched text is returned so
  2792. * the caller can render it escaped — it is never emitted as markup.
  2793. */
  2794. function parseHtmlTag() {
  2795. var raw = mdScanHtmlTag(subject, pos);
  2796. if (!raw) return null;
  2797. var node = mdAppendChild(block, mdInlineNode('html'));
  2798. node.literal = raw;
  2799. pos += raw.length;
  2800. return node;
  2801. }
  2802. // -- the main dispatch loop --
  2803. while (pos < subject.length) {
  2804. var c = peek();
  2805. var res = false;
  2806. switch (c) {
  2807. case '\n':
  2808. pos++;
  2809. {
  2810. var lastc = block.lastChild;
  2811. if (lastc && lastc.type === 'text' && / $/.test(lastc.literal)) {
  2812. var hard = / {2}$/.test(lastc.literal);
  2813. lastc.literal = lastc.literal.replace(MD_FINAL_SPACE_RE, '');
  2814. var sb = mdAppendChild(block, mdInlineNode('softbreak'));
  2815. sb.hardBreak = hard;
  2816. } else {
  2817. mdAppendChild(block, mdInlineNode('softbreak'));
  2818. }
  2819. match(/^ */);
  2820. }
  2821. break;
  2822. case '\\':
  2823. if (subject.charAt(pos + 1) === '\n') {
  2824. pos += 2;
  2825. var hb = mdAppendChild(block, mdInlineNode('softbreak'));
  2826. hb.hardBreak = true;
  2827. match(/^ */);
  2828. } else if (subject.charAt(pos + 1) !== '' &&
  2829. MD_ESCAPABLE.indexOf(subject.charAt(pos + 1)) >= 0) {
  2830. text(subject.charAt(pos + 1));
  2831. pos += 2;
  2832. } else {
  2833. text('\\');
  2834. pos++;
  2835. }
  2836. break;
  2837. case '`':
  2838. {
  2839. // opening run, then the next run of backticks of the SAME length
  2840. var ticks = subject.slice(pos).match(MD_TICKS_HERE_RE)[0];
  2841. var afterOpen = pos + ticks.length;
  2842. var searchPos = afterOpen, run;
  2843. var codeNode = null;
  2844. while ((run = subject.slice(searchPos).match(/^[\s\S]*?(`+)/)) && run[1]) {
  2845. var runStart = searchPos + run[0].length - run[1].length;
  2846. if (run[1].length === ticks.length) {
  2847. // newline -> space; spaces *before* a newline are dropped
  2848. // (spec: "line endings are replaced by spaces... preceded
  2849. // by a space are removed")
  2850. var content = subject.slice(afterOpen, runStart)
  2851. .replace(/ *\n/g, '\n').replace(/\n/g, ' ');
  2852. if (/^ /.test(content) && / $/.test(content) && content.replace(/ /g, '') !== '') {
  2853. content = content.slice(1, content.length - 1);
  2854. }
  2855. codeNode = mdAppendChild(block, mdInlineNode('code', content));
  2856. pos = runStart + run[1].length;
  2857. break;
  2858. }
  2859. searchPos = runStart + run[1].length;
  2860. }
  2861. if (!codeNode) {
  2862. text(ticks);
  2863. pos = afterOpen;
  2864. }
  2865. }
  2866. break;
  2867. case '*':
  2868. case '_':
  2869. handleDelim(c);
  2870. break;
  2871. case '[':
  2872. {
  2873. var node = text('[');
  2874. addBracket(node, pos + 1, false);
  2875. pos++;
  2876. }
  2877. break;
  2878. case '!':
  2879. if (subject.charAt(pos + 1) === '[') {
  2880. var n2 = text('![');
  2881. addBracket(n2, pos + 2, true);
  2882. pos += 2;
  2883. } else {
  2884. text('!');
  2885. pos++;
  2886. }
  2887. break;
  2888. case ']':
  2889. parseCloseBracket();
  2890. break;
  2891. case '<':
  2892. if (parseAutolink()) break;
  2893. if (parseHtmlTag()) break;
  2894. text('<');
  2895. pos++;
  2896. break;
  2897. case '&':
  2898. {
  2899. var rep = mdReplaceEntity(subject.slice(pos, pos + 34));
  2900. if (rep !== null) {
  2901. text(rep);
  2902. pos += subject.slice(pos, pos + 34).match(MD_ENTITY_HERE_RE)[0].length;
  2903. } else {
  2904. text('&');
  2905. pos++;
  2906. }
  2907. }
  2908. break;
  2909. default:
  2910. {
  2911. var m = subject.slice(pos).match(/^[^\n`\\[\]!<&*_]+/);
  2912. if (!m) { text(c); pos++; }
  2913. else { text(m[0]); pos += m[0].length; }
  2914. }
  2915. }
  2916. }
  2917. processEmphasis(null);
  2918. return block;
  2919. }
  2920. // ---------------------------------------------------------------------------
  2921. // Markdown HTML rendering
  2922. // ---------------------------------------------------------------------------
  2923. /**
  2924. * Escape the five HTML-significant characters. `"` becomes `&quot;` (also
  2925. * safe inside double-quoted attributes), matching cmark.
  2926. * @param {string} s
  2927. * @returns {string}
  2928. */
  2929. function mdEscape(s) {
  2930. return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
  2931. .replace(/"/g, '&quot;');
  2932. }
  2933. /**
  2934. * Parse (once) and render a leaf block's inline content. The parsed inline
  2935. * tree is cached on the block so image alt text can reuse it.
  2936. */
  2937. function mdLeafInlines(node, refmap) {
  2938. if (!node._inlines) {
  2939. node._inlines = mdParseInlines(node.strings.join('\n').trim(), refmap);
  2940. }
  2941. return node._inlines;
  2942. }
  2943. /** Render inline nodes to HTML (children of a paragraph/heading). */
  2944. function mdRenderInlines(node) {
  2945. var out = '', c = node.firstChild;
  2946. while (c) {
  2947. switch (c.type) {
  2948. case 'text':
  2949. out += mdEscape(c.literal);
  2950. break;
  2951. case 'softbreak':
  2952. out += c.hardBreak ? '<br />\n' : '\n';
  2953. break;
  2954. case 'code':
  2955. out += '<code>' + mdEscape(c.literal) + '</code>';
  2956. break;
  2957. case 'emph':
  2958. out += '<em>' + mdRenderInlines(c) + '</em>';
  2959. break;
  2960. case 'strong':
  2961. out += '<strong>' + mdRenderInlines(c) + '</strong>';
  2962. break;
  2963. case 'link':
  2964. out += '<a href="' + safeMarkdownUrl(mdEscapeAttr(c.destination)) + '"' +
  2965. (c.title ? ' title="' + mdEscapeAttr(c.title) + '"' : '') + '>' +
  2966. mdRenderInlines(c) + '</a>';
  2967. break;
  2968. case 'image':
  2969. out += '<img src="' + safeMarkdownUrl(mdEscapeAttr(c.destination)) + '"' +
  2970. ' alt="' + mdImageAlt(c) + '"' +
  2971. (c.title ? ' title="' + mdEscapeAttr(c.title) + '"' : '') + ' />';
  2972. break;
  2973. case 'html':
  2974. out += mdEscape(c.literal);
  2975. break;
  2976. default:
  2977. out += mdEscape(c.literal || '');
  2978. }
  2979. c = c.next;
  2980. }
  2981. return out;
  2982. }
  2983. /** Plain-text rendering of image alt text (markdown stripped). */
  2984. function mdImageAlt(node) {
  2985. var out = '', c = node.firstChild;
  2986. while (c) {
  2987. if (c.type === 'text' || c.type === 'code' || c.type === 'html') out += c.literal;
  2988. else if (c.type === 'softbreak') out += c.hardBreak ? '\n' : '\n';
  2989. else if (c.firstChild) out += mdImageAlt(c);
  2990. c = c.next;
  2991. }
  2992. return mdEscapeAttr(out);
  2993. }
  2994. /** Escape for a double-quoted attribute value. */
  2995. function mdEscapeAttr(s) {
  2996. return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;')
  2997. .replace(/</g, '&lt;').replace(/>/g, '&gt;');
  2998. }
  2999. /**
  3000. * Output buffer that suppresses repeated newlines (cmark's `cr`).
  3001. */
  3002. function mdOut() {
  3003. // `last` starts as a newline so the very first cr() is a no-op
  3004. return { s: '', last: '\n' };
  3005. }
  3006. /** Append a literal string. */
  3007. function mdLit(out, s) {
  3008. if (!s) return;
  3009. out.s += s;
  3010. out.last = s.charAt(s.length - 1);
  3011. }
  3012. /** Append a newline unless the output already ends with one. */
  3013. function mdCr(out) {
  3014. if (out.last !== '\n') mdLit(out, '\n');
  3015. }
  3016. /**
  3017. * Render one block node (and its children) to HTML. `tight` is the
  3018. * enclosing list's tightness: tight lists suppress `<p>` wrappers.
  3019. */
  3020. function mdRenderBlocks(node, tight, out, refmap) {
  3021. var i, child;
  3022. switch (node.type) {
  3023. case 'document':
  3024. for (i = 0; i < node.children.length; i++) mdRenderBlocks(node.children[i], tight, out, refmap);
  3025. break;
  3026. case 'paragraph':
  3027. // in a tight list the paragraph renders bare
  3028. if (tight) { mdLit(out, mdRenderInlines(mdLeafInlines(node, refmap))); }
  3029. else {
  3030. mdCr(out);
  3031. mdLit(out, '<p>');
  3032. mdLit(out, mdRenderInlines(mdLeafInlines(node, refmap)));
  3033. mdLit(out, '</p>');
  3034. mdCr(out);
  3035. }
  3036. break;
  3037. case 'heading':
  3038. mdCr(out);
  3039. mdLit(out, '<h' + node.level + '>');
  3040. mdLit(out, mdRenderInlines(mdLeafInlines(node, refmap)));
  3041. mdLit(out, '</h' + node.level + '>');
  3042. mdCr(out);
  3043. break;
  3044. case 'thematic':
  3045. mdCr(out);
  3046. mdLit(out, '<hr />');
  3047. mdCr(out);
  3048. break;
  3049. case 'code':
  3050. mdCr(out);
  3051. var info = node.info ? node.info.split(/\s+/)[0] : '';
  3052. var attrs = '';
  3053. if (info) attrs = ' class="language-' + mdEscapeAttr(info) + '"';
  3054. mdLit(out, '<pre><code' + attrs + '>' + mdEscape(node.literal) + '</code></pre>');
  3055. mdCr(out);
  3056. break;
  3057. case 'blockquote':
  3058. mdCr(out);
  3059. mdLit(out, '<blockquote>');
  3060. mdCr(out);
  3061. // cmark unwraps paragraphs only when their grandparent is a tight
  3062. // list; inside a blockquote they always keep their <p>.
  3063. for (i = 0; i < node.children.length; i++) mdRenderBlocks(node.children[i], false, out, refmap);
  3064. mdCr(out);
  3065. mdLit(out, '</blockquote>');
  3066. mdCr(out);
  3067. break;
  3068. case 'list':
  3069. mdCr(out);
  3070. var tag = node.listData.type === 'ordered' ? 'ol' : 'ul';
  3071. var start = '';
  3072. if (tag === 'ol' && node.listData.start != null && node.listData.start !== 1) {
  3073. start = ' start="' + node.listData.start + '"';
  3074. }
  3075. mdLit(out, '<' + tag + start + '>');
  3076. mdCr(out);
  3077. for (i = 0; i < node.children.length; i++) mdRenderBlocks(node.children[i], node.tight, out, refmap);
  3078. mdCr(out);
  3079. mdLit(out, '</' + tag + '>');
  3080. mdCr(out);
  3081. break;
  3082. case 'item':
  3083. mdLit(out, '<li>');
  3084. for (i = 0; i < node.children.length; i++) {
  3085. // tightness applies to the item's own paragraphs and lists;
  3086. // blockquote contents are always loose.
  3087. mdRenderBlocks(node.children[i], node.children[i].type === 'blockquote' ? false : tight, out, refmap);
  3088. }
  3089. mdLit(out, '</li>');
  3090. mdCr(out);
  3091. break;
  3092. default:
  3093. break;
  3094. }
  3095. }
  3096. /**
  3097. * Render markdown to safe HTML.
  3098. *
  3099. * CommonMark 0.31.2 minus raw HTML (escaped instead, see the note at the
  3100. * top of the markdown section) and minus HTML-block constructs that only
  3101. * exist to feed raw HTML through.
  3102. * @param {string} raw markdown source
  3103. * @returns {string} HTML safe for `innerHTML`
  3104. */
  3105. function renderMarkdown(raw) {
  3106. var parser = mdParseDocument(raw);
  3107. var out = mdOut();
  3108. mdRenderBlocks(parser.doc, false, out, parser.refmap);
  3109. return out.s;
  3110. }
  3111. /**
  3112. * Demote child header tags by N levels (h1 → h2, etc.), capping at h6.
  3113. */
  3114. function handleHDemotion(el) {
  3115. var attr = el.getAttribute('stm-h-demotion');
  3116. if (!attr) return;
  3117. var n = parseInt(attr, 10);
  3118. if (isNaN(n) || n <= 0) return;
  3119. el.querySelectorAll('h1,h2,h3,h4,h5,h6').forEach(function (h) {
  3120. var level = parseInt(h.tagName.charAt(1), 10);
  3121. var newLevel = Math.min(level + n, 6);
  3122. if (newLevel === level) return;
  3123. var replacement = document.createElement('h' + newLevel);
  3124. for (var i = 0; i < h.attributes.length; i++)
  3125. replacement.setAttribute(h.attributes[i].name, h.attributes[i].value);
  3126. while (h.firstChild) replacement.appendChild(h.firstChild);
  3127. h.replaceWith(replacement);
  3128. });
  3129. }
  3130. /**
  3131. * Bind `stm-text` / `stm-html` / `stm-markdown`. Per the "leave original DOM
  3132. * content" policy, an unresolved value (`undefined`) leaves the element
  3133. * untouched; otherwise the value is stringified (`null` becomes empty).
  3134. */
  3135. function handleTextHtml(el, scope) {
  3136. if (el.hasAttribute('stm-text')) {
  3137. var text = evalExpr(el.getAttribute('stm-text'), scope);
  3138. if (text !== undefined) el.textContent = text === null ? '' : String(text);
  3139. }
  3140. if (el.hasAttribute('stm-html')) {
  3141. var html = evalExpr(el.getAttribute('stm-html'), scope);
  3142. if (html !== undefined) el.innerHTML = html === null ? '' : String(html);
  3143. }
  3144. if (el.hasAttribute('stm-markdown')) {
  3145. var md = evalExpr(el.getAttribute('stm-markdown'), scope);
  3146. if (md !== undefined) el.innerHTML = renderMarkdown(md === null ? '' : String(md));
  3147. }
  3148. // stm-h-demotion runs after stm-html / stm-markdown so the rendered
  3149. // headers are present in the DOM.
  3150. if (el.hasAttribute('stm-h-demotion')) handleHDemotion(el);
  3151. }
  3152. /**
  3153. * Expand a loop holder: reconcile its instances against the current array.
  3154. * Keyed loops (`stm-key`) reuse DOM nodes for unchanged keys to preserve
  3155. * focus/selection/input state; unkeyed loops rebuild wholesale.
  3156. */
  3157. function handleLoop(el, scope) {
  3158. var info = getStmFor(el);
  3159. if (!info) return;
  3160. var template = templates.get(el);
  3161. if (!template) {
  3162. template = el.cloneNode(true);
  3163. stripLoopAttrs(template);
  3164. templates.set(el, template);
  3165. }
  3166. var raw = evalExpr(info.expr, scope);
  3167. if (raw === undefined) {
  3168. // No value for the source path yet: clear instances and restore the
  3169. // server-rendered holder content (progressive enhancement).
  3170. var prev = instancesByHolder.get(el) || [];
  3171. prev.forEach(function (n) { n.remove(); });
  3172. instancesByHolder.set(el, []);
  3173. if (el.getAttribute('data-stm-active') === '1') {
  3174. el.removeAttribute('data-stm-active');
  3175. el.removeAttribute('hidden');
  3176. }
  3177. return;
  3178. }
  3179. var arr = Array.isArray(raw) ? raw : [];
  3180. // Activate: hide the holder so only rendered instances are visible.
  3181. if (el.getAttribute('data-stm-active') !== '1') {
  3182. el.setAttribute('data-stm-active', '1');
  3183. el.setAttribute('hidden', '');
  3184. }
  3185. var keyExpr = el.getAttribute('stm-key');
  3186. var next = [];
  3187. if (keyExpr) {
  3188. var oldByKey = new Map();
  3189. (instancesByHolder.get(el) || []).forEach(function (n) {
  3190. if (n.__stmKey !== undefined) oldByKey.set(n.__stmKey, n);
  3191. });
  3192. var used = new Set();
  3193. arr.forEach(function (item) {
  3194. var child = childScope(scope, info.varName, item);
  3195. var key = evalExpr(keyExpr, child);
  3196. var node = (key !== undefined && !used.has(key)) ? oldByKey.get(key) : undefined;
  3197. if (node) {
  3198. used.add(key);
  3199. processElement(node, child);
  3200. } else {
  3201. node = template.cloneNode(true);
  3202. node.__stmKey = key;
  3203. instanceNodes.add(node);
  3204. processElement(node, child);
  3205. }
  3206. next.push(node);
  3207. });
  3208. oldByKey.forEach(function (n, key) { if (!used.has(key)) n.remove(); });
  3209. } else {
  3210. (instancesByHolder.get(el) || []).forEach(function (n) { n.remove(); });
  3211. arr.forEach(function (item) {
  3212. var child = childScope(scope, info.varName, item);
  3213. var node = template.cloneNode(true);
  3214. instanceNodes.add(node);
  3215. processElement(node, child);
  3216. next.push(node);
  3217. });
  3218. }
  3219. // Place instances in order, immediately after the holder.
  3220. var ref = el;
  3221. next.forEach(function (node) {
  3222. if (node !== ref.nextSibling) ref.parentNode.insertBefore(node, ref.nextSibling);
  3223. ref = node;
  3224. });
  3225. instancesByHolder.set(el, next);
  3226. }
  3227. /**
  3228. * Process one element's bindings (and recurse). Loop holders are handled by
  3229. * {@link handleLoop}; `stm-else-if` / `stm-else` are owned by their `stm-if`.
  3230. */
  3231. function processElement(el, scope) {
  3232. if (getStmFor(el)) { handleLoop(el, scope); return; }
  3233. if (el.hasAttribute('stm-else-if') || el.hasAttribute('stm-else')) return;
  3234. if (el.hasAttribute('stm-if')) {
  3235. var active = handleIfChain(el, scope);
  3236. if (active) processInner(active, scope);
  3237. return;
  3238. }
  3239. processInner(el, scope);
  3240. }
  3241. /**
  3242. * Collect `stm-def-{name}-as` attributes on an element into a child scope.
  3243. * Each expression is evaluated against the incoming scope (so the new
  3244. * variables cannot reference each other within the same element), and the
  3245. * results are added to a child scope visible to the element and its
  3246. * descendants. Multiple defs on one element are supported. If there are no
  3247. * defs, the original scope is returned unchanged.
  3248. * @param {Element} el
  3249. * @param {object} scope
  3250. * @returns {object}
  3251. */
  3252. function applyDefs(el, scope) {
  3253. var attrs = el.attributes;
  3254. var child = null;
  3255. for (var i = 0; i < attrs.length; i++) {
  3256. var name = attrs[i].name;
  3257. if (name.indexOf('stm-def-') === 0 && name.slice(-3) === '-as') {
  3258. var varName = name.slice(8, -3);
  3259. if (!varName) continue;
  3260. if (!child) child = Object.create(scope || null);
  3261. child[varName] = evalExpr(attrs[i].value, scope);
  3262. }
  3263. }
  3264. return child || scope;
  3265. }
  3266. /** Bind the non-control-flow attributes and recurse into children. */
  3267. function processInner(el, scope) {
  3268. var s = applyDefs(el, scope); // local definitions: element + descendants
  3269. el.__stmScope = s; // latest scope, used when an action fires
  3270. handleClass(el, s);
  3271. handleAttribute(el, s);
  3272. handleDisabled(el, s);
  3273. handleTextHtml(el, s);
  3274. wireAction(el);
  3275. render(el, s);
  3276. }
  3277. /**
  3278. * Walk an element's children, processing each. Loop instance nodes are
  3279. * skipped here (they are owned and processed by their holder).
  3280. */
  3281. function render(root, scope) {
  3282. if (!root || !root.children) return;
  3283. // Snapshot the children: processing mutates the DOM (detaching branches,
  3284. // inserting loop instances) and a live HTMLCollection would shift indices.
  3285. var children = Array.prototype.slice.call(root.children);
  3286. for (var i = 0; i < children.length; i++) {
  3287. var el = children[i];
  3288. if (instanceNodes.has(el)) continue;
  3289. processElement(el, scope);
  3290. }
  3291. }
  3292. /** Re-render the whole document against the current state. */
  3293. /**
  3294. * Evaluate every `<link rel="stm-guard">` in the document head. A guard whose
  3295. * `require` predicate is false redirects the page to its `href`.
  3296. *
  3297. * Guards marked `pre-entrypoint` are evaluated on every cycle (including the
  3298. * initial, pre-entrypoint render, which uses only hydrated state); guards
  3299. * without it are evaluated only once the entrypoint has loaded. On every later
  3300. * evaluate cycle (set/clear) all guards are re-checked, so logging out, for
  3301. * example, can bounce the user off a guarded page.
  3302. */
  3303. function evaluateGuards() {
  3304. if (guardRedirected) return;
  3305. var scope = stateScope();
  3306. var guards = document.querySelectorAll('link[rel~="stm-guard"]');
  3307. for (var i = 0; i < guards.length; i++) {
  3308. var g = guards[i];
  3309. var preEntrypoint = g.hasAttribute('pre-entrypoint');
  3310. if (!preEntrypoint && !entrypointLoaded) continue;
  3311. var require = g.getAttribute('require');
  3312. if (!require) continue;
  3313. if (!evalExpr(require, scope)) {
  3314. guardRedirected = true;
  3315. var href = g.getAttribute('href') || '/';
  3316. console.warn('[statum] guard failed (' + require + '); redirecting to ' + href);
  3317. window.location.href = href;
  3318. return;
  3319. }
  3320. }
  3321. }
  3322. // ---------------------------------------------------------------------------
  3323. // Evaluation hooks (no-arg callbacks before/after each render cycle)
  3324. // ---------------------------------------------------------------------------
  3325. var beforeHooks = [];
  3326. var afterHooks = [];
  3327. function renderAll() {
  3328. if (guardRedirected) return;
  3329. beforeHooks.forEach(function (fn) { try { fn(); } catch (e) { console.error('[statum] beforeEvaluate hook error', e); } });
  3330. try { render(document.body, stateScope()); }
  3331. catch (e) { console.error('[statum] render error', e); }
  3332. afterHooks.forEach(function (fn) { try { fn(); } catch (e) { console.error('[statum] afterEvaluate hook error', e); } });
  3333. evaluateGuards();
  3334. }
  3335. // ---------------------------------------------------------------------------
  3336. // HTML API: actions
  3337. // ---------------------------------------------------------------------------
  3338. /**
  3339. * Collect data for an action trigger: `stm-data-{key}` attributes plus, for a
  3340. * form, its named inputs.
  3341. * @param {HTMLElement} el
  3342. * @returns {object}
  3343. */
  3344. function collectData(el) {
  3345. var data = {};
  3346. var attrs = el.attributes;
  3347. for (var i = 0; i < attrs.length; i++) {
  3348. var name = attrs[i].name;
  3349. if (name.indexOf('stm-data-') === 0) data[name.slice('stm-data-'.length)] = attrs[i].value;
  3350. }
  3351. if (el.tagName === 'FORM') {
  3352. var fd = new FormData(el);
  3353. fd.forEach(function (value, key) {
  3354. data[key] = value instanceof File ? value.name : String(value);
  3355. });
  3356. }
  3357. return data;
  3358. }
  3359. /**
  3360. * Toggle the busy state of an action element: disables the element and its
  3361. * descendant form controls and applies `stm-busy-class` classes while busy.
  3362. *
  3363. * On un-busy the disabled state is not "restored" to a captured snapshot
  3364. * (that would clobber a `stm-disabled` value that `renderAll` applied during
  3365. * the response); instead the nodes are re-enabled and `stm-disabled` is
  3366. * re-evaluated, which re-disables any that should remain disabled.
  3367. */
  3368. function setBusyState(el, busy) {
  3369. var nodes = [el].concat(Array.prototype.slice.call(
  3370. el.querySelectorAll('button,input,select,textarea,fieldset')));
  3371. var busyClasses = (el.getAttribute('stm-busy-class') || '').split(/\s+/).filter(Boolean);
  3372. if (busy) {
  3373. nodes.forEach(function (n) { n.setAttribute('disabled', ''); });
  3374. busyClasses.forEach(function (c) { el.classList.add(c); });
  3375. } else {
  3376. nodes.forEach(function (n) { n.removeAttribute('disabled'); });
  3377. busyClasses.forEach(function (c) { el.classList.remove(c); });
  3378. reapplyDisabled(el);
  3379. }
  3380. }
  3381. /** Re-apply `stm-disabled` to an element and any descendants that carry it. */
  3382. function reapplyDisabled(el) {
  3383. var scope = el.__stmScope || stateScope();
  3384. if (el.hasAttribute('stm-disabled')) handleDisabled(el, scope);
  3385. var scoped = el.querySelectorAll('[stm-disabled]');
  3386. Array.prototype.forEach.call(scoped, function (n) {
  3387. handleDisabled(n, n.__stmScope || scope);
  3388. });
  3389. }
  3390. /** Apply or remove `stm-on-error` classes on an action element. */
  3391. function applyErrorClasses(el, on) {
  3392. if (!el || !el.hasAttribute || !el.hasAttribute('stm-on-error')) return;
  3393. var classes = (el.getAttribute('stm-on-error') || '').split(/\s+/).filter(Boolean);
  3394. classes.forEach(function (c) { on ? el.classList.add(c) : el.classList.remove(c); });
  3395. }
  3396. /**
  3397. * Wire an element's primary action once. The listener resolves the action
  3398. * object against the element's latest render scope, so loop instances bind to
  3399. * their own item.
  3400. */
  3401. function wireAction(el) {
  3402. if (!el.hasAttribute('stm-action') || wired.has(el)) return;
  3403. wired.add(el);
  3404. var eventName = el.tagName === 'FORM' ? 'submit' : 'click';
  3405. el.addEventListener(eventName, function (event) {
  3406. event.preventDefault();
  3407. runElementAction(el);
  3408. });
  3409. }
  3410. /**
  3411. * Resolve and perform the action attached to an element, managing busy and
  3412. * error-state classes around the request.
  3413. * @param {HTMLElement} el
  3414. */
  3415. async function runElementAction(el) {
  3416. var scope = el.__stmScope || stateScope();
  3417. var action = evalExpr(el.getAttribute('stm-action'), scope);
  3418. if (!action || typeof action.uri !== 'string') return;
  3419. // stm-confirm: gate the action behind the confirm handler (async-capable,
  3420. // so custom modals are supported).
  3421. if (el.hasAttribute('stm-confirm')) {
  3422. var confirmed = await triggerConfirm(el.getAttribute('stm-confirm'), el);
  3423. if (!confirmed) return;
  3424. }
  3425. applyErrorClasses(el, false); // clear error state on a new attempt
  3426. var data = collectData(el);
  3427. setBusyState(el, true);
  3428. try {
  3429. await performAction(action, data, el);
  3430. } catch (err) {
  3431. // Already handled: stm-on-error classes and the onErrorHandler run inside
  3432. // performAction/reportFailure. Swallow so an HTML-triggered action does
  3433. // not surface as an unhandled rejection.
  3434. } finally {
  3435. setBusyState(el, false);
  3436. }
  3437. }
  3438. // ---------------------------------------------------------------------------
  3439. // Boot visibility (stm-preloader / stm-content)
  3440. // ---------------------------------------------------------------------------
  3441. /**
  3442. * Toggle pre/post-entrypoint visibility with the `hidden` attribute. While
  3443. * loading, `stm-preloader` elements are shown and `stm-content` elements
  3444. * hidden; once ready, the reverse.
  3445. *
  3446. * To avoid a flash of un-bound template before the script runs, authors may
  3447. * pre-set `hidden` directly on `stm-content` in the HTML (e.g.
  3448. * `<div stm-content hidden>`); the library removes it once ready. The library
  3449. * also sets `hidden` itself during loading as a fallback for authors who do
  3450. * not pre-set it.
  3451. * @param {boolean} ready
  3452. */
  3453. function setBootState(ready) {
  3454. var preloaders = document.querySelectorAll('[stm-preloader]');
  3455. var contents = document.querySelectorAll('[stm-content]');
  3456. for (var i = 0; i < preloaders.length; i++) {
  3457. ready ? preloaders[i].setAttribute('hidden', '') : preloaders[i].removeAttribute('hidden');
  3458. }
  3459. for (var j = 0; j < contents.length; j++) {
  3460. ready ? contents[j].removeAttribute('hidden') : contents[j].setAttribute('hidden', '');
  3461. }
  3462. }
  3463. // ---------------------------------------------------------------------------
  3464. // Public state accessors
  3465. // ---------------------------------------------------------------------------
  3466. /** Build the scope used for expression evaluation: `{ typeName: public }`. */
  3467. function stateScope() {
  3468. var scope = {};
  3469. store.forEach(function (entry, type) { scope[type] = entry.snapshot.public; });
  3470. return scope;
  3471. }
  3472. /** Read a type's `public` data (what the attributes bind to). */
  3473. function read(type) {
  3474. var entry = store.get(type);
  3475. return entry ? entry.snapshot.public : undefined;
  3476. }
  3477. /** Read a type's full snapshot object. */
  3478. function getSnapshot(type) {
  3479. var entry = store.get(type);
  3480. return entry ? entry.snapshot : undefined;
  3481. }
  3482. /** Read the frame wrapping a type's snapshot. */
  3483. function getFrame(type) {
  3484. var entry = store.get(type);
  3485. return entry ? entry.frame : undefined;
  3486. }
  3487. /** Build an object of `{ typeName: public }` for every held type. */
  3488. function state() {
  3489. var result = {};
  3490. store.forEach(function (entry, type) { result[type] = entry.snapshot.public; });
  3491. return result;
  3492. }
  3493. /**
  3494. * Clear state. With a type argument, clears that type only; without, clears
  3495. * all held state.
  3496. * @param {string} [type]
  3497. */
  3498. function clear(type) {
  3499. if (type === undefined) {
  3500. Array.from(store.keys()).forEach(function (t) { clearType(t, { broadcast: true }); });
  3501. } else {
  3502. clearType(type, { broadcast: true });
  3503. }
  3504. renderAll();
  3505. }
  3506. /**
  3507. * Perform an action. `action` may be an action object or a string path that
  3508. * resolves to an action object within the current state.
  3509. * @param {object|string} action
  3510. * @param {object} [data]
  3511. * @returns {Promise<void>}
  3512. */
  3513. function act(action, data) {
  3514. var resolved = action;
  3515. if (typeof action === 'string') resolved = evalExpr(action, stateScope());
  3516. if (!resolved || typeof resolved.uri !== 'string') {
  3517. return Promise.reject(new StatumError('Invalid action'));
  3518. }
  3519. return performAction(resolved, data, null);
  3520. }
  3521. /** Attach a state listener for a type. */
  3522. function addStateListener(type, callback) {
  3523. if (typeof callback !== 'function') return;
  3524. if (!listeners.has(type)) listeners.set(type, new Set());
  3525. listeners.get(type).add(callback);
  3526. }
  3527. /** Remove a previously-attached state listener. */
  3528. function removeStateListener(type, callback) {
  3529. var set = listeners.get(type);
  3530. if (!set) return;
  3531. set.delete(callback);
  3532. if (set.size === 0) listeners.delete(type);
  3533. }
  3534. // ---------------------------------------------------------------------------
  3535. // Initialization
  3536. // ---------------------------------------------------------------------------
  3537. /**
  3538. * Inject the stylesheet that makes the `hidden` attribute authoritative.
  3539. * Statum toggles `stm-if`/`stm-else` branches and loop holders via
  3540. * `hidden`, whose user-agent rule loses specificity battles to ordinary
  3541. * author CSS (a rule like `li { display: flex }` let hidden loop holders
  3542. * leak as phantom rows). Idempotent: skipped when the stylesheet is
  3543. * already present.
  3544. */
  3545. function injectHiddenStyle() {
  3546. var id = 'statum-hidden-style';
  3547. if (document.getElementById(id)) return;
  3548. var head = document.head;
  3549. if (!head) return;
  3550. var style = document.createElement('style');
  3551. style.id = id;
  3552. style.textContent = '[hidden]{display:none !important}';
  3553. head.appendChild(style);
  3554. }
  3555. /**
  3556. * Scan the document for `pstm-*` attributes and `<pstm-*>` elements, which
  3557. * are intended for server-side pre-rendering and should never reach the
  3558. * browser. Emits a single `console.warn` summarizing them.
  3559. */
  3560. function warnServerSideMarkup() {
  3561. var found = [];
  3562. var els = document.querySelectorAll('*');
  3563. for (var i = 0; i < els.length; i++) {
  3564. var el = els[i];
  3565. var tag = el.tagName.toLowerCase();
  3566. if (tag.indexOf('pstm-') === 0) found.push('<' + tag + '>');
  3567. var attrs = el.attributes;
  3568. for (var j = 0; j < attrs.length; j++) {
  3569. var name = attrs[j].name;
  3570. if (name.indexOf('pstm-') === 0) found.push('<' + tag + '> ' + name);
  3571. }
  3572. }
  3573. if (found.length) {
  3574. console.warn('[statum] ' + found.length + ' server-side pstm-* attribute/element(s) ' +
  3575. 'found in the DOM; they should have been resolved during pre-rendering: ' +
  3576. found.slice(0, 10).join(', ') + (found.length > 10 ? ' …' : ''));
  3577. }
  3578. }
  3579. /**
  3580. * Initialize Statum: read URL overrides, run the session-cookie check,
  3581. * hydrate persisted state, open the cross-tab channel, fetch the entrypoint,
  3582. * and render. Safe to call multiple times.
  3583. * @returns {Promise<void>}
  3584. */
  3585. async function init() {
  3586. if (initialized) return;
  3587. initialized = true;
  3588. // Before the first render and the boot visibility toggling, so `hidden`
  3589. // wins over author CSS from the very first paint.
  3590. injectHiddenStyle();
  3591. var body = document.body;
  3592. if (body) {
  3593. if (body.hasAttribute('stm-entrypoint')) config.entrypointUrl = body.getAttribute('stm-entrypoint');
  3594. if (body.hasAttribute('stm-slots')) config.slotsUrl = body.getAttribute('stm-slots');
  3595. if (body.hasAttribute('stm-channel')) config.channelUrl = body.getAttribute('stm-channel');
  3596. if (body.hasAttribute('stm-worker')) config.workerUrl = body.getAttribute('stm-worker');
  3597. }
  3598. try { warnServerSideMarkup(); } catch (e) {}
  3599. try { initSession(); } catch (e) {}
  3600. try { hydrate(); } catch (e) {}
  3601. setupChannel();
  3602. setBootState(false);
  3603. renderAll(); // bind against any persisted/hydrated state immediately
  3604. try {
  3605. await loadEntrypoint();
  3606. } catch (err) {
  3607. console.error('[statum] entrypoint load failed', err);
  3608. } finally {
  3609. setBootState(true);
  3610. }
  3611. }
  3612. function autoInit() {
  3613. if (document.readyState === 'loading') {
  3614. document.addEventListener('DOMContentLoaded', init);
  3615. } else {
  3616. init();
  3617. }
  3618. }
  3619. // ---------------------------------------------------------------------------
  3620. // Public API
  3621. // ---------------------------------------------------------------------------
  3622. /** @namespace statum */
  3623. window.statum = {
  3624. /** Library version. */
  3625. version: '0.1.0',
  3626. /** Mutable endpoint configuration. */
  3627. config: config,
  3628. /** (Re)initialize the library. Usually runs automatically on load. */
  3629. init: init,
  3630. /** Read a type's `public` data. */
  3631. read: read,
  3632. /** Read a type's full snapshot object. */
  3633. getSnapshot: getSnapshot,
  3634. /** Read the frame wrapping a type's snapshot. */
  3635. getFrame: getFrame,
  3636. /** Build `{ typeName: public }` for every held type. */
  3637. state: state,
  3638. /** Clear one type (`clear("x")`) or all state (`clear()`). */
  3639. clear: clear,
  3640. /** Perform an action object or named action. */
  3641. act: act,
  3642. /** Attach a state-changed listener for a type. */
  3643. addStateListener: addStateListener,
  3644. /** Remove a previously-attached listener. */
  3645. removeStateListener: removeStateListener,
  3646. /** Register a callback that runs before each render cycle (no arguments). */
  3647. beforeEvaluate: function (fn) { if (typeof fn === 'function') beforeHooks.push(fn); },
  3648. /** Register a callback that runs after each render cycle (no arguments). */
  3649. afterEvaluate: function (fn) { if (typeof fn === 'function') afterHooks.push(fn); },
  3650. /** The Statum error type (thrown by `act` and surfaced to handlers). */
  3651. Error: StatumError,
  3652. get onConfirmHandler() { return handlers.onConfirm; },
  3653. set onConfirmHandler(v) { handlers.onConfirm = typeof v === 'function' ? v : null; },
  3654. get onNotifyHandler() { return handlers.onNotify; },
  3655. set onNotifyHandler(v) { handlers.onNotify = typeof v === 'function' ? v : null; },
  3656. get onErrorHandler() { return handlers.onError; },
  3657. set onErrorHandler(v) { handlers.onError = typeof v === 'function' ? v : null; }
  3658. };
  3659. autoInit();
  3660. // Test-only escape hatch: tests/markdown loads this file under QuickJS with
  3661. // `window.__STATUM_TEST__` set beforehand and reaches the markdown engine
  3662. // through statum._testMarkdown. Shipped pages never set the flag, so this
  3663. // adds no surface to the public API.
  3664. if (window.__STATUM_TEST__) window.statum._testMarkdown = renderMarkdown;
  3665. })();