mnfst 0.5.80 → 0.5.82

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.
Files changed (42) hide show
  1. package/LICENSE +1 -1
  2. package/lib/manifest.accordion.css +4 -4
  3. package/lib/manifest.appwrite.auth.js +66 -33
  4. package/lib/manifest.avatar.css +8 -8
  5. package/lib/manifest.button.css +7 -7
  6. package/lib/manifest.checkbox.css +5 -5
  7. package/lib/manifest.code.css +152 -193
  8. package/lib/manifest.code.js +841 -881
  9. package/lib/manifest.code.min.css +1 -1
  10. package/lib/manifest.colorpicker.css +11 -11
  11. package/lib/manifest.components.js +25 -155
  12. package/lib/manifest.css +278 -230
  13. package/lib/manifest.data.js +46 -2
  14. package/lib/manifest.dialog.css +2 -2
  15. package/lib/manifest.divider.css +2 -2
  16. package/lib/manifest.dropdown.css +9 -9
  17. package/lib/manifest.form.css +10 -10
  18. package/lib/manifest.input.css +9 -9
  19. package/lib/manifest.integrity.json +26 -0
  20. package/lib/manifest.js +60 -5
  21. package/lib/manifest.markdown.js +192 -79
  22. package/lib/manifest.min.css +1 -1
  23. package/lib/manifest.radio.css +1 -1
  24. package/lib/manifest.range.css +7 -7
  25. package/lib/manifest.resize.css +1 -1
  26. package/lib/manifest.router.js +49 -76
  27. package/lib/manifest.schema.json +1 -1
  28. package/lib/manifest.sidebar.css +5 -6
  29. package/lib/manifest.slides.css +5 -5
  30. package/lib/manifest.svg.js +75 -5
  31. package/lib/manifest.switch.css +4 -4
  32. package/lib/manifest.table.css +4 -4
  33. package/lib/manifest.theme.css +46 -41
  34. package/lib/manifest.toast.css +7 -7
  35. package/lib/manifest.tooltip.css +3 -3
  36. package/lib/manifest.tooltips.js +41 -0
  37. package/lib/manifest.typography.css +124 -69
  38. package/lib/manifest.utilities.css +48 -54
  39. package/lib/manifest.utilities.js +9 -29
  40. package/package.json +4 -7
  41. package/lib/manifest.export.js +0 -535
  42. package/lib/manifest.virtual.js +0 -319
@@ -17,6 +17,72 @@ if (typeof window !== 'undefined') {
17
17
  });
18
18
  }
19
19
 
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.
26
+ const MARKDOWN_PURIFY_CONFIG = {
27
+ CUSTOM_ELEMENT_HANDLING: {
28
+ tagNameCheck: /^x-[a-z][\w-]*$/,
29
+ attributeNameCheck: /^(?!on)[a-z][\w\-:]*$/i,
30
+ allowCustomizedBuiltInElements: false
31
+ }
32
+ };
33
+
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.
38
+ if (!window.ManifestDOMPurify) {
39
+ window.ManifestDOMPurify = {
40
+ _promise: null,
41
+ load() {
42
+ if (typeof window.DOMPurify !== 'undefined') return Promise.resolve(window.DOMPurify);
43
+ if (this._promise) return this._promise;
44
+ this._promise = new Promise((resolve, reject) => {
45
+ const script = document.createElement('script');
46
+ script.src = 'https://cdn.jsdelivr.net/npm/dompurify@latest/dist/purify.min.js';
47
+ script.onload = () => {
48
+ if (typeof window.DOMPurify !== 'undefined') {
49
+ resolve(window.DOMPurify);
50
+ } else {
51
+ this._promise = null;
52
+ reject(new Error('DOMPurify failed to load'));
53
+ }
54
+ };
55
+ script.onerror = (err) => {
56
+ this._promise = null;
57
+ reject(err);
58
+ };
59
+ document.head.appendChild(script);
60
+ });
61
+ return this._promise;
62
+ }
63
+ };
64
+ }
65
+
66
+ // Sanitize HTML if the .safe modifier was used; pass-through otherwise.
67
+ // Manifest's default is unsanitized so authors can render arbitrary HTML and
68
+ // the markdown custom-element extensions work — but the .safe opt-in lets
69
+ // authors render data-source content (e.g. user-submitted markdown from
70
+ // Appwrite) without an XSS sink.
71
+ async function maybeSanitizeMarkdownHtml(html, safe) {
72
+ if (!safe) return html;
73
+ try {
74
+ const DOMPurify = await window.ManifestDOMPurify.load();
75
+ return DOMPurify.sanitize(html, MARKDOWN_PURIFY_CONFIG);
76
+ } catch {
77
+ // Loader failure — fall back to escaping rather than silently emitting
78
+ // un-sanitized HTML. The author asked for safe; honour that.
79
+ const escaped = String(html)
80
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
81
+ console.warn('[Manifest Markdown] x-markdown.safe: DOMPurify unavailable — emitting escaped text.');
82
+ return escaped;
83
+ }
84
+ }
85
+
20
86
  // Load marked.js from CDN
21
87
  async function loadMarkedJS() {
22
88
  if (typeof marked !== 'undefined') {
@@ -52,45 +118,61 @@ async function loadMarkedJS() {
52
118
  return markedPromise;
53
119
  }
54
120
 
121
+ // HTML-escape a string for safe interpolation inside an attribute value.
122
+ // Used by the code-fence renderer below — title/language strings come from
123
+ // the markdown source, so without escaping a fence like ```js " onclick=alert(1) x="
124
+ // could inject arbitrary attributes onto the <pre> element.
125
+ function escapeForAttribute(s) {
126
+ return String(s)
127
+ .replace(/&/g, '&amp;')
128
+ .replace(/"/g, '&quot;')
129
+ .replace(/</g, '&lt;')
130
+ .replace(/>/g, '&gt;');
131
+ }
132
+
133
+ // Escape a literal HTML fragment so it displays as source text when placed
134
+ // inside a <code> element. Used for the ::: frame demo modifier, where the
135
+ // same content is rendered live AND shown below as its own source.
136
+ function escapeForText(s) {
137
+ return String(s)
138
+ .replace(/&/g, '&amp;')
139
+ .replace(/</g, '&lt;')
140
+ .replace(/>/g, '&gt;');
141
+ }
142
+
55
143
  // Configure marked to preserve full language strings
56
144
  async function configureMarked(marked) {
57
145
  marked.use({
58
146
  renderer: {
147
+ // Render fenced code blocks as <pre x-code="…"><code>…</code></pre>.
148
+ // The code plugin's directive then handles highlighting, copy
149
+ // buttons, collapse, line numbers, etc. — same code path whether
150
+ // the block was authored in HTML or markdown.
59
151
  code(token) {
60
152
  const lang = token.lang || '';
61
153
  const text = token.text || '';
62
- const escaped = token.escaped || false;
63
-
64
- // Parse the language string to extract attributes
65
- const attributes = parseLanguageString(lang);
66
-
67
- // Build attributes for the x-code element
68
- let xCodeAttributes = '';
69
- if (attributes.title) {
70
- xCodeAttributes += ` name="${attributes.title}"`;
71
- }
72
- if (attributes.language) {
73
- xCodeAttributes += ` language="${attributes.language}"`;
74
- }
75
- if (attributes.numbers) {
76
- xCodeAttributes += ' numbers';
77
- }
78
- if (attributes.copy) {
79
- xCodeAttributes += ' copy';
80
- }
81
-
82
- // For x-code elements, use the raw text to preserve formatting
83
- let code = text;
84
- let preserveOriginal = '';
85
154
 
86
- // For HTML language code blocks, preserve the original raw text to maintain indentation
87
- if (attributes.language === 'html' || text.includes('<!DOCTYPE') || (text.includes('<html') && text.includes('<head') && text.includes('<body'))) {
88
- // Store the original content in a data attribute to preserve indentation
89
- preserveOriginal = ` data-original-content="${text.replace(/"/g, '&quot;')}"`;
155
+ const attrs = parseLanguageString(lang);
156
+
157
+ let preAttrs = '';
158
+ // x-code carries the language as its value (empty string for
159
+ // "no explicit language; auto-detect")
160
+ preAttrs += ` x-code="${attrs.language ? escapeForAttribute(attrs.language) : ''}"`;
161
+ if (attrs.title) preAttrs += ` name="${escapeForAttribute(attrs.title)}"`;
162
+ if (attrs.lines) preAttrs += ' lines';
163
+ if (attrs.copy) preAttrs += ' copy';
164
+ if (attrs.edit) preAttrs += ' edit';
165
+ if (attrs.collapse !== null) {
166
+ preAttrs += attrs.collapse === ''
167
+ ? ' collapse'
168
+ : ` collapse="${escapeForAttribute(attrs.collapse)}"`;
90
169
  }
170
+ if (attrs.from) preAttrs += ` from="${escapeForAttribute(attrs.from)}"`;
91
171
 
92
- // Always create an x-code element, with or without attributes
93
- return `<x-code${xCodeAttributes}${preserveOriginal}>${code}</x-code>\n`;
172
+ // marked has already HTML-escaped `text`. It's safe to drop
173
+ // it inside <code> and let the code plugin read .textContent
174
+ // as the highlight source.
175
+ return `<pre${preAttrs}><code>${text}</code></pre>\n`;
94
176
  }
95
177
  },
96
178
  // Configure marked to allow custom HTML tags
@@ -145,9 +227,17 @@ async function configureMarked(marked) {
145
227
  }
146
228
  },
147
229
  renderer(token) {
148
- const classes = token.classes || '';
230
+ let classes = token.classes || '';
149
231
  const iconValue = token.iconValue || '';
150
232
 
233
+ // `::: frame demo` — render the frame contents live AND emit
234
+ // a sibling <pre x-code="html"> showing the same source. Lets
235
+ // authors write the example once and have it both rendered
236
+ // and documented. Strip `demo` from the class list so the
237
+ // resulting <aside> has just `frame`.
238
+ const isDemo = /\bframe\b/.test(classes) && /\bdemo\b/.test(classes);
239
+ if (isDemo) classes = classes.replace(/\bdemo\b/, '').replace(/\s+/g, ' ').trim();
240
+
151
241
  // For frame callouts, don't parse as markdown to avoid wrapping HTML in <p> tags
152
242
  let parsedContent;
153
243
  if (classes.includes('frame')) {
@@ -158,7 +248,7 @@ async function configureMarked(marked) {
158
248
  parsedContent = marked.parse(token.text);
159
249
  }
160
250
 
161
- const iconHtml = iconValue ? `<span x-icon="${iconValue}"></span>` : '';
251
+ const iconHtml = iconValue ? `<span x-icon="${escapeForAttribute(iconValue)}"></span>` : '';
162
252
 
163
253
  // Create a temporary div to count top-level elements
164
254
  const temp = document.createElement('div');
@@ -173,7 +263,11 @@ async function configureMarked(marked) {
173
263
  `<div>${parsedContent}</div>` :
174
264
  parsedContent;
175
265
 
176
- return `<aside${classes ? ` class="${classes}"` : ''}>${iconHtml}${wrappedContent}</aside>\n`;
266
+ const aside = `<aside${classes ? ` class="${classes}"` : ''}>${iconHtml}${wrappedContent}</aside>`;
267
+ if (isDemo) {
268
+ return `${aside}\n<pre x-code="html" copy><code>${escapeForText(token.text.trim())}</code></pre>\n`;
269
+ }
270
+ return `${aside}\n`;
177
271
  }
178
272
  }]
179
273
  });
@@ -185,17 +279,16 @@ async function configureMarked(marked) {
185
279
  });
186
280
  }
187
281
 
188
- // Custom renderer for x-code-group to handle line breaks properly
282
+ // Markdown preprocessor: ensure that block-level HTML containers (the
283
+ // wrappers authors use to group fenced code blocks into tabs, frames, etc.)
284
+ // have a blank line after their opening tag so marked treats the contents
285
+ // as block-level markdown rather than raw inline HTML. Without this, a
286
+ // fenced ```js immediately after `<div x-code-group>` is treated as text.
189
287
  function renderXCodeGroup(markdown) {
190
- // Find x-code-group blocks and process them specially
191
- const xCodeGroupRegex = /<x-code-group[^>]*>([\s\S]*?)<\/x-code-group>/g;
192
-
193
- return markdown.replace(xCodeGroupRegex, (match, content) => {
194
- // Ensure there's a line break after the opening tag if there isn't one
195
- const processedContent = content.replace(/^(?!\s*\n)/, '\n');
196
-
197
- return `<x-code-group>${processedContent}</x-code-group>`;
198
- });
288
+ return markdown.replace(
289
+ /(<(?:div|section|article|aside)[^>]*\bx-code-group\b[^>]*>)(?!\s*\n)/g,
290
+ '$1\n'
291
+ );
199
292
  }
200
293
 
201
294
  // Post-process HTML to enable checkboxes by removing disabled attribute
@@ -222,68 +315,69 @@ function isHighlightJsAvailable() {
222
315
 
223
316
 
224
317
 
225
- // Parse language string to extract title and attributes
318
+ // Parse a fence's info-string into an attributes bag. Supported tokens:
319
+ // javascript language (first non-flag bareword)
320
+ // "Tab name" quoted name → name attribute (tabs / title bar)
321
+ // lines line numbers gutter
322
+ // copy copy button
323
+ // edit CodeJar editor
324
+ // collapse collapse with default threshold (20 lines)
325
+ // collapse=10 collapse to first 10 lines
326
+ // from=#demo pull source from referenced element
226
327
  function parseLanguageString(languageString) {
227
- if (!languageString || languageString.trim() === '') {
228
- return { title: null, language: null, numbers: false, copy: false };
229
- }
230
-
231
- const parts = languageString.split(/\s+/);
232
-
233
328
  const attributes = {
234
329
  title: null,
235
330
  language: null,
236
- numbers: false,
237
- copy: false
331
+ lines: false,
332
+ copy: false,
333
+ edit: false,
334
+ collapse: null, // null = not collapsible; '' = default threshold; '10' = explicit
335
+ from: null
238
336
  };
337
+ if (!languageString || languageString.trim() === '') return attributes;
239
338
 
339
+ const parts = languageString.split(/\s+/);
240
340
  let i = 0;
241
341
  while (i < parts.length) {
242
342
  const part = parts[i];
243
343
 
244
- // Check for attributes
245
- if (part === 'numbers') {
246
- attributes.numbers = true;
247
- i++;
248
- continue;
344
+ if (part === 'lines') { attributes.lines = true; i++; continue; }
345
+ if (part === 'copy') { attributes.copy = true; i++; continue; }
346
+ if (part === 'edit') { attributes.edit = true; i++; continue; }
347
+ if (part === 'collapse') { attributes.collapse = ''; i++; continue; }
348
+ if (part.startsWith('collapse=')) {
349
+ attributes.collapse = part.slice('collapse='.length).replace(/^"|"$/g, '');
350
+ i++; continue;
249
351
  }
250
-
251
- if (part === 'copy') {
252
- attributes.copy = true;
253
- i++;
254
- continue;
352
+ if (part.startsWith('from=')) {
353
+ attributes.from = part.slice('from='.length).replace(/^"|"$/g, '');
354
+ i++; continue;
255
355
  }
256
356
 
257
- // Check for quoted names (e.g., "Example")
258
- if (part.startsWith('"') && part.endsWith('"')) {
259
- // Single word quoted name
357
+ // Quoted name handling single-word "Foo" or multi-word "Foo Bar Baz"
358
+ if (part.startsWith('"') && part.endsWith('"') && part.length > 1) {
260
359
  attributes.title = part.slice(1, -1);
261
- i++;
262
- continue;
263
- } else if (part.startsWith('"')) {
264
- // Multi-word quoted name
360
+ i++; continue;
361
+ }
362
+ if (part.startsWith('"')) {
265
363
  let fullName = part.slice(1);
266
364
  i++;
267
365
  while (i < parts.length) {
268
- const nextPart = parts[i];
269
- if (nextPart.endsWith('"')) {
270
- fullName += ' ' + nextPart.slice(0, -1);
366
+ const next = parts[i];
367
+ if (next.endsWith('"')) {
368
+ fullName += ' ' + next.slice(0, -1);
271
369
  attributes.title = fullName;
272
370
  i++;
273
371
  break;
274
- } else {
275
- fullName += ' ' + nextPart;
276
- i++;
277
372
  }
373
+ fullName += ' ' + next;
374
+ i++;
278
375
  }
279
376
  continue;
280
377
  }
281
378
 
282
- // Store language identifiers (e.g., "css", "javascript", etc.)
283
- // Use the first language identifier found
284
- if (!attributes.language) {
285
- attributes.language = part;
286
- }
379
+ // Unrecognized bareword → treat as language (first one wins)
380
+ if (!attributes.language) attributes.language = part;
287
381
  i++;
288
382
  }
289
383
 
@@ -337,6 +431,16 @@ async function initializeMarkdownPlugin() {
337
431
  return;
338
432
  }
339
433
 
434
+ // Opt-in sanitization. When `.safe` is on the directive
435
+ // (`x-markdown.safe="$x.user.bio"`), parsed HTML is run through
436
+ // DOMPurify before injection. Default is unsanitized — Manifest's
437
+ // design lets authors render raw HTML and custom-element extensions
438
+ // (x-icon, callouts) and directive attributes (x-code, etc.) freely.
439
+ // Use .safe when the markdown
440
+ // source can contain content from untrusted parties (Appwrite
441
+ // collections, API responses, crowdsourced translations, etc.).
442
+ const safe = Array.isArray(modifiers) && modifiers.includes('safe');
443
+
340
444
  // Prerender idempotency: if the page is a prerendered MPA and this
341
445
  // element already has rendered HTML children, the content was baked
342
446
  // at build time and is authoritative for SEO + no-JS users. Skip
@@ -394,6 +498,9 @@ async function initializeMarkdownPlugin() {
394
498
  // Post-process HTML to enable checkboxes (remove disabled attribute)
395
499
  html = enableCheckboxes(html);
396
500
 
501
+ // Apply opt-in DOMPurify sanitization for x-markdown.safe
502
+ html = await maybeSanitizeMarkdownHtml(html, safe);
503
+
397
504
  // Only update if content has changed and isn't empty
398
505
  if (element.innerHTML !== html && html.trim() !== '') {
399
506
  // Create a temporary container to hold the HTML
@@ -567,6 +674,9 @@ async function initializeMarkdownPlugin() {
567
674
  // Post-process HTML to enable checkboxes (remove disabled attribute)
568
675
  html = enableCheckboxes(html);
569
676
 
677
+ // Apply opt-in DOMPurify sanitization for x-markdown.safe
678
+ html = await maybeSanitizeMarkdownHtml(html, safe);
679
+
570
680
  // Only update DOM if HTML actually changed
571
681
  if (el.innerHTML !== html) {
572
682
  // Create temporary container
@@ -649,6 +759,9 @@ async function initializeMarkdownPlugin() {
649
759
  // Post-process HTML to enable checkboxes (remove disabled attribute)
650
760
  html = html.replace(/<input type="checkbox"([^>]*?)disabled([^>]*?)>/g, '<input type="checkbox"$1$2>');
651
761
 
762
+ // Apply opt-in DOMPurify sanitization for x-markdown.safe
763
+ html = await maybeSanitizeMarkdownHtml(html, safe);
764
+
652
765
  // Create temporary container
653
766
  const temp = document.createElement('div');
654
767
  temp.innerHTML = html;