@cube-drone/marquee-html-renderer 0.9.3 → 0.10.0
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/dist/profile.js +13 -3
- package/dist/render.d.ts +3 -1
- package/dist/render.js +36 -36
- package/package.json +5 -5
- package/src/profile.ts +14 -3
- package/src/render.ts +37 -36
package/dist/profile.js
CHANGED
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
// guesses trust, never widens a scheme allowlist.
|
|
5
5
|
// Mirror of the Rust renderer's table (rust/html_renderer/src/profile.rs). `apng` and
|
|
6
6
|
// `opus` joined 2026-09-03: ringtome mints both and they rendered as the placeholder.
|
|
7
|
-
const MEDIA_KINDS = {
|
|
7
|
+
const MEDIA_KINDS = Object.assign(Object.create(null), {
|
|
8
8
|
png: "image", jpg: "image", jpeg: "image", gif: "image", webp: "image",
|
|
9
9
|
avif: "image", apng: "image", svg: "image",
|
|
10
10
|
mp3: "audio", ogg: "audio", opus: "audio", wav: "audio", flac: "audio", m4a: "audio",
|
|
11
11
|
mp4: "video", webm: "video",
|
|
12
|
-
};
|
|
12
|
+
});
|
|
13
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
14
|
+
// Read the scheme the way a browser will: the URL parser strips leading C0
|
|
15
|
+
// controls and spaces and drops tabs/newlines anywhere, so
|
|
16
|
+
// "\x01javascript:" and "java\tscript:" are javascript: URLs. Targets with
|
|
17
|
+
// controls are refused outright (see linkAllowed); leading spaces are skipped.
|
|
13
18
|
function scheme(target) {
|
|
14
|
-
const m = /^([A-Za-z][A-Za-z0-9+.-]*):/.exec(target);
|
|
19
|
+
const m = /^ *([A-Za-z][A-Za-z0-9+.-]*):/.exec(target);
|
|
15
20
|
return m ? m[1].toLowerCase() : null;
|
|
16
21
|
}
|
|
17
22
|
/** The bare-web default: https links, extension-sniffed media, no widgets,
|
|
@@ -19,6 +24,11 @@ function scheme(target) {
|
|
|
19
24
|
* degrades inert. */
|
|
20
25
|
export const bareWebProfile = {
|
|
21
26
|
linkAllowed(target) {
|
|
27
|
+
// A lone surrogate isn't a URL anyone can write; refusing it here keeps
|
|
28
|
+
// every anchor and media path well-formed.
|
|
29
|
+
if (/[\x00-\x1f\x7f]/.test(target) || LONE_SURROGATE.test(target)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
22
32
|
const s = scheme(target);
|
|
23
33
|
return s === null || s === "http" || s === "https" || s === "mailto";
|
|
24
34
|
},
|
package/dist/render.d.ts
CHANGED
|
@@ -16,7 +16,9 @@ export declare function render(node: Node, profile?: Profile, options?: RenderOp
|
|
|
16
16
|
* no files, and the grab bag - SIL OFL faces the embedder serves itself
|
|
17
17
|
* (never a third-party CDN: fonts are a tracking vector, care-modes apply).
|
|
18
18
|
* Values are the `face` presentational attribute - the no-stylesheet floor;
|
|
19
|
-
* the mq-font-* class is the ceiling, where the real @font-face lives.
|
|
19
|
+
* the mq-font-* class is the ceiling, where the real @font-face lives.
|
|
20
|
+
* Author-keyed tables like this one have no prototype, so `font=constructor`
|
|
21
|
+
* finds nothing instead of Object's own properties. */
|
|
20
22
|
export declare const FONTS: Record<string, string>;
|
|
21
23
|
/** Which grab-bag faces does this rendered HTML actually wear? Pure string
|
|
22
24
|
* scan of the mq-font-* class contract - feed the result to
|
package/dist/render.js
CHANGED
|
@@ -119,8 +119,10 @@ function isToken(v) {
|
|
|
119
119
|
* no files, and the grab bag - SIL OFL faces the embedder serves itself
|
|
120
120
|
* (never a third-party CDN: fonts are a tracking vector, care-modes apply).
|
|
121
121
|
* Values are the `face` presentational attribute - the no-stylesheet floor;
|
|
122
|
-
* the mq-font-* class is the ceiling, where the real @font-face lives.
|
|
123
|
-
|
|
122
|
+
* the mq-font-* class is the ceiling, where the real @font-face lives.
|
|
123
|
+
* Author-keyed tables like this one have no prototype, so `font=constructor`
|
|
124
|
+
* finds nothing instead of Object's own properties. */
|
|
125
|
+
export const FONTS = Object.assign(Object.create(null), {
|
|
124
126
|
// standard stacks
|
|
125
127
|
sans: "sans-serif",
|
|
126
128
|
serif: "serif",
|
|
@@ -158,7 +160,7 @@ export const FONTS = {
|
|
|
158
160
|
rye: "Rye",
|
|
159
161
|
bitcount: "Bitcount",
|
|
160
162
|
quicksand: "Quicksand",
|
|
161
|
-
};
|
|
163
|
+
});
|
|
162
164
|
/** Which grab-bag faces does this rendered HTML actually wear? Pure string
|
|
163
165
|
* scan of the mq-font-* class contract - feed the result to
|
|
164
166
|
* @cube-drone/marquee-fonts (externalFontFaces / inlineFontFaces) to deliver
|
|
@@ -193,12 +195,12 @@ const SCHEME_FONTS = [
|
|
|
193
195
|
function sizeRung(value, inner) {
|
|
194
196
|
return `<font class="mq-size-${value}" size="${value}">${inner}</font>`;
|
|
195
197
|
}
|
|
196
|
-
const MEDIA_SIZE_TOKENS = {
|
|
198
|
+
const MEDIA_SIZE_TOKENS = Object.assign(Object.create(null), {
|
|
197
199
|
small: "10rem",
|
|
198
200
|
medium: "20rem",
|
|
199
201
|
large: "32rem",
|
|
200
202
|
full: "100%",
|
|
201
|
-
};
|
|
203
|
+
});
|
|
202
204
|
/** Media width/height: a size token or a capped integer of pixels (SPEC.md,
|
|
203
205
|
* "Media"). Anything else degrades to natural sizing. */
|
|
204
206
|
function mediaSize(v) {
|
|
@@ -274,8 +276,10 @@ function turbolink(target, levelAttr, ctx) {
|
|
|
274
276
|
* that encodeURI deliberately leaves are percent-encoded on top, so the
|
|
275
277
|
* value can never terminate the url() or the declaration - author bytes
|
|
276
278
|
* must not write CSS, even inside their own style attribute. */
|
|
279
|
+
// encodeURI throws on a lone surrogate; an embedder's media() may pass one.
|
|
280
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
277
281
|
function cssUrl(url) {
|
|
278
|
-
return encodeURI(url).replaceAll("'", "%27").replaceAll("(", "%28").replaceAll(")", "%29");
|
|
282
|
+
return encodeURI(url.replace(LONE_SURROGATE, "\uFFFD")).replaceAll("'", "%27").replaceAll("(", "%28").replaceAll(")", "%29");
|
|
279
283
|
}
|
|
280
284
|
/** Style knobs on a block node: validated values into --mq-* slots; the
|
|
281
285
|
* stylesheet owns which CSS property each slot feeds. `background` takes a
|
|
@@ -306,9 +310,12 @@ function styleVarList(attrs, profile) {
|
|
|
306
310
|
}
|
|
307
311
|
return vars.join(";");
|
|
308
312
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
313
|
+
/** The style attribute, attribute-escaped like any other: the browser
|
|
314
|
+
* decodes character references before CSS sees the value, so a raw `'`
|
|
315
|
+
* in a tile URL would otherwise close the url() and write CSS. */
|
|
316
|
+
function styleVars(attrs, ctx) {
|
|
317
|
+
const list = styleVarList(attrs, ctx.profile);
|
|
318
|
+
return list === "" ? "" : ` style="${ctx.attr(list)}"`;
|
|
312
319
|
}
|
|
313
320
|
/** The visual identity a layout container (:::page / :::section) wears -
|
|
314
321
|
* its scheme/font classes and validated style knobs - as bare class/style
|
|
@@ -331,14 +338,13 @@ function fontClass(attrs) {
|
|
|
331
338
|
}
|
|
332
339
|
function directive(name, attrs, nodes, ctx) {
|
|
333
340
|
const { profile } = ctx;
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
ctx
|
|
340
|
-
|
|
341
|
-
};
|
|
341
|
+
// Built-ins that lay their children out themselves (table, stack) probe
|
|
342
|
+
// the hook (it claims on name and attrs alone) instead of rendering the
|
|
343
|
+
// children for it: rendering them twice per level is exponential in
|
|
344
|
+
// nesting, and a note inside would count twice.
|
|
345
|
+
if ((name === "table" || name === "stack") && profile.directive(name, attrs, "") === null) {
|
|
346
|
+
return name === "table" ? renderTable(attrs, nodes, ctx) : renderStack(attrs, nodes, ctx);
|
|
347
|
+
}
|
|
342
348
|
const inner = children(nodes, ctx);
|
|
343
349
|
const custom = profile.directive(name, attrs, inner);
|
|
344
350
|
if (custom !== null) {
|
|
@@ -351,11 +357,11 @@ function directive(name, attrs, nodes, ctx) {
|
|
|
351
357
|
return inner;
|
|
352
358
|
case "page": {
|
|
353
359
|
const layout = isToken(attrs["layout"]) ? ` mq-layout-${attrs["layout"]}` : "";
|
|
354
|
-
return `<div class="mq-page${layout}${schemeClass(attrs)}${fontClass(attrs)}"${styleVars(attrs,
|
|
360
|
+
return `<div class="mq-page${layout}${schemeClass(attrs)}${fontClass(attrs)}"${styleVars(attrs, ctx)}>${inner}</div>`;
|
|
355
361
|
}
|
|
356
362
|
case "section": {
|
|
357
363
|
const slot = isToken(attrs["slot"]) ? ` data-slot="${attrs["slot"]}"` : "";
|
|
358
|
-
return `<section class="mq-section${schemeClass(attrs)}${fontClass(attrs)}"${slot}${styleVars(attrs,
|
|
364
|
+
return `<section class="mq-section${schemeClass(attrs)}${fontClass(attrs)}"${slot}${styleVars(attrs, ctx)}>${inner}</section>`;
|
|
359
365
|
}
|
|
360
366
|
case "turbolink": {
|
|
361
367
|
const target = attrs["target"];
|
|
@@ -377,16 +383,10 @@ function directive(name, attrs, nodes, ctx) {
|
|
|
377
383
|
const style = vars.length === 0 ? "" : ` style="${vars.join(";")}"`;
|
|
378
384
|
return `<div class="mq-media"${style}>${inner}</div>`;
|
|
379
385
|
}
|
|
380
|
-
case "table":
|
|
381
|
-
rewind();
|
|
382
|
-
return renderTable(attrs, nodes, ctx);
|
|
383
|
-
case "stack":
|
|
384
|
-
rewind();
|
|
385
|
-
return renderStack(attrs, nodes, ctx);
|
|
386
386
|
case "layer":
|
|
387
387
|
// Outside a stack a layer is just a section-like container: its style
|
|
388
388
|
// knobs apply, place/backing mean nothing.
|
|
389
|
-
return `<div class="mq-layer${schemeClass(attrs)}${fontClass(attrs)}"${styleVars(attrs,
|
|
389
|
+
return `<div class="mq-layer${schemeClass(attrs)}${fontClass(attrs)}"${styleVars(attrs, ctx)}>${inner}</div>`;
|
|
390
390
|
case "center":
|
|
391
391
|
case "right":
|
|
392
392
|
case "left":
|
|
@@ -482,13 +482,13 @@ const PLACES = new Set([
|
|
|
482
482
|
"fill", "top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right",
|
|
483
483
|
]);
|
|
484
484
|
/** The `aspect` vocabulary as CSS aspect-ratio values. */
|
|
485
|
-
const STACK_ASPECTS = {
|
|
485
|
+
const STACK_ASPECTS = Object.assign(Object.create(null), {
|
|
486
486
|
"16:9": "16/9",
|
|
487
487
|
"4:3": "4/3",
|
|
488
488
|
"3:2": "3/2",
|
|
489
489
|
"1:1": "1/1",
|
|
490
490
|
card: "5/7",
|
|
491
|
-
};
|
|
491
|
+
});
|
|
492
492
|
/** :::stack (SPEC.md, "Stacks"): every direct child is a layer, source order
|
|
493
493
|
* is z-order. The stylesheet puts every layer in one grid cell, so the box
|
|
494
494
|
* is as big as its biggest layer needs and paint order is DOM order: no
|
|
@@ -528,7 +528,7 @@ function renderLayer(node, ctx) {
|
|
|
528
528
|
? " mq-layer-cover"
|
|
529
529
|
: "";
|
|
530
530
|
const inner = children(content, ctx);
|
|
531
|
-
return `<div class="mq-layer mq-place-${place}${backing}${cover}${schemeClass(attrs)}${fontClass(attrs)}"${styleVars(attrs, ctx
|
|
531
|
+
return `<div class="mq-layer mq-place-${place}${backing}${cover}${schemeClass(attrs)}${fontClass(attrs)}"${styleVars(attrs, ctx)}>${inner}</div>`;
|
|
532
532
|
}
|
|
533
533
|
/** Effect/typographic spans. The `<font>` tag is deliberate: presentational
|
|
534
534
|
* attributes are outside CSP's jurisdiction and are implemented by the
|
|
@@ -536,19 +536,19 @@ function renderLayer(node, ctx) {
|
|
|
536
536
|
* attributes - the floor of the degradation ladder. The --mq-color slot is
|
|
537
537
|
* the stylesheet-era ceiling on the same element. */
|
|
538
538
|
function span(name, attrs, nodes, ctx) {
|
|
539
|
-
|
|
539
|
+
// Probe first (the hook claims on name and attrs alone): a split renders
|
|
540
|
+
// its children itself, and rendering them here as well doubled the work at
|
|
541
|
+
// every nesting level - exponential in a document anyone can sync.
|
|
542
|
+
const look = ctx.profile.span(name, attrs, "") === null ? effectLook(name, attrs, nodes) : null;
|
|
543
|
+
if (look !== null && look.units !== null) {
|
|
544
|
+
return renderEffect(name, look, nodes, "", ctx);
|
|
545
|
+
}
|
|
540
546
|
const inner = children(nodes, ctx);
|
|
541
547
|
const custom = ctx.profile.span(name, attrs, inner);
|
|
542
548
|
if (custom !== null) {
|
|
543
549
|
return custom;
|
|
544
550
|
}
|
|
545
|
-
const look = effectLook(name, attrs, nodes);
|
|
546
551
|
if (look !== null) {
|
|
547
|
-
if (look.units !== null) {
|
|
548
|
-
// A split renders its children again: rewind the aside numbering.
|
|
549
|
-
ctx.note.n = saved.n;
|
|
550
|
-
ctx.note.pending = [...saved.pending];
|
|
551
|
-
}
|
|
552
552
|
return renderEffect(name, look, nodes, inner, ctx);
|
|
553
553
|
}
|
|
554
554
|
switch (name) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cube-drone/marquee-html-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Reference static HTML renderer for the Marquee markup language",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"prepack": "npm run build"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@cube-drone/marquee-parser": "^0.
|
|
38
|
+
"@cube-drone/marquee-parser": "^0.10.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@cube-drone/marquee-css": "^0.
|
|
42
|
-
"@cube-drone/marquee-fonts": "^0.
|
|
43
|
-
"@cube-drone/marquee-turbolink": "^0.
|
|
41
|
+
"@cube-drone/marquee-css": "^0.10.0",
|
|
42
|
+
"@cube-drone/marquee-fonts": "^0.10.0",
|
|
43
|
+
"@cube-drone/marquee-turbolink": "^0.10.0",
|
|
44
44
|
"@types/node": "^24.0.0",
|
|
45
45
|
"saxes": "^6.0.0",
|
|
46
46
|
"typescript": "^5.9.0"
|
package/src/profile.ts
CHANGED
|
@@ -76,15 +76,21 @@ export interface Profile {
|
|
|
76
76
|
|
|
77
77
|
// Mirror of the Rust renderer's table (rust/html_renderer/src/profile.rs). `apng` and
|
|
78
78
|
// `opus` joined 2026-09-03: ringtome mints both and they rendered as the placeholder.
|
|
79
|
-
const MEDIA_KINDS: Record<string, MediaResolution["kind"]> = {
|
|
79
|
+
const MEDIA_KINDS: Record<string, MediaResolution["kind"]> = Object.assign(Object.create(null), {
|
|
80
80
|
png: "image", jpg: "image", jpeg: "image", gif: "image", webp: "image",
|
|
81
81
|
avif: "image", apng: "image", svg: "image",
|
|
82
82
|
mp3: "audio", ogg: "audio", opus: "audio", wav: "audio", flac: "audio", m4a: "audio",
|
|
83
83
|
mp4: "video", webm: "video",
|
|
84
|
-
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
85
87
|
|
|
88
|
+
// Read the scheme the way a browser will: the URL parser strips leading C0
|
|
89
|
+
// controls and spaces and drops tabs/newlines anywhere, so
|
|
90
|
+
// "\x01javascript:" and "java\tscript:" are javascript: URLs. Targets with
|
|
91
|
+
// controls are refused outright (see linkAllowed); leading spaces are skipped.
|
|
86
92
|
function scheme(target: string): string | null {
|
|
87
|
-
const m = /^([A-Za-z][A-Za-z0-9+.-]*):/.exec(target);
|
|
93
|
+
const m = /^ *([A-Za-z][A-Za-z0-9+.-]*):/.exec(target);
|
|
88
94
|
return m ? m[1]!.toLowerCase() : null;
|
|
89
95
|
}
|
|
90
96
|
|
|
@@ -93,6 +99,11 @@ function scheme(target: string): string | null {
|
|
|
93
99
|
* degrades inert. */
|
|
94
100
|
export const bareWebProfile: Profile = {
|
|
95
101
|
linkAllowed(target) {
|
|
102
|
+
// A lone surrogate isn't a URL anyone can write; refusing it here keeps
|
|
103
|
+
// every anchor and media path well-formed.
|
|
104
|
+
if (/[\x00-\x1f\x7f]/.test(target) || LONE_SURROGATE.test(target)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
96
107
|
const s = scheme(target);
|
|
97
108
|
return s === null || s === "http" || s === "https" || s === "mailto";
|
|
98
109
|
},
|
package/src/render.ts
CHANGED
|
@@ -161,8 +161,10 @@ function isToken(v: string | undefined): v is string {
|
|
|
161
161
|
* no files, and the grab bag - SIL OFL faces the embedder serves itself
|
|
162
162
|
* (never a third-party CDN: fonts are a tracking vector, care-modes apply).
|
|
163
163
|
* Values are the `face` presentational attribute - the no-stylesheet floor;
|
|
164
|
-
* the mq-font-* class is the ceiling, where the real @font-face lives.
|
|
165
|
-
|
|
164
|
+
* the mq-font-* class is the ceiling, where the real @font-face lives.
|
|
165
|
+
* Author-keyed tables like this one have no prototype, so `font=constructor`
|
|
166
|
+
* finds nothing instead of Object's own properties. */
|
|
167
|
+
export const FONTS: Record<string, string> = Object.assign(Object.create(null), {
|
|
166
168
|
// standard stacks
|
|
167
169
|
sans: "sans-serif",
|
|
168
170
|
serif: "serif",
|
|
@@ -200,7 +202,7 @@ export const FONTS: Record<string, string> = {
|
|
|
200
202
|
rye: "Rye",
|
|
201
203
|
bitcount: "Bitcount",
|
|
202
204
|
quicksand: "Quicksand",
|
|
203
|
-
};
|
|
205
|
+
});
|
|
204
206
|
|
|
205
207
|
/** Which grab-bag faces does this rendered HTML actually wear? Pure string
|
|
206
208
|
* scan of the mq-font-* class contract - feed the result to
|
|
@@ -239,12 +241,12 @@ function sizeRung(value: string, inner: string): string {
|
|
|
239
241
|
return `<font class="mq-size-${value}" size="${value}">${inner}</font>`;
|
|
240
242
|
}
|
|
241
243
|
|
|
242
|
-
const MEDIA_SIZE_TOKENS: Record<string, string> = {
|
|
244
|
+
const MEDIA_SIZE_TOKENS: Record<string, string> = Object.assign(Object.create(null), {
|
|
243
245
|
small: "10rem",
|
|
244
246
|
medium: "20rem",
|
|
245
247
|
large: "32rem",
|
|
246
248
|
full: "100%",
|
|
247
|
-
};
|
|
249
|
+
});
|
|
248
250
|
|
|
249
251
|
/** Media width/height: a size token or a capped integer of pixels (SPEC.md,
|
|
250
252
|
* "Media"). Anything else degrades to natural sizing. */
|
|
@@ -328,8 +330,11 @@ function turbolink(target: string, levelAttr: string | undefined, ctx: Ctx): str
|
|
|
328
330
|
* that encodeURI deliberately leaves are percent-encoded on top, so the
|
|
329
331
|
* value can never terminate the url() or the declaration - author bytes
|
|
330
332
|
* must not write CSS, even inside their own style attribute. */
|
|
333
|
+
// encodeURI throws on a lone surrogate; an embedder's media() may pass one.
|
|
334
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
335
|
+
|
|
331
336
|
function cssUrl(url: string): string {
|
|
332
|
-
return encodeURI(url).replaceAll("'", "%27").replaceAll("(", "%28").replaceAll(")", "%29");
|
|
337
|
+
return encodeURI(url.replace(LONE_SURROGATE, "\uFFFD")).replaceAll("'", "%27").replaceAll("(", "%28").replaceAll(")", "%29");
|
|
333
338
|
}
|
|
334
339
|
|
|
335
340
|
/** Style knobs on a block node: validated values into --mq-* slots; the
|
|
@@ -361,9 +366,12 @@ function styleVarList(attrs: Attrs, profile: Profile): string {
|
|
|
361
366
|
return vars.join(";");
|
|
362
367
|
}
|
|
363
368
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
369
|
+
/** The style attribute, attribute-escaped like any other: the browser
|
|
370
|
+
* decodes character references before CSS sees the value, so a raw `'`
|
|
371
|
+
* in a tile URL would otherwise close the url() and write CSS. */
|
|
372
|
+
function styleVars(attrs: Attrs, ctx: Ctx): string {
|
|
373
|
+
const list = styleVarList(attrs, ctx.profile);
|
|
374
|
+
return list === "" ? "" : ` style="${ctx.attr(list)}"`;
|
|
367
375
|
}
|
|
368
376
|
|
|
369
377
|
/** The visual identity a layout container (:::page / :::section) wears -
|
|
@@ -390,14 +398,13 @@ function fontClass(attrs: Attrs): string {
|
|
|
390
398
|
|
|
391
399
|
function directive(name: string, attrs: Attrs, nodes: Node[], ctx: Ctx): string {
|
|
392
400
|
const { profile } = ctx;
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
ctx
|
|
399
|
-
|
|
400
|
-
};
|
|
401
|
+
// Built-ins that lay their children out themselves (table, stack) probe
|
|
402
|
+
// the hook (it claims on name and attrs alone) instead of rendering the
|
|
403
|
+
// children for it: rendering them twice per level is exponential in
|
|
404
|
+
// nesting, and a note inside would count twice.
|
|
405
|
+
if ((name === "table" || name === "stack") && profile.directive(name, attrs, "") === null) {
|
|
406
|
+
return name === "table" ? renderTable(attrs, nodes, ctx) : renderStack(attrs, nodes, ctx);
|
|
407
|
+
}
|
|
401
408
|
const inner = children(nodes, ctx);
|
|
402
409
|
const custom = profile.directive(name, attrs, inner);
|
|
403
410
|
if (custom !== null) {
|
|
@@ -410,11 +417,11 @@ function directive(name: string, attrs: Attrs, nodes: Node[], ctx: Ctx): string
|
|
|
410
417
|
return inner;
|
|
411
418
|
case "page": {
|
|
412
419
|
const layout = isToken(attrs["layout"]) ? ` mq-layout-${attrs["layout"]}` : "";
|
|
413
|
-
return `<div class="mq-page${layout}${schemeClass(attrs)}${fontClass(attrs)}"${styleVars(attrs,
|
|
420
|
+
return `<div class="mq-page${layout}${schemeClass(attrs)}${fontClass(attrs)}"${styleVars(attrs, ctx)}>${inner}</div>`;
|
|
414
421
|
}
|
|
415
422
|
case "section": {
|
|
416
423
|
const slot = isToken(attrs["slot"]) ? ` data-slot="${attrs["slot"]}"` : "";
|
|
417
|
-
return `<section class="mq-section${schemeClass(attrs)}${fontClass(attrs)}"${slot}${styleVars(attrs,
|
|
424
|
+
return `<section class="mq-section${schemeClass(attrs)}${fontClass(attrs)}"${slot}${styleVars(attrs, ctx)}>${inner}</section>`;
|
|
418
425
|
}
|
|
419
426
|
case "turbolink": {
|
|
420
427
|
const target = attrs["target"];
|
|
@@ -436,16 +443,10 @@ function directive(name: string, attrs: Attrs, nodes: Node[], ctx: Ctx): string
|
|
|
436
443
|
const style = vars.length === 0 ? "" : ` style="${vars.join(";")}"`;
|
|
437
444
|
return `<div class="mq-media"${style}>${inner}</div>`;
|
|
438
445
|
}
|
|
439
|
-
case "table":
|
|
440
|
-
rewind();
|
|
441
|
-
return renderTable(attrs, nodes, ctx);
|
|
442
|
-
case "stack":
|
|
443
|
-
rewind();
|
|
444
|
-
return renderStack(attrs, nodes, ctx);
|
|
445
446
|
case "layer":
|
|
446
447
|
// Outside a stack a layer is just a section-like container: its style
|
|
447
448
|
// knobs apply, place/backing mean nothing.
|
|
448
|
-
return `<div class="mq-layer${schemeClass(attrs)}${fontClass(attrs)}"${styleVars(attrs,
|
|
449
|
+
return `<div class="mq-layer${schemeClass(attrs)}${fontClass(attrs)}"${styleVars(attrs, ctx)}>${inner}</div>`;
|
|
449
450
|
case "center":
|
|
450
451
|
case "right":
|
|
451
452
|
case "left":
|
|
@@ -545,13 +546,13 @@ const PLACES = new Set([
|
|
|
545
546
|
]);
|
|
546
547
|
|
|
547
548
|
/** The `aspect` vocabulary as CSS aspect-ratio values. */
|
|
548
|
-
const STACK_ASPECTS: Record<string, string> = {
|
|
549
|
+
const STACK_ASPECTS: Record<string, string> = Object.assign(Object.create(null), {
|
|
549
550
|
"16:9": "16/9",
|
|
550
551
|
"4:3": "4/3",
|
|
551
552
|
"3:2": "3/2",
|
|
552
553
|
"1:1": "1/1",
|
|
553
554
|
card: "5/7",
|
|
554
|
-
};
|
|
555
|
+
});
|
|
555
556
|
|
|
556
557
|
/** :::stack (SPEC.md, "Stacks"): every direct child is a layer, source order
|
|
557
558
|
* is z-order. The stylesheet puts every layer in one grid cell, so the box
|
|
@@ -594,7 +595,7 @@ function renderLayer(node: Node, ctx: Ctx): string {
|
|
|
594
595
|
? " mq-layer-cover"
|
|
595
596
|
: "";
|
|
596
597
|
const inner = children(content, ctx);
|
|
597
|
-
return `<div class="mq-layer mq-place-${place}${backing}${cover}${schemeClass(attrs)}${fontClass(attrs)}"${styleVars(attrs, ctx
|
|
598
|
+
return `<div class="mq-layer mq-place-${place}${backing}${cover}${schemeClass(attrs)}${fontClass(attrs)}"${styleVars(attrs, ctx)}>${inner}</div>`;
|
|
598
599
|
}
|
|
599
600
|
|
|
600
601
|
/** Effect/typographic spans. The `<font>` tag is deliberate: presentational
|
|
@@ -603,19 +604,19 @@ function renderLayer(node: Node, ctx: Ctx): string {
|
|
|
603
604
|
* attributes - the floor of the degradation ladder. The --mq-color slot is
|
|
604
605
|
* the stylesheet-era ceiling on the same element. */
|
|
605
606
|
function span(name: string, attrs: Attrs, nodes: Node[], ctx: Ctx): string {
|
|
606
|
-
|
|
607
|
+
// Probe first (the hook claims on name and attrs alone): a split renders
|
|
608
|
+
// its children itself, and rendering them here as well doubled the work at
|
|
609
|
+
// every nesting level - exponential in a document anyone can sync.
|
|
610
|
+
const look = ctx.profile.span(name, attrs, "") === null ? effectLook(name, attrs, nodes) : null;
|
|
611
|
+
if (look !== null && look.units !== null) {
|
|
612
|
+
return renderEffect(name, look, nodes, "", ctx);
|
|
613
|
+
}
|
|
607
614
|
const inner = children(nodes, ctx);
|
|
608
615
|
const custom = ctx.profile.span(name, attrs, inner);
|
|
609
616
|
if (custom !== null) {
|
|
610
617
|
return custom;
|
|
611
618
|
}
|
|
612
|
-
const look = effectLook(name, attrs, nodes);
|
|
613
619
|
if (look !== null) {
|
|
614
|
-
if (look.units !== null) {
|
|
615
|
-
// A split renders its children again: rewind the aside numbering.
|
|
616
|
-
ctx.note.n = saved.n;
|
|
617
|
-
ctx.note.pending = [...saved.pending];
|
|
618
|
-
}
|
|
619
620
|
return renderEffect(name, look, nodes, inner, ctx);
|
|
620
621
|
}
|
|
621
622
|
switch (name) {
|