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.
- package/LICENSE +1 -1
- package/lib/manifest.accordion.css +4 -4
- package/lib/manifest.appwrite.auth.js +66 -33
- package/lib/manifest.avatar.css +8 -8
- package/lib/manifest.button.css +7 -7
- package/lib/manifest.checkbox.css +5 -5
- package/lib/manifest.code.css +152 -193
- package/lib/manifest.code.js +841 -881
- package/lib/manifest.code.min.css +1 -1
- package/lib/manifest.colorpicker.css +11 -11
- package/lib/manifest.components.js +25 -155
- package/lib/manifest.css +278 -230
- package/lib/manifest.data.js +46 -2
- package/lib/manifest.dialog.css +2 -2
- package/lib/manifest.divider.css +2 -2
- package/lib/manifest.dropdown.css +9 -9
- package/lib/manifest.form.css +10 -10
- package/lib/manifest.input.css +9 -9
- package/lib/manifest.integrity.json +26 -0
- package/lib/manifest.js +60 -5
- package/lib/manifest.markdown.js +192 -79
- package/lib/manifest.min.css +1 -1
- package/lib/manifest.radio.css +1 -1
- package/lib/manifest.range.css +7 -7
- package/lib/manifest.resize.css +1 -1
- package/lib/manifest.router.js +49 -76
- package/lib/manifest.schema.json +1 -1
- package/lib/manifest.sidebar.css +5 -6
- package/lib/manifest.slides.css +5 -5
- package/lib/manifest.svg.js +75 -5
- package/lib/manifest.switch.css +4 -4
- package/lib/manifest.table.css +4 -4
- package/lib/manifest.theme.css +46 -41
- package/lib/manifest.toast.css +7 -7
- package/lib/manifest.tooltip.css +3 -3
- package/lib/manifest.tooltips.js +41 -0
- package/lib/manifest.typography.css +124 -69
- package/lib/manifest.utilities.css +48 -54
- package/lib/manifest.utilities.js +9 -29
- package/package.json +4 -7
- package/lib/manifest.export.js +0 -535
- package/lib/manifest.virtual.js +0 -319
package/lib/manifest.markdown.js
CHANGED
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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, '&')
|
|
128
|
+
.replace(/"/g, '"')
|
|
129
|
+
.replace(/</g, '<')
|
|
130
|
+
.replace(/>/g, '>');
|
|
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, '&')
|
|
139
|
+
.replace(/</g, '<')
|
|
140
|
+
.replace(/>/g, '>');
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
//
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
if (part === '
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
263
|
-
|
|
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
|
|
269
|
-
if (
|
|
270
|
-
fullName += ' ' +
|
|
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
|
-
//
|
|
283
|
-
|
|
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;
|