mnfst 0.5.213 → 0.5.215

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,4 +1,5 @@
1
- /* Manifest Export — x-export / $export download of pages, regions, or data (pdf/png/jpeg/webp/csv/json) */
1
+ /* Manifest Export — x-export / $export download of pages, regions, or data (pdf/png/jpeg/webp/csv/json),
2
+ and its mirror x-import / $import: pick a local file and parse it into data (json/csv) */
2
3
 
3
4
  (function () {
4
5
 
@@ -523,4 +524,251 @@ if (window.Alpine && typeof window.Alpine.directive === 'function') {
523
524
  }
524
525
 
525
526
 
527
+ })();
528
+
529
+ // ----- Import (x-import / $import) ------------------------------------------
530
+
531
+ (function () {
532
+
533
+
534
+ function initializeImportPlugin() {
535
+
536
+ Alpine.directive('import', (el, { modifiers, expression }, { evaluate, cleanup }) => {
537
+
538
+ const onClick = async (e) => {
539
+ if (e && typeof e.preventDefault === 'function') e.preventDefault();
540
+ const opts = resolveOptions(expression, modifiers, evaluate);
541
+ try {
542
+ await runImport(el, opts);
543
+ } catch (err) {
544
+ if (!err || !err._mnfstReported) emitError(el, opts.format, err);
545
+ }
546
+ };
547
+ el.addEventListener('click', onClick);
548
+ cleanup(() => el.removeEventListener('click', onClick));
549
+ });
550
+
551
+ // ----- $import magic — programmatic file pick, resolves parsed data -----
552
+ Alpine.magic('import', (el) => async (opts = {}) => {
553
+ return runImport(el || null, { ...opts });
554
+ });
555
+
556
+ // ------- Options + format helpers -----------------------------------
557
+
558
+ function resolveOptions(expression, modifiers, evaluate) {
559
+ let opts = {};
560
+ if (expression && expression.trim()) {
561
+ try {
562
+ const v = evaluate(expression);
563
+ if (v && typeof v === 'object') opts = { ...v };
564
+ else if (typeof v === 'string') opts = isKnownFormat(v.toLowerCase()) ? { format: v.toLowerCase() } : { source: v };
565
+ } catch (err) {
566
+ console.warn('[x-import] could not evaluate options expression:', err.message);
567
+ }
568
+ }
569
+ // Modifier shorthand for format only (.json, .csv).
570
+ if (!opts.format && Array.isArray(modifiers) && modifiers.length) {
571
+ const found = modifiers.find((m) => isKnownFormat(String(m).toLowerCase()));
572
+ if (found) opts.format = String(found).toLowerCase();
573
+ }
574
+ return opts;
575
+ }
576
+
577
+ function isKnownFormat(f) {
578
+ return f === 'json' || f === 'csv';
579
+ }
580
+
581
+ function acceptFor(format) {
582
+ if (format === 'json') return '.json,application/json';
583
+ if (format === 'csv') return '.csv,text/csv,.tsv';
584
+ return '.json,application/json,.csv,text/csv,.tsv';
585
+ }
586
+
587
+ function formatOf(file, declared) {
588
+ if (declared && isKnownFormat(declared)) return declared;
589
+ const name = (file && file.name || '').toLowerCase();
590
+ if (name.endsWith('.csv') || name.endsWith('.tsv')) return 'csv';
591
+ return 'json';
592
+ }
593
+
594
+ // ------- File picking ------------------------------------------------
595
+
596
+ // Hidden input per pick; resolves the chosen File, or null when the dialog
597
+ // is dismissed (the input 'cancel' event, where supported).
598
+ function pickFile(accept) {
599
+ return new Promise((resolve) => {
600
+ const input = document.createElement('input');
601
+ input.type = 'file';
602
+ if (accept) input.accept = accept;
603
+ input.style.display = 'none';
604
+ const done = (file) => { input.remove(); resolve(file); };
605
+ input.addEventListener('change', () => done(input.files && input.files[0] || null), { once: true });
606
+ input.addEventListener('cancel', () => done(null), { once: true });
607
+ document.body.appendChild(input);
608
+ input.click();
609
+ });
610
+ }
611
+
612
+ function readAsText(file) {
613
+ if (typeof file.text === 'function') return file.text();
614
+ return new Promise((resolve, reject) => {
615
+ const reader = new FileReader();
616
+ reader.onload = () => resolve(String(reader.result || ''));
617
+ reader.onerror = () => reject(reader.error || new Error('could not read file'));
618
+ reader.readAsText(file);
619
+ });
620
+ }
621
+
622
+ // ------- Parsing -------------------------------------------------------
623
+
624
+ function parseContent(text, format) {
625
+ if (format === 'json') {
626
+ return JSON.parse(text);
627
+ }
628
+ return parseCsv(text);
629
+ }
630
+
631
+ // RFC 4180-style CSV: quoted fields, embedded commas/newlines/quotes.
632
+ // Header row → array of objects. Delimiter sniffed from the header line.
633
+ function parseCsv(text) {
634
+ const src = String(text).replace(/^/, '');
635
+ if (!src.trim()) return [];
636
+ const delimiter = sniffDelimiter(src);
637
+ const rows = [];
638
+ let row = [];
639
+ let cell = '';
640
+ let inQuotes = false;
641
+ for (let i = 0; i < src.length; i++) {
642
+ const ch = src[i];
643
+ if (inQuotes) {
644
+ if (ch === '"') {
645
+ if (src[i + 1] === '"') { cell += '"'; i++; }
646
+ else inQuotes = false;
647
+ } else cell += ch;
648
+ } else if (ch === '"') {
649
+ inQuotes = true;
650
+ } else if (ch === delimiter) {
651
+ row.push(cell); cell = '';
652
+ } else if (ch === '\n' || ch === '\r') {
653
+ if (ch === '\r' && src[i + 1] === '\n') i++;
654
+ row.push(cell); cell = '';
655
+ rows.push(row); row = [];
656
+ } else {
657
+ cell += ch;
658
+ }
659
+ }
660
+ if (cell !== '' || row.length) { row.push(cell); rows.push(row); }
661
+ while (rows.length && rows[rows.length - 1].every((c) => c === '')) rows.pop();
662
+ if (!rows.length) return [];
663
+ const headers = rows[0].map((h) => String(h).trim());
664
+ return rows.slice(1).map((cells) => {
665
+ const out = {};
666
+ headers.forEach((h, i) => { if (h) out[h] = coerceCell(cells[i]); });
667
+ return out;
668
+ });
669
+ }
670
+
671
+ function sniffDelimiter(src) {
672
+ const header = src.slice(0, src.indexOf('\n') < 0 ? src.length : src.indexOf('\n'));
673
+ let best = ',', bestCount = 0;
674
+ for (const d of [',', ';', '\t']) {
675
+ const count = header.split(d).length - 1;
676
+ if (count > bestCount) { best = d; bestCount = count; }
677
+ }
678
+ return best;
679
+ }
680
+
681
+ // Undo x-export's CSV serialization: JSON-looking cells parse back to
682
+ // objects/arrays; numbers, booleans and null coerce; everything else stays a string.
683
+ function coerceCell(v) {
684
+ if (v == null) return '';
685
+ const s = String(v);
686
+ const t = s.trim();
687
+ if (t === '') return '';
688
+ if (t === 'true') return true;
689
+ if (t === 'false') return false;
690
+ if (t === 'null') return null;
691
+ if (/^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(t)) {
692
+ const n = Number(t);
693
+ if (Number.isFinite(n)) return n;
694
+ }
695
+ if (t[0] === '{' || t[0] === '[') {
696
+ try { return JSON.parse(t); } catch { /* keep string */ }
697
+ }
698
+ return s;
699
+ }
700
+
701
+ // ------- Delivery ------------------------------------------------------
702
+
703
+ function applyToSource(source, data) {
704
+ const store = window.ManifestDataStore;
705
+ if (!store || typeof store.updateStore !== 'function') {
706
+ throw new Error(`cannot write to source "${source}" — the data plugin is not loaded`);
707
+ }
708
+ store.updateStore(source, data, { loading: false, error: null, ready: true, fresh: true, allowDuringInit: true });
709
+ }
710
+
711
+ async function runImport(el, opts) {
712
+ const accept = opts.accept || acceptFor(opts.format);
713
+ const file = await pickFile(accept);
714
+ if (!file) return null;
715
+ const format = formatOf(file, opts.format);
716
+ let data;
717
+ try {
718
+ data = parseContent(await readAsText(file), format);
719
+ } catch (err) {
720
+ const e = new Error(`could not parse ${file.name} as ${format}: ${err.message}`);
721
+ emitError(el, format, e);
722
+ e._mnfstReported = true;
723
+ throw e;
724
+ }
725
+ if (opts.source) applyToSource(String(opts.source), data);
726
+ emit(el, 'manifest:import', {
727
+ data,
728
+ format,
729
+ source: opts.source || null,
730
+ file: { name: file.name, size: file.size, type: file.type }
731
+ });
732
+ return data;
733
+ }
734
+
735
+ function emit(el, name, detail) {
736
+ try {
737
+ const event = new CustomEvent(name, { detail, bubbles: true, composed: true });
738
+ (el && el.dispatchEvent ? el : window).dispatchEvent(event);
739
+ } catch { /* ignore */ }
740
+ }
741
+
742
+ function emitError(el, format, err) {
743
+ console.error('[x-import] import failed:', err && err.message || err);
744
+ emit(el, 'manifest:import-error', { format: format || null, error: err && err.message || String(err) });
745
+ }
746
+ }
747
+
748
+ // Standard plugin init lifecycle.
749
+ let importPluginInitialized = false;
750
+ function ensureImportPluginInitialized() {
751
+ if (importPluginInitialized) return;
752
+ if (!window.Alpine || typeof window.Alpine.directive !== 'function') return;
753
+ importPluginInitialized = true;
754
+ initializeImportPlugin();
755
+ }
756
+ window.ensureImportPluginInitialized = ensureImportPluginInitialized;
757
+ if (document.readyState === 'loading') {
758
+ document.addEventListener('DOMContentLoaded', ensureImportPluginInitialized);
759
+ }
760
+ document.addEventListener('alpine:init', ensureImportPluginInitialized);
761
+ if (window.Alpine && typeof window.Alpine.directive === 'function') {
762
+ setTimeout(ensureImportPluginInitialized, 0);
763
+ } else {
764
+ const checkAlpine = setInterval(() => {
765
+ if (window.Alpine && typeof window.Alpine.directive === 'function') {
766
+ clearInterval(checkAlpine);
767
+ ensureImportPluginInitialized();
768
+ }
769
+ }, 10);
770
+ setTimeout(() => clearInterval(checkAlpine), 5000);
771
+ }
772
+
773
+
526
774
  })();
@@ -25,7 +25,12 @@
25
25
  }
26
26
 
27
27
  & input,
28
- & .combobox,
28
+ & .combobox {
29
+ width: fit-content
30
+ }
31
+
32
+ /* Own rule: a selector list is not forgiving, so a browser that can't
33
+ parse the :has() would drop the two plain selectors above with it. */
29
34
  & :where(label, .label):has(input:not([type=checkbox], [type=radio]), select, textarea) {
30
35
  width: fit-content
31
36
  }
@@ -1,34 +1,34 @@
1
1
  {
2
- "manifest.appwrite.auth.js": "sha384-HMJmRXnvdDBWyuteXL+DTxWM2llSCdjl4EDamEI7iHNAVlqeOCE4GDTm8ZHipPRj",
3
- "manifest.appwrite.data.js": "sha384-Qs+BZTiBnMjU9p5AZ2wZ0Dv7nJII4PtCnzrhu2h9QF3/7S/ULMOa4Gh7tpsagJz8",
2
+ "manifest.appwrite.auth.js": "sha384-X30RFvbwT9qEIaMsMUumnW54ZzcnY5aY9QQeXPG8/0sI6VAbAw3+FZ6iuZb6b4cu",
3
+ "manifest.appwrite.data.js": "sha384-cfOicas38L58E3CLRnXIdHR5FFFBPuzesQHl5HgqXARZq29XMBlqOmITteI35d5a",
4
4
  "manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
5
5
  "manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
6
- "manifest.charts.js": "sha384-WE0pnHK4LBAkFyTw4+R6gheB9NFVJg81ZAG9GwMnbKI5gY3zlJIKM5neMlBUhTso",
6
+ "manifest.charts.js": "sha384-gEAv/98+29RbhvNkiNWTNyBPbLTFr2Ld15wLu29FLCL57j1cwGChRrL9Pfk7dBtT",
7
7
  "manifest.chat.js": "sha384-88DfMfJLoi2oxclVD2iIMDkIGAExMXq5k47mekUredlicLaP3oZY5bwtwqs+TqDF",
8
8
  "manifest.code.js": "sha384-aoQ4E26vrhNe/+tL7K0h90c8/0ies7e8oIR6Ay92he2X3dnWZ+OPcn2Dna8fmc+c",
9
9
  "manifest.color.js": "sha384-6Rv3LxyTcZNjrhtayQfqRdCx0uSZ4BiEbgEI98I62eTvp8Aw7LBIoNJ0Je1oktwL",
10
- "manifest.colorpicker.js": "sha384-1+ZV5Hhjf7WnGP7b71Cqa5h3BKW8UgGT5V14i8Q+aF5zH8Xxwowt/ibSUVsiIHSm",
11
- "manifest.combobox.js": "sha384-5DaKDWkWF7zxS80h7G1AcZG9hr+uGqJ1+B09aaVIVF+z0Qn4j8+9KEg7gys/ABQG",
12
- "manifest.components.js": "sha384-458gCDrUIZts3WE1WZ/UA8Y1XcV6PaF+5k3Wf/Jr2iIY5EM/FisjwbLzpON8I25s",
10
+ "manifest.colorpicker.js": "sha384-BhcCe8cGcd0DTJNwzXWWY+FQNRfSFFl6XqfHIFP8yHg/aXFks1NIC6lVx0Sr1VkM",
11
+ "manifest.combobox.js": "sha384-MILTkrgsTzsYUjGo4ZuADqV85Xs4GmfB4xexfe9vbJMIoQTZ42W4zs+DTaVSPEsx",
12
+ "manifest.components.js": "sha384-27RMMNmXRgFLysdvJgZjDxfXvpIGlQUoWzmwaYCwdfNA+78LARTbOt3nKAPfXwvA",
13
13
  "manifest.computed.js": "sha384-GYML/e81gQfN+iDbqhPUgny8bINCyL+SrbTofoGk4hlCnLLjv51mCv42JMqq+ku5",
14
- "manifest.data.js": "sha384-qGsyOceU+J0j0gQB804fp4PIw0fEn2FsMuKCWEaPYKZ13gb7RIaGK1kY/jJO089E",
15
- "manifest.datepicker.js": "sha384-8vTJYkLqqvgXgm1w1becuaRR/jUEpiLBjBJ6RsRbUr+ZkypvspD6EsssuDrX4SQq",
14
+ "manifest.data.js": "sha384-rZO5eUSHAFhSWqcGTqgQibrThpKZcTTgvaI+x1HYa2oepLjDGwhbjgNeYCWqZNM+",
15
+ "manifest.datepicker.js": "sha384-XnbeURni2X91ozVzatQcSjYYIS4MoW12dBRJnYAGBr0nCQ624heH5s+xtQSpLZX6",
16
16
  "manifest.defer.js": "sha384-wXa60M2OHigi5rBurXQ7Oa8eyr5WTKs7EUn2GOru62KMdfs/LDyNegxCc9MNJFSe",
17
17
  "manifest.device.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
18
18
  "manifest.dropdowns.js": "sha384-0ilj9AvRph0LacIBJSqwpu1fvPRn5gcWbhIG1SOpaoM/Q8idMbKBLupwM+5O0UaX",
19
19
  "manifest.edit.js": "sha384-oLsrH7/UkJ/KEl3UO77hexcf2rYCGgzXZd0fe2EMq3hKRE7aUqLiEnLJhSV7zTaY",
20
- "manifest.export.js": "sha384-HCy3n6ifrcAGIyzTRgZ5JIRvSX9q9/8ykMKRSbt1QTnhXz6PgYlV0AE2N2Yi2re+",
20
+ "manifest.export.js": "sha384-Rx6XvqBZc4apzP/FwcDr6yIln4/zNa08Q4ZqBZcEqUz1aNZQfl4h0+Ae7mqCQyuG",
21
21
  "manifest.icons.js": "sha384-kKOINpX70YzNO3FvkV6umrmO3f0KIu0DNbwB+ej55a7y/lhU7v/myQtXL0bwCTbt",
22
22
  "manifest.localization.js": "sha384-EImsIw8Z3l13V5eH9ZJF/yfsOfIvR1oPRNSArANkPji7V8O3IRf6zMwsNiAGj3Hk",
23
- "manifest.markdown.js": "sha384-HFE2VwBRX5vhDGQ3c8XgZ10Y5ENkXxHzgX9DjmIEffqLoUDbnJYNk3TFWT9k3e+f",
23
+ "manifest.markdown.js": "sha384-fzyRaD7LwPOYcUeJZCMh6aWgk1ChxIVgy3eUTEjSwZ3DvAHrsl5EMvfxlvOKXD5U",
24
24
  "manifest.native.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
25
25
  "manifest.payments.js": "sha384-8pofk2zhxtapPMpwnFyTelPMrYhShNhDCE722uQh/EYFvZFuPLmSdgunqsarVQjv",
26
26
  "manifest.resize.js": "sha384-+0IsyrGP/4gZmNbD6oU8dCgEyhpBsXMm74NIenYeUGuGRWyJDurcG27xE/CA0/aA",
27
27
  "manifest.router.js": "sha384-4qmGEkRpgAo9xoQkW6+hHIhowvcyAggKB2J3Wf8tJtXp+L0hufoVAqzfhnqw4cNR",
28
28
  "manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
29
- "manifest.status.js": "sha384-nwvUSbT+gpRMqhYx6MeBYswUMH72rY92mjgxv0vrSG7Ht7l83XYpEB6fCTbXjjfK",
29
+ "manifest.status.js": "sha384-MBO345ab7H6Uq1X2dZLt4Ge1Gcra+Z2qYApvlAeIV8CJhDDJrz2FPXpp+Kcp8zcM",
30
30
  "manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
31
- "manifest.sw.js": "sha384-qbDoL97nOZnBUklWe6gAESKBUOL8EAR2qWzVw2TqBn7laSoPHnkXqVBc7kFS7HCG",
31
+ "manifest.sw.js": "sha384-Kh0gcISFTnkgXmmn2ykDjYsGw7NFDFaorDt5ZYVfg5Xs1Uy8H0ll0E98MMPqdBPy",
32
32
  "manifest.tabs.js": "sha384-OIIcyaQnGS0eNYk4zgAFEDboNYect7THvUd9D0V6nqCeDOxQcoiqLrBEmmWVTTet",
33
33
  "manifest.tailwind.js": "sha384-r/Jvqb+Piz21jUts10BbVAJoKkShOdJRVFpywyi4HPIi/OfOJq5CtPyWAPbdkMgt",
34
34
  "manifest.text.edit.js": "sha384-pfhc4IPKNJokj6ldlgD+KwGLN4UoM21CNeVf2sVq/34X1Un2PUhEuyTg5itmtsXS",
@@ -37,5 +37,5 @@
37
37
  "manifest.url.parameters.js": "sha384-J2aSVBOPsoqsbafHndrWLHEDpKb4AKJAoQnVmKA4gZh5P/8HHjFWHO/lkusmG84l",
38
38
  "manifest.utilities.js": "sha384-pIKcTvk9lrlXZK0OXld6SA6wrmlaFTCmTpLn67nOCE+XhNw4fuGSDX4Z3C2eR6CJ",
39
39
  "manifest.virtual.js": "sha384-YwwmAsZNddajnAkLhBWtrK8edHMJOhD7cJkQi3qmm5o1Thg0wbtDU0dsfrCHKTIQ",
40
- "manifest.js": "sha384-fyktZSCaKJ267Hj5ORpbS/Dk11vtT+J8BN1XkA8zG+kXG1wPDwcA1hmB/40fY5Pq"
40
+ "manifest.js": "sha384-OMEyd7TUB5EfAhabW58OgEn6AkZgOuH5IQ85dHZf7ciI0jmXXrsCp2Eh74iVnJ62"
41
41
  }
package/lib/manifest.js CHANGED
@@ -461,7 +461,7 @@
461
461
  const resolved = [];
462
462
  const added = new Set();
463
463
 
464
- const PLUGIN_ALIASES = { native: 'device' }; // renamed 0.5.199; the old name keeps working
464
+ const PLUGIN_ALIASES = { native: 'device', import: 'export' }; // native renamed 0.5.199; import ships inside export
465
465
  function addPluginWithDeps(plugin) {
466
466
  plugin = PLUGIN_ALIASES[plugin] || plugin;
467
467
  if (added.has(plugin)) return;
@@ -540,11 +540,16 @@
540
540
  { magic: '$presence', plugin: 'appwrite-presences' },
541
541
  ...['$share', '$secure', '$links', '$push', '$app', '$haptics', '$biometric', '$camera'].map(magic => ({ magic, plugin: 'device' }))
542
542
  ];
543
+ // Framework web components — <x-*> tags that are NOT project components.
544
+ const FRAMEWORK_TAG_RE = /^x-(code|code-group)$/;
543
545
  function detectUsagePlugins(alreadyLoaded) {
544
546
  const wanted = USAGE_PLUGINS.filter(u => !alreadyLoaded.includes(u.plugin));
545
- if (wanted.length === 0 || typeof document === 'undefined') return [];
547
+ // <x-*> tags load the components plugin even with nothing registered in
548
+ // manifest.json (the loader's components/<name>.html convention).
549
+ const wantComponents = !alreadyLoaded.includes('components');
550
+ if ((wanted.length === 0 && !wantComponents) || typeof document === 'undefined') return [];
546
551
  const found = new Set();
547
- const done = () => found.size === wanted.length;
552
+ const done = () => found.size === wanted.length + (wantComponents ? 1 : 0);
548
553
 
549
554
  for (const s of document.querySelectorAll('script:not([src])')) {
550
555
  const t = s.textContent || '';
@@ -554,6 +559,10 @@
554
559
 
555
560
  const scan = (root) => {
556
561
  for (const el of root.querySelectorAll('*')) {
562
+ if (wantComponents && !found.has('components')) {
563
+ const tag = el.tagName.toLowerCase();
564
+ if (tag.startsWith('x-') && !FRAMEWORK_TAG_RE.test(tag)) found.add('components');
565
+ }
557
566
  for (const attr of el.attributes || []) {
558
567
  const v = attr.value;
559
568
  if (!v || v.indexOf('$') === -1) continue;
@@ -690,7 +699,7 @@
690
699
  // browser engine (lib/manifest.tailwind.js) — but the utilities plugin's
691
700
  // uncovered-class watcher lazily loads it this way (see
692
701
  // manifest.utilities.static.js setupUncoveredClassWatcher).
693
- const allPlugins = [...AVAILABLE_PLUGINS, ...APPWRITE_PLUGINS, ...AUTHORING_PLUGINS, 'payments', 'chat', 'device', 'tailwind'];
702
+ const allPlugins = [...AVAILABLE_PLUGINS, ...APPWRITE_PLUGINS, ...AUTHORING_PLUGINS, 'payments', 'chat', 'device', 'native', 'import', 'tailwind'];
694
703
  if (!allPlugins.includes(pluginName)) {
695
704
  console.warn(`[Manifest Loader] Unknown plugin: ${pluginName}`);
696
705
  return Promise.reject(new Error(`Unknown plugin: ${pluginName}`));
@@ -463,16 +463,17 @@ async function initializeMarkdownPlugin() {
463
463
  };
464
464
 
465
465
  Alpine.directive('markdown', (el, { expression, modifiers }, { effect, evaluateLater }) => {
466
+ // `.safe` runs parsed HTML through DOMPurify before injection. Default
467
+ // is unsanitized (raw HTML + custom-element extensions); use .safe for
468
+ // untrusted source (Appwrite, API responses, crowdsourced translations).
469
+ const safe = Array.isArray(modifiers) && modifiers.includes('safe');
470
+
466
471
 
467
472
  // Handle null/undefined expressions gracefully
468
473
  if (!expression) {
469
474
  return;
470
475
  }
471
476
 
472
- // `.safe` runs parsed HTML through DOMPurify before injection. Default
473
- // is unsanitized (raw HTML + custom-element extensions); use .safe for
474
- // untrusted source (Appwrite, API responses, crowdsourced translations).
475
- const safe = Array.isArray(modifiers) && modifiers.includes('safe');
476
477
 
477
478
  // Prerender idempotency: baked HTML children are build-time authoritative
478
479
  // — skip the initial hide-and-re-render, but still register the effect
@@ -677,6 +678,10 @@ async function initializeMarkdownPlugin() {
677
678
  resolvedPath = (basePath ? basePath + '/' : '/') + pathOrContent;
678
679
  }
679
680
 
681
+ // Prerender classification signal: the resolved
682
+ // per-locale source, stamped before the fetch so the
683
+ // renderer sees it even if capture races the request.
684
+ try { el.setAttribute('data-mnfst-md-src', resolvedPath); } catch (_) { }
680
685
  // Check cache first
681
686
  if (markdownCache.has(resolvedPath)) {
682
687
  markdownContent = markdownCache.get(resolvedPath);
@@ -799,6 +804,9 @@ async function initializeMarkdownPlugin() {
799
804
  existingMarkdownElements.forEach(el => {
800
805
  const expression = el.getAttribute('x-markdown');
801
806
 
807
+ // Modifiers live in the attribute name in this pre-registration path.
808
+ const safe = el.getAttributeNames().some(n => n.startsWith('x-markdown.') && n.split('.').includes('safe'));
809
+
802
810
  // Create a temporary Alpine component context for this element
803
811
  const tempComponent = Alpine.$data(el) || {};
804
812