mnfst 0.5.162 → 0.5.164

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,28 +1,19 @@
1
1
  /* Manifest Markdown */
2
2
 
3
- // Cache for marked.js loading
4
3
  let markedPromise = null;
5
4
 
6
- // Cache for fetched markdown files to prevent duplicate requests
7
5
  const markdownCache = new Map();
8
6
 
9
- // Invalidate the markdown fetch cache when mnfst-run signals a data file
10
- // changed on disk. Without this, a saved .md file is re-read by the data
11
- // plugin but x-markdown still serves the old content from cache; combined
12
- // with the lastProcessedContent short-circuit in the directive's effect,
13
- // the article appears blank until the user manually reloads.
7
+ // Invalidate the fetch cache on dev-reload, else a saved .md serves stale
8
+ // cached content (and the lastProcessedContent short-circuit leaves it blank).
14
9
  if (typeof window !== 'undefined') {
15
10
  window.addEventListener('manifest:dev-reload', () => {
16
11
  markdownCache.clear();
17
12
  });
18
13
  }
19
14
 
20
- // DOMPurify config tuned for Manifest's markdown output. The markdown
21
- // extensions emit <x-icon> custom elements and `x-*` directive attributes that must
22
- // survive sanitization, so custom-element handling is enabled with a
23
- // tag-name allowlist (x-*) and an attribute filter that rejects event
24
- // handlers (on*). DOMPurify's defaults handle <script>, javascript: URLs,
25
- // srcdoc, and the usual XSS vectors for standard HTML tags.
15
+ // DOMPurify config for Manifest markdown: allow x-* custom elements and
16
+ // directive attributes to survive, but reject on* event handlers.
26
17
  const MARKDOWN_PURIFY_CONFIG = {
27
18
  CUSTOM_ELEMENT_HANDLING: {
28
19
  tagNameCheck: /^x-[a-z][\w-]*$/,
@@ -31,10 +22,8 @@ const MARKDOWN_PURIFY_CONFIG = {
31
22
  }
32
23
  };
33
24
 
34
- // DOMPurify loader is defined on window by whichever of svg/markdown loads
35
- // first (see manifest.svg.js). Declaring `let purifyPromise` here at top
36
- // level collides with svg.js's identical declaration in the realm's shared
37
- // global lexical environment, so we use the shared loader instead.
25
+ // Shared DOMPurify loader on window (svg.js or markdown, whichever loads first).
26
+ // A top-level `let purifyPromise` would collide with svg.js's identical one.
38
27
  if (!window.ManifestDOMPurify) {
39
28
  window.ManifestDOMPurify = {
40
29
  _promise: null,
@@ -43,10 +32,7 @@ if (!window.ManifestDOMPurify) {
43
32
  if (this._promise) return this._promise;
44
33
  this._promise = new Promise((resolve, reject) => {
45
34
  const script = document.createElement('script');
46
- // Pinned + Subresource Integrity: a moving `@latest` (or a
47
- // tampered CDN file) would otherwise run arbitrary JS in the
48
- // user's page. The browser rejects the script if the bytes don't
49
- // match the hash. Bump version AND integrity together.
35
+ // Pinned + SRI (browser rejects on hash mismatch). Bump version AND integrity together.
50
36
  script.src = 'https://cdn.jsdelivr.net/npm/dompurify@3.4.10/dist/purify.min.js';
51
37
  script.integrity = 'sha384-eguRoJERj8ghOpzO//Rl7+ScQsQIR1cH+ajll7+fG+IpbNPlkZsQn9h8ccr+wPXx';
52
38
  script.crossOrigin = 'anonymous';
@@ -69,19 +55,15 @@ if (!window.ManifestDOMPurify) {
69
55
  };
70
56
  }
71
57
 
72
- // Sanitize HTML if the .safe modifier was used; pass-through otherwise.
73
- // Manifest's default is unsanitized so authors can render arbitrary HTML and
74
- // the markdown custom-element extensions work — but the .safe opt-in lets
75
- // authors render data-source content (e.g. user-submitted markdown from
76
- // Appwrite) without an XSS sink.
58
+ // Sanitize HTML when .safe is set; pass-through otherwise (default is
59
+ // unsanitized so custom-element extensions work). Use .safe for untrusted source.
77
60
  async function maybeSanitizeMarkdownHtml(html, safe) {
78
61
  if (!safe) return html;
79
62
  try {
80
63
  const DOMPurify = await window.ManifestDOMPurify.load();
81
64
  return DOMPurify.sanitize(html, MARKDOWN_PURIFY_CONFIG);
82
65
  } catch {
83
- // Loader failure — fall back to escaping rather than silently emitting
84
- // un-sanitized HTML. The author asked for safe; honour that.
66
+ // Loader failure — escape rather than emit un-sanitized HTML (.safe was asked for).
85
67
  const escaped = String(html)
86
68
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
87
69
  console.warn('[Manifest Markdown] x-markdown.safe: DOMPurify unavailable — emitting escaped text.');
@@ -102,8 +84,7 @@ async function loadMarkedJS() {
102
84
 
103
85
  markedPromise = new Promise((resolve, reject) => {
104
86
  const script = document.createElement('script');
105
- // Pinned + Subresource Integrity (see DOMPurify loader above) — a
106
- // floating version or tampered CDN file can't inject arbitrary JS.
87
+ // Pinned + SRI (see DOMPurify loader above).
107
88
  script.src = 'https://cdn.jsdelivr.net/npm/marked@15.0.12/marked.min.js';
108
89
  script.integrity = 'sha384-948ahk4ZmxYVYOc+rxN1H2gM1EJ2Duhp7uHtZ4WSLkV4Vtx5MUqnV+l7u9B+jFv+';
109
90
  script.crossOrigin = 'anonymous';
@@ -128,10 +109,8 @@ async function loadMarkedJS() {
128
109
  return markedPromise;
129
110
  }
130
111
 
131
- // HTML-escape a string for safe interpolation inside an attribute value.
132
- // Used by the code-fence renderer below title/language strings come from
133
- // the markdown source, so without escaping a fence like ```js " onclick=alert(1) x="
134
- // could inject arbitrary attributes onto the <pre> element.
112
+ // HTML-escape for safe interpolation in an attribute value — fence title/language
113
+ // strings come from source and could otherwise inject attributes onto the <pre>.
135
114
  function escapeForAttribute(s) {
136
115
  return String(s)
137
116
  .replace(/&/g, '&amp;')
@@ -140,9 +119,7 @@ function escapeForAttribute(s) {
140
119
  .replace(/>/g, '&gt;');
141
120
  }
142
121
 
143
- // Escape a literal HTML fragment so it displays as source text when placed
144
- // inside a <code> element. Used for the ::: frame demo modifier, where the
145
- // same content is rendered live AND shown below as its own source.
122
+ // Escape a literal HTML fragment to display as source text inside a <code>.
146
123
  function escapeForText(s) {
147
124
  return String(s)
148
125
  .replace(/&/g, '&amp;')
@@ -154,10 +131,8 @@ function escapeForText(s) {
154
131
  async function configureMarked(marked) {
155
132
  marked.use({
156
133
  renderer: {
157
- // Render fenced code blocks as <pre x-code="…"><code>…</code></pre>.
158
- // The code plugin's directive then handles highlighting, copy
159
- // buttons, collapse, line numbers, etc. — same code path whether
160
- // the block was authored in HTML or markdown.
134
+ // Render fenced blocks as <pre x-code><code>…</code></pre>; the code
135
+ // plugin handles highlighting/copy/collapse/lines from there.
161
136
  code(token) {
162
137
  const lang = token.lang || '';
163
138
  const text = token.text || '';
@@ -165,8 +140,7 @@ async function configureMarked(marked) {
165
140
  const attrs = parseLanguageString(lang);
166
141
 
167
142
  let preAttrs = '';
168
- // x-code carries the language as its value (empty string for
169
- // "no explicit language; auto-detect")
143
+ // x-code value = language ('' means auto-detect)
170
144
  preAttrs += ` x-code="${attrs.language ? escapeForAttribute(attrs.language) : ''}"`;
171
145
  if (attrs.title) preAttrs += ` name="${escapeForAttribute(attrs.title)}"`;
172
146
  if (attrs.lines) preAttrs += ' lines';
@@ -179,13 +153,8 @@ async function configureMarked(marked) {
179
153
  }
180
154
  if (attrs.from) preAttrs += ` from="${escapeForAttribute(attrs.from)}"`;
181
155
 
182
- // Escape the fence body before injection. Newer marked versions
183
- // pass `token.text` raw, and if we leave it unescaped an HTML
184
- // fence like ```html <script>…</script>``` becomes a live
185
- // element in the document instead of source text. Escaping
186
- // here keeps the <code> body as pure text — the code plugin's
187
- // resolveSource reads textContent which decodes the entities
188
- // back to the original source.
156
+ // Escape the fence body so an HTML fence stays source text, not a
157
+ // live element (the code plugin reads textContent back to source).
189
158
  return `<pre${preAttrs}><code>${escapeForText(text)}</code></pre>\n`;
190
159
  }
191
160
  },
@@ -207,12 +176,9 @@ async function configureMarked(marked) {
207
176
  const openMatch = src.match(/^:::(.*?)(?:\n|$)/);
208
177
  if (!openMatch) return;
209
178
 
210
- // Parse the opening line for classes, icon, and an optional
211
- // quoted name. The name follows the same convention as the
212
- // fenced-code info-string (`::: frame "header.html"`) — it
213
- // becomes the `name` attribute on the rendered <aside>, which
214
- // lets the code plugin pair the frame with a fenced block
215
- // sharing the same name inside an <x-code-group>.
179
+ // Parse the opening line for classes, icon, and an optional quoted
180
+ // name (`::: frame "header.html"`) `name` attr on the <aside>,
181
+ // used to pair with a same-named fence inside an <x-code-group>.
216
182
  const openingLine = openMatch[1].trim();
217
183
  let classes = '';
218
184
  let iconValue = '';
@@ -259,34 +225,27 @@ async function configureMarked(marked) {
259
225
  const iconValue = token.iconValue || '';
260
226
  const nameValue = token.nameValue || '';
261
227
 
262
- // `::: frame demo` — render the frame contents live AND emit
263
- // a sibling <pre x-code="html"> showing the same source. Lets
264
- // authors write the example once and have it both rendered
265
- // and documented. Strip `demo` from the class list so the
266
- // resulting <aside> has just `frame`.
228
+ // `::: frame demo` — render the frame live AND emit a sibling
229
+ // <pre x-code="html"> of the same source. Strip `demo` from classes.
267
230
  const isDemo = /\bframe\b/.test(classes) && /\bdemo\b/.test(classes);
268
231
  if (isDemo) classes = classes.replace(/\bdemo\b/, '').replace(/\s+/g, ' ').trim();
269
232
 
270
- // For frame callouts, don't parse as markdown to avoid wrapping HTML in <p> tags
233
+ // Frame callouts keep raw HTML (parsing would wrap it in <p>).
271
234
  let parsedContent;
272
235
  if (classes.includes('frame')) {
273
- // Use raw content for frame callouts to preserve HTML structure
274
236
  parsedContent = token.text;
275
237
  } else {
276
- // Parse the content as markdown to support nested markdown syntax
277
238
  parsedContent = marked.parse(token.text);
278
239
  }
279
240
 
280
241
  const iconHtml = iconValue ? `<span x-icon="${escapeForAttribute(iconValue)}"></span>` : '';
281
242
 
282
- // Create a temporary div to count top-level elements
243
+ // Count top-level elements
283
244
  const temp = document.createElement('div');
284
245
  temp.innerHTML = parsedContent;
285
246
  const elementCount = temp.children.length;
286
247
 
287
- // Only wrap in a div if:
288
- // 1. There are 2 or more elements AND
289
- // 2. There's an icon (which needs the content to be wrapped as a sibling)
248
+ // Wrap only when there's an icon plus 2+ elements (icon needs a sibling wrapper).
290
249
  const needsWrapper = elementCount >= 2 && iconValue;
291
250
  const wrappedContent = needsWrapper ?
292
251
  `<div>${parsedContent}</div>` :
@@ -309,11 +268,8 @@ async function configureMarked(marked) {
309
268
  });
310
269
  }
311
270
 
312
- // Markdown preprocessor: ensure that block-level HTML containers (the
313
- // wrappers authors use to group fenced code blocks into tabs, frames, etc.)
314
- // have a blank line after their opening tag so marked treats the contents
315
- // as block-level markdown rather than raw inline HTML. Without this, a
316
- // fenced ```js immediately after `<div x-code-group>` is treated as text.
271
+ // Insert a newline after block HTML containers (x-code-group wrappers) so marked
272
+ // treats their contents as block markdown rather than raw inline HTML.
317
273
  function renderXCodeGroup(markdown) {
318
274
  return markdown.replace(
319
275
  /(<(?:div|section|article|aside)[^>]*\bx-code-group\b[^>]*>)(?!\s*\n)/g,
@@ -321,13 +277,11 @@ function renderXCodeGroup(markdown) {
321
277
  );
322
278
  }
323
279
 
324
- // Post-process HTML to enable checkboxes by removing disabled attribute
280
+ // Enable task-list checkboxes by removing marked's disabled attribute.
325
281
  function enableCheckboxes(html) {
326
- // Create a temporary DOM element to parse the HTML
327
282
  const temp = document.createElement('div');
328
283
  temp.innerHTML = html;
329
284
 
330
- // Find all checkbox inputs and remove disabled attribute
331
285
  const checkboxes = temp.querySelectorAll('input[type="checkbox"]');
332
286
  checkboxes.forEach(checkbox => {
333
287
  checkbox.removeAttribute('disabled');
@@ -336,34 +290,15 @@ function enableCheckboxes(html) {
336
290
  return temp.innerHTML;
337
291
  }
338
292
 
339
- // Apply trailing `{…}` attribute lists to inline `<code>` elements emitted
340
- // by marked. Authors write ``` `npm i mnfst`{copy} ``` or ``` `code`{bash copy} ```
341
- // in markdown; marked's default codespan handling drops the trailing brace
342
- // block as literal text. We rewrite it post-parse so the code plugin's
343
- // directive sees the attributes and wires copy / syntax highlighting.
344
- //
345
- // Supported tokens inside the braces:
346
- // copy → adds the `copy` attribute (click-to-copy)
347
- // <language> → bareword like `bash`, `js`, `html` becomes
348
- // the `x-code="…"` value (drives highlighting)
349
- // .class → appended to the element's class list
350
- // key=value, key="quoted" → arbitrary attribute (rarely needed)
351
- //
352
- // Multiple tokens separate by whitespace: `cmd`{bash copy} works.
293
+ // Rewrite marked codespans with a trailing `{…}` attribute list into real
294
+ // attributes (`` `cmd`{bash copy} `` `<code x-code="bash" copy>`), so the code
295
+ // plugin wires copy / highlighting. Tokens: `copy`/`lines`/`edit` flags, a bare
296
+ // language word, `.class`, and `key=value`.
353
297
  function applyInlineCodeAttributes(html) {
354
- // marked emits `<code>…</code>` for codespans (no attributes). When we see
355
- // `<code>X</code>{tokens}` we rewrite into `<code x-code[=lang] tokens>X</code>`.
356
- // Be conservative: only rewrite when the brace block immediately follows
357
- // a `<code>` close tag (no whitespace), so prose like "foo `bar` {note}" is
358
- // untouched.
359
- //
360
- // Body capture is `[^<]*` (not `[\s\S]*?`) so the match can't span across
361
- // intermediate `<` characters — without this guard, an unmatched
362
- // `<code>foo</code>` followed later by `<code>bar</code>{copy}` would be
363
- // captured as ONE big match with body = "foo</code><…><code>bar". That
364
- // bug surfaces noticeably in tables, where multiple codespans per row mix
365
- // marked and unmarked instances. Marked HTML-escapes any literal `<` in
366
- // codespan content to `&lt;`, so the restriction is safe.
298
+ // Only rewrite when `{…}` immediately follows a `</code>` (no whitespace).
299
+ // Body capture is `[^<]*` (not `[\s\S]*?`) so it can't span `<` — without this
300
+ // an unmatched codespan could be swallowed into a later match (notably in
301
+ // tables). Marked escapes literal `<` in codespans, so this is safe.
367
302
  return html.replace(
368
303
  /<code>([^<]*)<\/code>\{([^}\n]+)\}/g,
369
304
  (_, body, attrString) => {
@@ -385,14 +320,8 @@ function applyInlineCodeAttributes(html) {
385
320
  language = tok;
386
321
  }
387
322
  }
388
- // Only emit x-code when the author actually requested a language.
389
- // `inline`{copy} should produce `<code copy>` copy is a UI flag,
390
- // not a request for syntax highlighting. Previously we always
391
- // wrote `x-code=""`, which the code plugin treated as auto-detect
392
- // and ran hljs.highlightElement on the codespan, colouring
393
- // identifiers like `x-code` themselves as tokens. Authors who
394
- // want highlighting opt in explicitly via `code`{bash} or by
395
- // hand-writing `<code x-code="bash">…</code>`.
323
+ // Only emit x-code when a language was given `foo`{copy} is copy-only,
324
+ // not a highlight request (an empty x-code would auto-detect and mis-colour).
396
325
  let attrs = '';
397
326
  if (language) attrs += ` x-code="${escapeForAttribute(language)}"`;
398
327
  for (const flag of flags) attrs += ` ${flag}`;
@@ -511,10 +440,8 @@ async function initializeMarkdownPlugin() {
511
440
  // Check if there are any elements with x-markdown already on the page
512
441
  const existingMarkdownElements = document.querySelectorAll('[x-markdown]');
513
442
 
514
- // Detect whether this page was produced by the prerender. On
515
- // prerendered pages, x-markdown elements arrive with their content
516
- // already rendered to HTML — we must NOT hide them on init or the
517
- // user sees a flash of empty content while the plugin re-fetches.
443
+ // Prerendered pages arrive with x-markdown content already rendered
444
+ // don't hide on init or the user sees a flash of empty content.
518
445
  const isPrerenderedPage = !!(
519
446
  document.querySelector('meta[name="manifest:prerendered"]') &&
520
447
  document.querySelector('meta[name="manifest:prerendered"]').getAttribute('content') !== '0'
@@ -528,25 +455,17 @@ async function initializeMarkdownPlugin() {
528
455
  return;
529
456
  }
530
457
 
531
- // Opt-in sanitization. When `.safe` is on the directive
532
- // (`x-markdown.safe="$x.user.bio"`), parsed HTML is run through
533
- // DOMPurify before injection. Default is unsanitized — Manifest's
534
- // design lets authors render raw HTML and custom-element extensions
535
- // (x-icon, callouts) and directive attributes (x-code, etc.) freely.
536
- // Use .safe when the markdown
537
- // source can contain content from untrusted parties (Appwrite
538
- // collections, API responses, crowdsourced translations, etc.).
458
+ // `.safe` runs parsed HTML through DOMPurify before injection. Default
459
+ // is unsanitized (raw HTML + custom-element extensions); use .safe for
460
+ // untrusted source (Appwrite, API responses, crowdsourced translations).
539
461
  const safe = Array.isArray(modifiers) && modifiers.includes('safe');
540
462
 
541
- // Prerender idempotency: if the page is a prerendered MPA and this
542
- // element already has rendered HTML children, the content was baked
543
- // at build time and is authoritative for SEO + no-JS users. Skip
544
- // the initial hide-and-re-render step entirely. We still register
545
- // the reactive effect below so the content can update if its
546
- // expression is dynamic and later changes (e.g. via $route).
463
+ // Prerender idempotency: baked HTML children are build-time authoritative
464
+ // skip the initial hide-and-re-render, but still register the effect
465
+ // below so a dynamic expression can still update.
547
466
  const hasBakedContent = isPrerenderedPage && el.innerHTML && el.innerHTML.trim() !== '';
548
467
  if (!hasBakedContent) {
549
- // Hide element initially to prevent flicker (live SPA behaviour)
468
+ // Hide initially to prevent flicker (live SPA)
550
469
  el.style.opacity = '0';
551
470
  el.style.transition = 'opacity 0.15s ease-in-out';
552
471
  }
@@ -595,16 +514,12 @@ async function initializeMarkdownPlugin() {
595
514
  // Post-process HTML to enable checkboxes (remove disabled attribute)
596
515
  html = enableCheckboxes(html);
597
516
 
598
- // Promote inline code attribute blocks (`foo`{copy}) to
599
- // real attributes so the code plugin can wire copy/highlight.
517
+ // Promote inline `foo`{copy} blocks to real attributes.
600
518
  html = applyInlineCodeAttributes(html);
601
519
 
602
- // Apply opt-in DOMPurify sanitization for x-markdown.safe
603
520
  html = await maybeSanitizeMarkdownHtml(html, safe);
604
521
 
605
- // Only update if content has changed and isn't empty
606
522
  if (element.innerHTML !== html && html.trim() !== '') {
607
- // Create a temporary container to hold the HTML
608
523
  const temp = document.createElement('div');
609
524
  temp.innerHTML = html;
610
525
 
@@ -614,10 +529,8 @@ async function initializeMarkdownPlugin() {
614
529
  element.appendChild(temp.firstChild);
615
530
  }
616
531
 
617
- // Notify the code plugin to scan the new subtree
618
- // fenced blocks and `inline`{copy} elements are added
619
- // outside Alpine's initial walk and won't otherwise
620
- // be picked up by the IntersectionObserver.
532
+ // Notify the code plugin to scan the new subtree (added
533
+ // outside Alpine's initial walk).
621
534
  if (window.ManifestCode?.observeAll) {
622
535
  window.ManifestCode.observeAll(element);
623
536
  }
@@ -677,21 +590,16 @@ async function initializeMarkdownPlugin() {
677
590
  return;
678
591
  }
679
592
 
680
- // Prerender idempotency: on prerendered MPA pages with baked content,
681
- // the x-markdown element is already correct — skip the reactive effect
682
- // entirely. Navigation on MPA is full page loads, so there's no
683
- // dynamic re-resolution to handle; each route serves its own prerendered
684
- // HTML with the right baked content.
593
+ // Prerendered MPA pages with baked content are already correct — skip
594
+ // the effect (MPA navigation is full page loads).
685
595
  if (hasBakedContent) {
686
596
  return;
687
597
  }
688
598
 
689
- // Handle expressions (file paths, inline strings, content references)
690
- // Check if this is a simple string literal that needs to be quoted
599
+ // Quote bare string literals so Alpine doesn't treat them as expressions.
691
600
  let processedExpression = expression;
692
601
  if (!expression.includes('+') && !expression.includes('`') && !expression.includes('${') &&
693
602
  !expression.startsWith('$') && !expression.startsWith("'") && !expression.startsWith('"')) {
694
- // Wrap simple string literals in quotes to prevent Alpine from treating them as expressions
695
603
  processedExpression = `'${expression.replace(/'/g, "\\'")}'`;
696
604
  }
697
605
  const getMarkdownContent = evaluateLater(processedExpression);
@@ -758,12 +666,8 @@ async function initializeMarkdownPlugin() {
758
666
  }
759
667
  }
760
668
 
761
- // Skip re-render if content hasn't changed, but still restore
762
- // visibility during a dev-reload the data plugin briefly
763
- // clears its source cache, which makes the expression
764
- // resolve to undefined and pushes opacity to 0; if we
765
- // early-return here without restoring it, the article stays
766
- // hidden even though innerHTML is intact.
669
+ // Content unchanged: skip re-render but restore visibility a
670
+ // dev-reload can transiently push opacity to 0 while innerHTML is intact.
767
671
  if (markdownContent === lastProcessedContent) {
768
672
  if (el.innerHTML && el.innerHTML.trim() !== '') {
769
673
  hasContent = true;
@@ -787,14 +691,11 @@ async function initializeMarkdownPlugin() {
787
691
  // Post-process HTML to enable checkboxes (remove disabled attribute)
788
692
  html = enableCheckboxes(html);
789
693
 
790
- // Promote inline code attribute blocks (`foo`{copy}) to
791
- // real attributes so the code plugin can wire copy/highlight.
694
+ // Promote inline `foo`{copy} blocks to real attributes.
792
695
  html = applyInlineCodeAttributes(html);
793
696
 
794
- // Apply opt-in DOMPurify sanitization for x-markdown.safe
795
697
  html = await maybeSanitizeMarkdownHtml(html, safe);
796
698
 
797
- // Only update DOM if HTML actually changed
798
699
  if (el.innerHTML !== html) {
799
700
  // Create temporary container
800
701
  const temp = document.createElement('div');
@@ -888,9 +789,8 @@ async function initializeMarkdownPlugin() {
888
789
  element.appendChild(temp.firstChild);
889
790
  }
890
791
 
891
- // Ensure Alpine processes the newly inserted HTML
892
- // This is critical for data source expressions like $x.projects
893
- // Try to wait for magic methods, but proceed anyway if not ready
792
+ // Let Alpine process the inserted HTML (needed for $x.* exprs);
793
+ // wait briefly for magic methods, but proceed if not ready.
894
794
  const initAlpine = (retryCount = 0) => {
895
795
  if (!window.Alpine || typeof window.Alpine.initTree !== 'function') {
896
796
  if (retryCount < 5) {
@@ -942,13 +842,11 @@ async function initializeMarkdownPlugin() {
942
842
  const content = expression.slice(1, -1);
943
843
  updateContent(el, content);
944
844
  } else {
945
- // For complex expressions, we need to force Alpine to re-process this element
946
-
947
- // Remove and re-add the attribute to force Alpine to re-process it
845
+ // Complex expressions: remove + re-add the attribute to force
846
+ // Alpine to re-process once the directive is registered.
948
847
  const originalExpression = expression;
949
848
  el.removeAttribute('x-markdown');
950
849
 
951
- // Use a small delay to ensure the directive is registered
952
850
  setTimeout(() => {
953
851
  el.setAttribute('x-markdown', originalExpression);
954
852
  }, 50);
@@ -964,8 +862,7 @@ async function initializeMarkdownPlugin() {
964
862
  // Track initialization to prevent duplicates
965
863
  let markdownPluginInitialized = false;
966
864
 
967
- // True once Alpine has completed its initial DOM walk. Listener is bound at
968
- // module load so we never miss the event, whatever the script order.
865
+ // True once Alpine finished its initial DOM walk; listener bound at module load.
969
866
  let markdownAlpineHasWalked = false;
970
867
  document.addEventListener('alpine:initialized', () => { markdownAlpineHasWalked = true; });
971
868
 
@@ -980,17 +877,11 @@ async function ensureMarkdownPluginInitialized() {
980
877
  markdownPluginInitialized = true;
981
878
  await initializeMarkdownPlugin();
982
879
 
983
- // Only walk existing [x-markdown] subtrees ourselves when Alpine has ALREADY
984
- // finished its initial walk (i.e. this plugin loaded late, e.g. after a
985
- // component was swapped in). In the normal flow we register the directive
986
- // during `alpine:init`, before Alpine's one boot walk — so Alpine processes
987
- // every element with all sibling directives (x-icon, x-tooltip, …) already
988
- // registered. Walking here during boot would re-init those subtrees before
989
- // the other directives exist, dropping nested plugin content.
880
+ // Only walk existing subtrees ourselves when Alpine already finished its boot
881
+ // walk (late load). Walking during boot would drop nested plugin content.
990
882
  if (markdownAlpineHasWalked && typeof window.Alpine.initTree === 'function') {
991
883
  const existingMarkdownElements = document.querySelectorAll('[x-markdown]');
992
884
  existingMarkdownElements.forEach(el => {
993
- // Only process if not already processed by Alpine
994
885
  if (!el.__x) {
995
886
  window.Alpine.initTree(el);
996
887
  }