@cube-drone/marquee-html-renderer 0.8.0 → 0.9.1
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/effects.d.ts +47 -0
- package/dist/effects.js +252 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/render.js +127 -175
- package/package.json +5 -5
- package/src/effects.ts +316 -0
- package/src/index.ts +1 -0
- package/src/render.ts +138 -207
package/src/render.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { Attrs, Node } from "@cube-drone/marquee-parser";
|
|
8
8
|
import { bareWebProfile, type Profile, type TurbolinkLevel } from "./profile.ts";
|
|
9
|
+
import { effectLook, splitPieces, type EffectLook, type SplitPiece } from "./effects.ts";
|
|
9
10
|
|
|
10
11
|
/** Render state: the profile, plus the one piece of cross-block
|
|
11
12
|
* coordination the renderer owns - aside numbering (sequential through the
|
|
@@ -175,9 +176,26 @@ export function usedFontTokens(html: string): string[] {
|
|
|
175
176
|
for (const m of html.matchAll(/mq-font-([a-z0-9-]+)/g)) {
|
|
176
177
|
used.add(m[1]!);
|
|
177
178
|
}
|
|
179
|
+
// A scheme that wears a grab-bag face of its own needs that face shipped.
|
|
180
|
+
for (const m of html.matchAll(/mq-scheme-([a-z0-9-]+)/g)) {
|
|
181
|
+
for (const [scheme, token] of SCHEME_FONTS) {
|
|
182
|
+
if (m[1] === scheme || m[1]!.startsWith(`${scheme}-`)) {
|
|
183
|
+
used.add(token);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
178
187
|
return [...used].sort();
|
|
179
188
|
}
|
|
180
189
|
|
|
190
|
+
/** Schemes (and their `-flavor` variants) that wear a grab-bag face by
|
|
191
|
+
* default. Kept in lockstep with marquee.css and the Rust renderer. */
|
|
192
|
+
const SCHEME_FONTS: [string, string][] = [
|
|
193
|
+
["earthbound", "press-start"],
|
|
194
|
+
["crosscode", "vt323"],
|
|
195
|
+
["ff6", "vt323"],
|
|
196
|
+
["chronotrigger", "vt323"],
|
|
197
|
+
];
|
|
198
|
+
|
|
181
199
|
/** One rung of the font-element seven-step dial: presentational floor
|
|
182
200
|
* (works with no stylesheet, under any CSP), stylesheet class as ceiling.
|
|
183
201
|
* The named rungs - teeny, tiny, huge, enormous - are unnecessary
|
|
@@ -297,6 +315,11 @@ function styleVarList(attrs: Attrs, profile: Profile): string {
|
|
|
297
315
|
} else if (isColorValue(bg)) {
|
|
298
316
|
vars.push(`--mq-bg:${bg}`);
|
|
299
317
|
}
|
|
318
|
+
// A scheme's signature color - the thing its game let you customize (an
|
|
319
|
+
// EarthBound frame, an FF6 window). Schemes without one ignore it.
|
|
320
|
+
if (isColorValue(attrs["accent"])) {
|
|
321
|
+
vars.push(`--mq-accent:${attrs["accent"]}`);
|
|
322
|
+
}
|
|
300
323
|
return vars.join(";");
|
|
301
324
|
}
|
|
302
325
|
|
|
@@ -329,6 +352,14 @@ function fontClass(attrs: Attrs): string {
|
|
|
329
352
|
|
|
330
353
|
function directive(name: string, attrs: Attrs, nodes: Node[], ctx: Ctx): string {
|
|
331
354
|
const { profile } = ctx;
|
|
355
|
+
// The profile hook sees the children rendered; built-ins that lay their
|
|
356
|
+
// children out themselves (table, stack) render them again, so rewind the
|
|
357
|
+
// aside numbering first - or a note inside counts twice.
|
|
358
|
+
const saved = { n: ctx.note.n, pending: [...ctx.note.pending] };
|
|
359
|
+
const rewind = () => {
|
|
360
|
+
ctx.note.n = saved.n;
|
|
361
|
+
ctx.note.pending = [...saved.pending];
|
|
362
|
+
};
|
|
332
363
|
const inner = children(nodes, ctx);
|
|
333
364
|
const custom = profile.directive(name, attrs, inner);
|
|
334
365
|
if (custom !== null) {
|
|
@@ -368,7 +399,15 @@ function directive(name: string, attrs: Attrs, nodes: Node[], ctx: Ctx): string
|
|
|
368
399
|
return `<div class="mq-media"${style}>${inner}</div>`;
|
|
369
400
|
}
|
|
370
401
|
case "table":
|
|
402
|
+
rewind();
|
|
371
403
|
return renderTable(attrs, nodes, ctx);
|
|
404
|
+
case "stack":
|
|
405
|
+
rewind();
|
|
406
|
+
return renderStack(attrs, nodes, ctx);
|
|
407
|
+
case "layer":
|
|
408
|
+
// Outside a stack a layer is just a section-like container: its style
|
|
409
|
+
// knobs apply, place/backing mean nothing.
|
|
410
|
+
return `<div class="mq-layer${schemeClass(attrs)}${fontClass(attrs)}"${styleVars(attrs, profile)}>${inner}</div>`;
|
|
372
411
|
case "center":
|
|
373
412
|
case "right":
|
|
374
413
|
case "left":
|
|
@@ -460,17 +499,87 @@ function renderTable(attrs: Attrs, nodes: Node[], ctx: Ctx): string {
|
|
|
460
499
|
return flushNotes(ctx, `<table class="mq-table">${rows.join("")}</table>`);
|
|
461
500
|
}
|
|
462
501
|
|
|
502
|
+
/** A stack paints at most this many layers; the rest flow below the box. */
|
|
503
|
+
const MAX_LAYERS = 8;
|
|
504
|
+
|
|
505
|
+
const PLACES = new Set([
|
|
506
|
+
"fill", "top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right",
|
|
507
|
+
]);
|
|
508
|
+
|
|
509
|
+
/** The `aspect` vocabulary as CSS aspect-ratio values. */
|
|
510
|
+
const STACK_ASPECTS: Record<string, string> = {
|
|
511
|
+
"16:9": "16/9",
|
|
512
|
+
"4:3": "4/3",
|
|
513
|
+
"3:2": "3/2",
|
|
514
|
+
"1:1": "1/1",
|
|
515
|
+
card: "5/7",
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
/** :::stack (SPEC.md, "Stacks"): every direct child is a layer, source order
|
|
519
|
+
* is z-order. The stylesheet puts every layer in one grid cell, so the box
|
|
520
|
+
* is as big as its biggest layer needs and paint order is DOM order: no
|
|
521
|
+
* z-index, no absolute positioning, nothing clipped. Layers past the cap
|
|
522
|
+
* render in flow below the box. */
|
|
523
|
+
function renderStack(attrs: Attrs, nodes: Node[], ctx: Ctx): string {
|
|
524
|
+
const vars: string[] = [];
|
|
525
|
+
const aspect = Object.hasOwn(STACK_ASPECTS, attrs["aspect"] ?? "") ? STACK_ASPECTS[attrs["aspect"]!] : undefined;
|
|
526
|
+
if (aspect !== undefined) {
|
|
527
|
+
vars.push(`--mq-stack-aspect:${aspect}`);
|
|
528
|
+
}
|
|
529
|
+
const w = mediaSize(attrs["width"]);
|
|
530
|
+
if (w !== null) {
|
|
531
|
+
vars.push(`--mq-stack-w:${w}`);
|
|
532
|
+
}
|
|
533
|
+
const style = vars.length === 0 ? "" : ` style="${vars.join(";")}"`;
|
|
534
|
+
const layers = nodes.slice(0, MAX_LAYERS).map((n) => renderLayer(n, ctx)).join("");
|
|
535
|
+
const rest = nodes.slice(MAX_LAYERS);
|
|
536
|
+
const restHtml = rest.length === 0 ? "" : `<div class="mq-stack-rest">${children(rest, ctx)}</div>`;
|
|
537
|
+
// The class tells the stylesheet a cover base fills the box rather than
|
|
538
|
+
// sizing it.
|
|
539
|
+
const cls = aspect === undefined ? "mq-stack" : "mq-stack mq-stack-aspect";
|
|
540
|
+
return `<div class="${cls}"${style}>${layers}</div>${restHtml}`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** One layer of a stack: a `:::layer` child carries knobs; any other child
|
|
544
|
+
* block is a layer with the defaults (`fill`, no backing). */
|
|
545
|
+
function renderLayer(node: Node, ctx: Ctx): string {
|
|
546
|
+
const isLayer = node.type === "directive" && node.name === "layer";
|
|
547
|
+
const attrs: Attrs = isLayer ? node.attrs : {};
|
|
548
|
+
const content: Node[] = isLayer ? node.children : [node];
|
|
549
|
+
const place = PLACES.has(attrs["place"] ?? "") ? attrs["place"]! : "fill";
|
|
550
|
+
const backing = attrs["backing"] === "box" || attrs["backing"] === "outline" ? ` mq-backing-${attrs["backing"]}` : "";
|
|
551
|
+
// A fill layer that is exactly one embed is that media covering the box -
|
|
552
|
+
// the one crop the spec permits, of pictures, never of words.
|
|
553
|
+
const only = content.length === 1 ? content[0]! : undefined;
|
|
554
|
+
const cover =
|
|
555
|
+
place === "fill" && only?.type === "paragraph" && only.children.length === 1 && only.children[0]!.type === "embed"
|
|
556
|
+
? " mq-layer-cover"
|
|
557
|
+
: "";
|
|
558
|
+
const inner = children(content, ctx);
|
|
559
|
+
return `<div class="mq-layer mq-place-${place}${backing}${cover}${schemeClass(attrs)}${fontClass(attrs)}"${styleVars(attrs, ctx.profile)}>${inner}</div>`;
|
|
560
|
+
}
|
|
561
|
+
|
|
463
562
|
/** Effect/typographic spans. The `<font>` tag is deliberate: presentational
|
|
464
563
|
* attributes are outside CSP's jurisdiction and are implemented by the
|
|
465
564
|
* browser itself, so color survives with no stylesheet and no style
|
|
466
565
|
* attributes - the floor of the degradation ladder. The --mq-color slot is
|
|
467
566
|
* the stylesheet-era ceiling on the same element. */
|
|
468
567
|
function span(name: string, attrs: Attrs, nodes: Node[], ctx: Ctx): string {
|
|
568
|
+
const saved = { n: ctx.note.n, pending: [...ctx.note.pending] };
|
|
469
569
|
const inner = children(nodes, ctx);
|
|
470
570
|
const custom = ctx.profile.span(name, attrs, inner);
|
|
471
571
|
if (custom !== null) {
|
|
472
572
|
return custom;
|
|
473
573
|
}
|
|
574
|
+
const look = effectLook(name, attrs, nodes);
|
|
575
|
+
if (look !== null) {
|
|
576
|
+
if (look.units !== null) {
|
|
577
|
+
// A split renders its children again: rewind the aside numbering.
|
|
578
|
+
ctx.note.n = saved.n;
|
|
579
|
+
ctx.note.pending = [...saved.pending];
|
|
580
|
+
}
|
|
581
|
+
return renderEffect(name, look, nodes, inner, ctx);
|
|
582
|
+
}
|
|
474
583
|
switch (name) {
|
|
475
584
|
case "sup":
|
|
476
585
|
return `<sup>${inner}</sup>`;
|
|
@@ -530,225 +639,47 @@ function span(name: string, attrs: Attrs, nodes: Node[], ctx: Ctx): string {
|
|
|
530
639
|
ctx.note.pending.push(`<span class="mq-note-num">${ctx.note.n}</span>${inner}`);
|
|
531
640
|
return `<sup class="mq-noteref">${ctx.note.n}</sup>`;
|
|
532
641
|
}
|
|
533
|
-
case "marquee": {
|
|
534
|
-
const dir = isToken(attrs["direction"]) ? ` data-direction="${attrs["direction"]}"` : "";
|
|
535
|
-
const speed = attrs["speed"] !== undefined && COUNT.test(attrs["speed"])
|
|
536
|
-
? ` style="--mq-speed:${attrs["speed"]}"`
|
|
537
|
-
: "";
|
|
538
|
-
return `<span class="mq-marquee"${dir}${speed}><span class="mq-marquee-inner">${inner}</span></span>`;
|
|
539
|
-
}
|
|
540
|
-
case "blink": {
|
|
541
|
-
const rate = attrs["rate"] !== undefined && COUNT.test(attrs["rate"])
|
|
542
|
-
? ` style="--mq-rate:${attrs["rate"]}"`
|
|
543
|
-
: "";
|
|
544
|
-
if (attrs["by"] === "letter" || attrs["by"] === "word") {
|
|
545
|
-
// Split blink: ramp is theater-marquee chase lights, scatter is
|
|
546
|
-
// twinkle. The rate var rides the container; units inherit it.
|
|
547
|
-
return bySegments(name, attrs["by"], attrs["phase"], nodes, ctx, rate);
|
|
548
|
-
}
|
|
549
|
-
return `<span class="mq-blink"${rate}>${inner}</span>`;
|
|
550
|
-
}
|
|
551
|
-
case "rainbow":
|
|
552
|
-
case "bounce":
|
|
553
|
-
case "jitter":
|
|
554
|
-
case "wave":
|
|
555
|
-
case "rubber":
|
|
556
|
-
if (attrs["by"] === "letter" || attrs["by"] === "word") {
|
|
557
|
-
return bySegments(name, attrs["by"], attrs["phase"], nodes, ctx);
|
|
558
|
-
}
|
|
559
|
-
return `<span class="mq-${name}">${inner}</span>`;
|
|
560
|
-
case "typewriter": {
|
|
561
|
-
// Inherently per-unit: the reveal IS a by=letter effect (by=word for
|
|
562
|
-
// word-at-a-time). speed= is units per second; the container carries
|
|
563
|
-
// the per-unit delay step, each unit its ordinal in --mq-o.
|
|
564
|
-
const by = attrs["by"] === "word" ? "word" : "letter";
|
|
565
|
-
const step = revealStep(attrs["speed"], 14);
|
|
566
|
-
return bySegments(name, by, attrs["phase"], nodes, ctx, ` style="--mq-tw-step:${step}s"`);
|
|
567
|
-
}
|
|
568
|
-
case "fadein": {
|
|
569
|
-
// The ghostly reveal. Bare [fadein] fades the whole run in once;
|
|
570
|
-
// by=letter / by=word drift units in on staggered starts (same
|
|
571
|
-
// one-shot family as typewriter: sequential ordinals, high cap);
|
|
572
|
-
// phase=scatter is apparition weather.
|
|
573
|
-
if (attrs["by"] === "letter" || attrs["by"] === "word") {
|
|
574
|
-
const step = revealStep(attrs["speed"], 16);
|
|
575
|
-
return bySegments(name, attrs["by"], attrs["phase"], nodes, ctx, ` style="--mq-fi-step:${step}s"`);
|
|
576
|
-
}
|
|
577
|
-
return `<span class="mq-fadein">${inner}</span>`;
|
|
578
|
-
}
|
|
579
642
|
}
|
|
580
643
|
return inner; // unknown span: pure shrug, children as plain content
|
|
581
644
|
}
|
|
582
645
|
|
|
583
|
-
// --
|
|
584
|
-
//
|
|
585
|
-
// through a negative animation-delay. Pure markup - the document stays a
|
|
586
|
-
// document. Locale pinned so segmentation (and thus output) is stable.
|
|
587
|
-
|
|
588
|
-
const SEGMENTERS = {
|
|
589
|
-
letter: new Intl.Segmenter("en", { granularity: "grapheme" }),
|
|
590
|
-
word: new Intl.Segmenter("en", { granularity: "word" }),
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
type SplitBy = keyof typeof SEGMENTERS;
|
|
594
|
-
|
|
595
|
-
/** DOM-weight discipline: past this many units, the run animates whole.
|
|
596
|
-
* Loopers pay a live animation per element forever, so they cap low;
|
|
597
|
-
* typewriter's units are 1ms one-shots (finished animations cost nothing)
|
|
598
|
-
* and its natural material is long text, so it caps high. */
|
|
599
|
-
const MAX_SPLIT_UNITS = 400;
|
|
600
|
-
const MAX_REVEAL_UNITS = 2000;
|
|
601
|
-
|
|
602
|
-
type Phase = "ramp" | "scatter";
|
|
603
|
-
|
|
604
|
-
interface SplitState {
|
|
605
|
-
effect: string;
|
|
606
|
-
by: SplitBy;
|
|
607
|
-
phase: Phase;
|
|
608
|
-
i: number;
|
|
609
|
-
total: number;
|
|
610
|
-
}
|
|
646
|
+
// -- effects: the look (classes, knobs, per-unit split) comes from
|
|
647
|
+
// effects.ts, shared with every other surface that draws effects.
|
|
611
648
|
|
|
612
|
-
function
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
ctx: Ctx,
|
|
618
|
-
containerStyle = "",
|
|
619
|
-
): string {
|
|
620
|
-
// Each effect has a natural phase order (jitter scatters, the rest sweep);
|
|
621
|
-
// the knob overrides it either way. Invalid values degrade to the default.
|
|
622
|
-
const phase: Phase =
|
|
623
|
-
phaseAttr === "scatter" || phaseAttr === "ramp"
|
|
624
|
-
? phaseAttr
|
|
625
|
-
: effect === "jitter"
|
|
626
|
-
? "scatter"
|
|
627
|
-
: "ramp";
|
|
628
|
-
const state: SplitState = { effect, by, phase, i: 0, total: 0 };
|
|
629
|
-
state.total = countUnits(nodes, by);
|
|
630
|
-
const cap = isReveal(effect) ? MAX_REVEAL_UNITS : MAX_SPLIT_UNITS;
|
|
631
|
-
if (state.total === 0 || state.total > cap) {
|
|
632
|
-
return `<span class="mq-${effect}">${children(nodes, ctx)}</span>`;
|
|
649
|
+
function renderEffect(name: string, look: EffectLook, nodes: Node[], inner: string, ctx: Ctx): string {
|
|
650
|
+
const data = look.data.map(([k, v]) => ` data-${k}="${escapeAttr(v)}"`).join("");
|
|
651
|
+
const style = look.vars.length === 0 ? "" : ` style="${look.vars.map(([k, v]) => `${k}:${escapeAttr(v)}`).join(";")}"`;
|
|
652
|
+
if (name === "marquee") {
|
|
653
|
+
return `<span class="${look.className}"${data}${style}><span class="mq-marquee-inner">${inner}</span></span>`;
|
|
633
654
|
}
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
*
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
// point, nudged coprime with the total, walks a well-spread permutation
|
|
653
|
-
// at EVERY length. (A fixed prime stride is a trap: 7919 mod 40 = 39 =
|
|
654
|
-
// -1, so forty-unit runs typed in backwards.)
|
|
655
|
-
if (isReveal(state.effect)) {
|
|
656
|
-
const o =
|
|
657
|
-
state.phase === "scatter"
|
|
658
|
-
? (state.i * scatterStride(state.total)) % state.total
|
|
659
|
-
: state.i;
|
|
660
|
-
return String(o);
|
|
661
|
-
}
|
|
662
|
-
let o: number;
|
|
663
|
-
if (state.phase === "scatter") {
|
|
664
|
-
o = ((state.i * 7919) % 101) / 101;
|
|
665
|
-
} else {
|
|
666
|
-
switch (state.effect) {
|
|
667
|
-
case "rainbow":
|
|
668
|
-
o = state.i / state.total; // gradient across the whole run
|
|
669
|
-
break;
|
|
670
|
-
case "wave":
|
|
671
|
-
o = (state.i % 8) / 8; // fixed ripple wavelength
|
|
672
|
-
break;
|
|
673
|
-
case "bounce":
|
|
674
|
-
o = (state.i % 6) / 6;
|
|
675
|
-
break;
|
|
676
|
-
default:
|
|
677
|
-
o = (state.i % 8) / 8; // jitter in ramp mode: a rippling shudder
|
|
678
|
-
break;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
return String(Math.round(o * 1000) / 1000);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
/** speed= (units per second, a COUNT) into a per-unit delay step in
|
|
685
|
-
* seconds; invalid or absent falls to the effect's default rate. */
|
|
686
|
-
function revealStep(speedAttr: string | undefined, dflt: number): number {
|
|
687
|
-
const speed =
|
|
688
|
-
speedAttr !== undefined && COUNT.test(speedAttr) && Number(speedAttr) > 0
|
|
689
|
-
? Number(speedAttr)
|
|
690
|
-
: dflt;
|
|
691
|
-
return Math.round(1000 / speed) / 1000;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
/** The one-shot reveals: sequential ordinals, the high unit cap. */
|
|
695
|
-
function isReveal(effect: string): boolean {
|
|
696
|
-
return effect === "typewriter" || effect === "fadein";
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function gcd(a: number, b: number): number {
|
|
700
|
-
return b === 0 ? a : gcd(b, a % b);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
/** The smallest stride >= ~61.8% of total that's coprime with it (falls
|
|
704
|
-
* back to 1 for degenerate totals - a 1- or 2-unit "scramble" is fate). */
|
|
705
|
-
function scatterStride(total: number): number {
|
|
706
|
-
let stride = Math.max(1, Math.round(total * 0.618));
|
|
707
|
-
while (stride < total && gcd(stride, total) !== 1) {
|
|
708
|
-
stride += 1;
|
|
709
|
-
}
|
|
710
|
-
return gcd(stride, total) === 1 ? stride : 1;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function countUnits(nodes: Node[], by: SplitBy): number {
|
|
714
|
-
let n = 0;
|
|
715
|
-
for (const node of nodes) {
|
|
716
|
-
if (node.type === "text") {
|
|
717
|
-
for (const seg of SEGMENTERS[by].segment(node.value)) {
|
|
718
|
-
if (isUnit(seg, by)) {
|
|
719
|
-
n += 1;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
} else if (
|
|
723
|
-
node.type === "emphasis" ||
|
|
724
|
-
node.type === "strong" ||
|
|
725
|
-
node.type === "strikethrough"
|
|
726
|
-
) {
|
|
727
|
-
n += countUnits(node.children, by);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
return n;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function splitRender(nodes: Node[], ctx: Ctx, state: SplitState): string {
|
|
655
|
+
const body = look.units === null ? inner : splitRender(nodes, ctx, look);
|
|
656
|
+
return `<span class="${look.className}"${data}${style}>${body}</span>`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** Each unit in its own span with its phase offset; spaces and punctuation
|
|
660
|
+
* between units ride along as plain text; each word's units sit in a group
|
|
661
|
+
* that wraps as one. */
|
|
662
|
+
function splitRender(nodes: Node[], ctx: Ctx, look: EffectLook): string {
|
|
663
|
+
const pieces = (list: SplitPiece[]): string =>
|
|
664
|
+
list
|
|
665
|
+
.map((p) =>
|
|
666
|
+
p.kind === "text"
|
|
667
|
+
? escapeText(p.text)
|
|
668
|
+
: p.kind === "unit"
|
|
669
|
+
? `<span class="mq-l" style="--mq-o:${p.offset}">${escapeText(p.text)}</span>`
|
|
670
|
+
: `<span class="mq-w">${pieces(p.pieces)}</span>`,
|
|
671
|
+
)
|
|
672
|
+
.join("");
|
|
734
673
|
let out = "";
|
|
735
674
|
for (const node of nodes) {
|
|
736
675
|
if (node.type === "text") {
|
|
737
|
-
|
|
738
|
-
if (!isUnit(seg, state.by)) {
|
|
739
|
-
out += escapeText(seg.segment); // spaces/punctuation ride along
|
|
740
|
-
continue;
|
|
741
|
-
}
|
|
742
|
-
const o = unitOffset(state);
|
|
743
|
-
state.i += 1;
|
|
744
|
-
out += `<span class="mq-l" style="--mq-o:${o}">${escapeText(seg.segment)}</span>`;
|
|
745
|
-
}
|
|
676
|
+
out += pieces(splitPieces(node, look));
|
|
746
677
|
} else if (node.type === "emphasis") {
|
|
747
|
-
out += `<em>${splitRender(node.children, ctx,
|
|
678
|
+
out += `<em>${splitRender(node.children, ctx, look)}</em>`;
|
|
748
679
|
} else if (node.type === "strong") {
|
|
749
|
-
out += `<strong>${splitRender(node.children, ctx,
|
|
680
|
+
out += `<strong>${splitRender(node.children, ctx, look)}</strong>`;
|
|
750
681
|
} else if (node.type === "strikethrough") {
|
|
751
|
-
out += `<del>${splitRender(node.children, ctx,
|
|
682
|
+
out += `<del>${splitRender(node.children, ctx, look)}</del>`;
|
|
752
683
|
} else {
|
|
753
684
|
out += renderNode(node, ctx); // anything else renders whole, un-split
|
|
754
685
|
}
|