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.
- package/lib/manifest.appwrite.auth.js +107 -199
- package/lib/manifest.appwrite.data.js +1 -3
- package/lib/manifest.charts.js +88 -97
- package/lib/manifest.chat.js +515 -0
- package/lib/manifest.code.js +110 -340
- package/lib/manifest.colorpicker.js +252 -551
- package/lib/manifest.combobox.js +73 -91
- package/lib/manifest.components.js +34 -95
- package/lib/manifest.css +18 -144
- package/lib/manifest.data.js +189 -562
- package/lib/manifest.datepicker.js +43 -128
- package/lib/manifest.dropdowns.js +18 -39
- package/lib/manifest.export.js +29 -147
- package/lib/manifest.form.css +18 -12
- package/lib/manifest.icons.js +9 -24
- package/lib/manifest.integrity.json +23 -23
- package/lib/manifest.js +62 -126
- package/lib/manifest.localization.js +67 -200
- package/lib/manifest.markdown.js +65 -174
- package/lib/manifest.min.css +1 -1
- package/lib/manifest.payments.js +40 -139
- package/lib/manifest.router.js +24 -73
- package/lib/manifest.status.js +20 -56
- package/lib/manifest.svg.js +12 -30
- package/lib/manifest.tooltips.js +28 -65
- package/lib/manifest.utilities.js +40 -107
- package/lib/manifest.virtual.js +15 -53
- package/package.json +1 -1
- package/lib/manifest.appwrite.presence.js +0 -1650
package/lib/manifest.markdown.js
CHANGED
|
@@ -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
|
|
10
|
-
//
|
|
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
|
|
21
|
-
//
|
|
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
|
|
35
|
-
//
|
|
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 +
|
|
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
|
|
73
|
-
//
|
|
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 —
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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 +
|
|
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
|
|
132
|
-
//
|
|
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, '&')
|
|
@@ -140,9 +119,7 @@ function escapeForAttribute(s) {
|
|
|
140
119
|
.replace(/>/g, '>');
|
|
141
120
|
}
|
|
142
121
|
|
|
143
|
-
// Escape a literal HTML fragment
|
|
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, '&')
|
|
@@ -154,10 +131,8 @@ function escapeForText(s) {
|
|
|
154
131
|
async function configureMarked(marked) {
|
|
155
132
|
marked.use({
|
|
156
133
|
renderer: {
|
|
157
|
-
// Render fenced
|
|
158
|
-
//
|
|
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
|
|
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
|
|
183
|
-
//
|
|
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
|
-
//
|
|
212
|
-
//
|
|
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
|
|
263
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
313
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
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
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
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 `<`, 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
|
|
389
|
-
//
|
|
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
|
-
//
|
|
515
|
-
//
|
|
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
|
-
//
|
|
532
|
-
// (
|
|
533
|
-
//
|
|
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:
|
|
542
|
-
//
|
|
543
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
681
|
-
// the
|
|
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
|
-
//
|
|
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
|
-
//
|
|
762
|
-
//
|
|
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
|
|
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
|
-
//
|
|
892
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
984
|
-
//
|
|
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
|
}
|