@velumo/renderer-dom 0.1.0-beta.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/background-renderer.d.ts +16 -0
- package/dist/background-renderer.d.ts.map +1 -0
- package/dist/background-renderer.js +41 -0
- package/dist/background-renderer.js.map +1 -0
- package/dist/camera-adapter.d.ts +16 -0
- package/dist/camera-adapter.d.ts.map +1 -0
- package/dist/camera-adapter.js +13 -0
- package/dist/camera-adapter.js.map +1 -0
- package/dist/default-theme.d.ts +5 -0
- package/dist/default-theme.d.ts.map +1 -0
- package/dist/default-theme.js +579 -0
- package/dist/default-theme.js.map +1 -0
- package/dist/dom-renderer.d.ts +62 -0
- package/dist/dom-renderer.d.ts.map +1 -0
- package/dist/dom-renderer.js +306 -0
- package/dist/dom-renderer.js.map +1 -0
- package/dist/html-sanitize.d.ts +18 -0
- package/dist/html-sanitize.d.ts.map +1 -0
- package/dist/html-sanitize.js +379 -0
- package/dist/html-sanitize.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/layout-renderer.d.ts +16 -0
- package/dist/layout-renderer.d.ts.map +1 -0
- package/dist/layout-renderer.js +450 -0
- package/dist/layout-renderer.js.map +1 -0
- package/dist/slide-thumbnail.d.ts +63 -0
- package/dist/slide-thumbnail.d.ts.map +1 -0
- package/dist/slide-thumbnail.js +93 -0
- package/dist/slide-thumbnail.js.map +1 -0
- package/dist/spatial-renderer.d.ts +18 -0
- package/dist/spatial-renderer.d.ts.map +1 -0
- package/dist/spatial-renderer.js +36 -0
- package/dist/spatial-renderer.js.map +1 -0
- package/dist/theme-fonts.d.ts +8 -0
- package/dist/theme-fonts.d.ts.map +1 -0
- package/dist/theme-fonts.js +68 -0
- package/dist/theme-fonts.js.map +1 -0
- package/dist/theme-tokens.d.ts +3 -0
- package/dist/theme-tokens.d.ts.map +1 -0
- package/dist/theme-tokens.js +43 -0
- package/dist/theme-tokens.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +20 -0
- package/src/background-renderer.ts +77 -0
- package/src/camera-adapter.ts +21 -0
- package/src/default-theme.ts +582 -0
- package/src/dom-renderer.ts +486 -0
- package/src/html-sanitize.ts +446 -0
- package/src/index.ts +25 -0
- package/src/layout-renderer.ts +721 -0
- package/src/slide-thumbnail.ts +146 -0
- package/src/spatial-renderer.ts +62 -0
- package/src/theme-fonts.ts +75 -0
- package/src/theme-tokens.ts +66 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
JsonValue,
|
|
3
|
+
Layer,
|
|
4
|
+
Manifest,
|
|
5
|
+
SlideEntry,
|
|
6
|
+
Theme,
|
|
7
|
+
ThemeTokens,
|
|
8
|
+
} from "@velumo/core";
|
|
9
|
+
import {
|
|
10
|
+
ActionDispatcher,
|
|
11
|
+
dispatchControlAction,
|
|
12
|
+
type ActionContext,
|
|
13
|
+
type ActionHandler,
|
|
14
|
+
type LayerRenderer,
|
|
15
|
+
type RuntimeController,
|
|
16
|
+
} from "@velumo/runtime";
|
|
17
|
+
import { defaultTheme } from "./default-theme.js";
|
|
18
|
+
import { compileFontFaceCss, escapeCssString } from "./theme-fonts.js";
|
|
19
|
+
import { tokensToCssProperties } from "./theme-tokens.js";
|
|
20
|
+
import { renderBackground } from "./background-renderer.js";
|
|
21
|
+
import { renderLayoutNode } from "./layout-renderer.js";
|
|
22
|
+
import { renderSafeHtml } from "./html-sanitize.js";
|
|
23
|
+
|
|
24
|
+
interface DomDocumentLike {
|
|
25
|
+
createElement(tagName: string): DomElementLike;
|
|
26
|
+
createElementNS(namespace: string, tagName: string): DomElementLike;
|
|
27
|
+
createTextNode(text: string): DomNodeLike;
|
|
28
|
+
readonly head?: DomElementLike;
|
|
29
|
+
querySelector?(selectors: string): DomElementLike | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type DomNodeChild = DomElementLike | DomNodeLike;
|
|
33
|
+
type DomChild = DomNodeChild | string;
|
|
34
|
+
type DomLayerRenderResult = DomChild | readonly DomChild[] | null | undefined;
|
|
35
|
+
|
|
36
|
+
interface DomNodeLike {
|
|
37
|
+
parentNode?: unknown;
|
|
38
|
+
textContent: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface DomElementLike extends DomNodeLike {
|
|
42
|
+
readonly ownerDocument: DomDocumentLike;
|
|
43
|
+
readonly style?: DomStyleLike;
|
|
44
|
+
append(...children: DomChild[]): void;
|
|
45
|
+
appendChild(child: DomNodeChild): DomNodeChild;
|
|
46
|
+
removeAttribute(name: string): void;
|
|
47
|
+
replaceChildren(...children: DomChild[]): void;
|
|
48
|
+
setAttribute(name: string, value: string): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface DomStyleLike {
|
|
52
|
+
removeProperty(name: string): string;
|
|
53
|
+
setProperty(name: string, value: string): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DomLayerRendererContext {
|
|
57
|
+
readonly layer: Layer;
|
|
58
|
+
readonly slide: SlideEntry;
|
|
59
|
+
readonly manifest: Manifest;
|
|
60
|
+
readonly runtime: RuntimeController;
|
|
61
|
+
createElement(tagName: string): DomElementLike;
|
|
62
|
+
createText(text: string): DomNodeLike;
|
|
63
|
+
// Imperative control (click) dispatch. Always bound; routes to a registered
|
|
64
|
+
// action handler via the Ward 042 ActionDispatcher with source:"control".
|
|
65
|
+
// A no-op when the registry has no matching handler (or no action handlers
|
|
66
|
+
// at all), never throws.
|
|
67
|
+
dispatch(action: { type: string; payload?: JsonValue }): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface DomRuntimeRegistry {
|
|
71
|
+
getLayerRenderer(
|
|
72
|
+
type: string,
|
|
73
|
+
): LayerRenderer<DomLayerRendererContext, DomLayerRenderResult> | undefined;
|
|
74
|
+
// Control-path handler lookup. Typed to the control context (ActionContext);
|
|
75
|
+
// the real generic RuntimeRegistry's ActionHandler<unknown> is assignable
|
|
76
|
+
// here. IMPORTANT: handlers reached this way are invoked with
|
|
77
|
+
// source:"control" and NO cue/cueIndex/timeMs. A handler shared with the
|
|
78
|
+
// timeline cue path must guard `if (context.source === "cue")` before
|
|
79
|
+
// touching cue-only fields (gate-3).
|
|
80
|
+
getActionHandler?(type: string): ActionHandler<ActionContext> | undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface DomRendererOptions {
|
|
84
|
+
readonly root: DomElementLike;
|
|
85
|
+
readonly manifest: Manifest;
|
|
86
|
+
readonly runtime: RuntimeController;
|
|
87
|
+
readonly registry?: DomRuntimeRegistry;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class DomRenderer {
|
|
91
|
+
readonly #root: DomElementLike;
|
|
92
|
+
readonly #manifest: Manifest;
|
|
93
|
+
readonly #runtime: RuntimeController;
|
|
94
|
+
readonly #registry: DomRuntimeRegistry | undefined;
|
|
95
|
+
readonly #actionDispatcher: ActionDispatcher<ActionContext> | undefined;
|
|
96
|
+
readonly #themeTokenProperties = new Set<string>();
|
|
97
|
+
|
|
98
|
+
constructor(options: DomRendererOptions) {
|
|
99
|
+
this.#root = options.root;
|
|
100
|
+
this.#manifest = options.manifest;
|
|
101
|
+
this.#runtime = options.runtime;
|
|
102
|
+
this.#registry = options.registry;
|
|
103
|
+
// One dispatcher over the active registry, reused by every layer context's
|
|
104
|
+
// dispatch. Absent when the registry has no action-handler seam (a minimal
|
|
105
|
+
// host) — control clicks then render inert.
|
|
106
|
+
const registry = options.registry;
|
|
107
|
+
this.#actionDispatcher =
|
|
108
|
+
registry?.getActionHandler !== undefined
|
|
109
|
+
? new ActionDispatcher<ActionContext>({
|
|
110
|
+
registry: {
|
|
111
|
+
getActionHandler: (type) => registry.getActionHandler?.(type),
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
: undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getRoot(): DomElementLike {
|
|
118
|
+
return this.#root;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
mount(): void {
|
|
122
|
+
this.#root.setAttribute("data-velumo-renderer", "dom");
|
|
123
|
+
this.render();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
render(): void {
|
|
127
|
+
this.#applyTheme();
|
|
128
|
+
this.#root.replaceChildren(this.#renderSlide(this.#currentSlide()));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
unmount(): void {
|
|
132
|
+
this.#root.replaceChildren();
|
|
133
|
+
this.#root.removeAttribute("data-velumo-renderer");
|
|
134
|
+
this.#root.removeAttribute("data-velumo-theme");
|
|
135
|
+
this.#clearThemeTokenProperties();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
dispose(): void {
|
|
139
|
+
this.unmount();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#applyTheme(): void {
|
|
143
|
+
const theme = this.#manifest.theme ?? defaultTheme;
|
|
144
|
+
const themeId = theme.id;
|
|
145
|
+
|
|
146
|
+
this.#root.setAttribute("data-velumo-theme", themeId);
|
|
147
|
+
this.#applyThemeTokenProperties(theme.tokens);
|
|
148
|
+
this.#applyThemeCss(theme);
|
|
149
|
+
this.#applyThemeFonts(theme);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Injects the active theme's own CSS into document.head once per
|
|
153
|
+
// (document, theme id). The injected <style> is intentionally NOT removed on
|
|
154
|
+
// unmount: it is document-level and may be shared by other live renderers
|
|
155
|
+
// (e.g. the presenter mounts several). This is only safe because every rule a
|
|
156
|
+
// theme ships in `theme.css` is expected to be scoped to
|
|
157
|
+
// `[data-velumo-theme="<id>"]`, so a lingering style for an inactive theme
|
|
158
|
+
// matches nothing. A theme shipping unscoped rules here would leak across the page.
|
|
159
|
+
#applyThemeCss(theme: Theme): void {
|
|
160
|
+
const css = typeof theme.css === "string" ? theme.css : undefined;
|
|
161
|
+
if (css === undefined || css === "") {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const doc = this.#root.ownerDocument;
|
|
165
|
+
const head = doc.head;
|
|
166
|
+
if (head === undefined || typeof doc.querySelector !== "function") {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const marker = "data-velumo-theme-style";
|
|
170
|
+
if (
|
|
171
|
+
doc.querySelector(`[${marker}="${escapeCssString(theme.id)}"]`) !== null
|
|
172
|
+
) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const style = doc.createElement("style");
|
|
176
|
+
style.setAttribute(marker, theme.id);
|
|
177
|
+
style.textContent = css;
|
|
178
|
+
head.appendChild(style);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Injects the active theme's declared font faces into document.head as a
|
|
182
|
+
// single `<style>`, deduped per (document, theme id), no-op when there are no
|
|
183
|
+
// fonts or the host lacks head/querySelector. The injected text is exactly
|
|
184
|
+
// `compileFontFaceCss(theme.fonts)` — there is no second, hand-rolled code
|
|
185
|
+
// path. Like the theme-css style it is NOT removed on unmount: it is
|
|
186
|
+
// document-level and may be shared by other live renderers (the presenter
|
|
187
|
+
// mounts several of the same theme).
|
|
188
|
+
//
|
|
189
|
+
// The safety argument differs from `#applyThemeCss`. Theme CSS is selector-
|
|
190
|
+
// scoped to `[data-velumo-theme="<id>"]`, so a lingering style for an
|
|
191
|
+
// inactive theme matches nothing. `@font-face` is NOT selector-scoped — it
|
|
192
|
+
// registers a font family document-globally. Deduping per theme id keeps each
|
|
193
|
+
// theme's faces under their own marker so they coexist without clobbering, and
|
|
194
|
+
// a lingering face for an inactive theme is mostly inert (nothing references
|
|
195
|
+
// the family; a data-URI triggers no network fetch). The one residual edge:
|
|
196
|
+
// two themes declaring the SAME `family` with different bytes in one document
|
|
197
|
+
// resolve by cascade/head order. Not reachable today (a document carries one
|
|
198
|
+
// theme id); themes should namespace family names.
|
|
199
|
+
#applyThemeFonts(theme: Theme): void {
|
|
200
|
+
const fonts = theme.fonts;
|
|
201
|
+
if (fonts === undefined || fonts.length === 0) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const doc = this.#root.ownerDocument;
|
|
205
|
+
const head = doc.head;
|
|
206
|
+
if (head === undefined || typeof doc.querySelector !== "function") {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const marker = "data-velumo-theme-fonts";
|
|
210
|
+
if (
|
|
211
|
+
doc.querySelector(`[${marker}="${escapeCssString(theme.id)}"]`) !== null
|
|
212
|
+
) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const style = doc.createElement("style");
|
|
216
|
+
style.setAttribute(marker, theme.id);
|
|
217
|
+
style.textContent = compileFontFaceCss(fonts);
|
|
218
|
+
head.appendChild(style);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#currentSlide(): SlideEntry {
|
|
222
|
+
const state = this.#runtime.getState();
|
|
223
|
+
return (
|
|
224
|
+
this.#manifest.slides.find((slide) => slide.id === state.slideId) ??
|
|
225
|
+
this.#manifest.slides[0]
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#renderSlide(slide: SlideEntry): DomElementLike {
|
|
230
|
+
const slideElement = this.#createElement("section");
|
|
231
|
+
slideElement.setAttribute("data-velumo-slide-id", slide.id);
|
|
232
|
+
slideElement.setAttribute("data-velumo-slide-kind", slide.kind);
|
|
233
|
+
slideElement.setAttribute(
|
|
234
|
+
"aria-label",
|
|
235
|
+
`${this.#manifest.deck.title} ${slide.id}`,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
if (slide.transition !== undefined) {
|
|
239
|
+
slideElement.setAttribute("data-velumo-transition", slide.transition);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const watermark = this.#manifest.deck.watermark;
|
|
243
|
+
const hasWatermark =
|
|
244
|
+
typeof watermark === "string" &&
|
|
245
|
+
watermark.trim().length > 0 &&
|
|
246
|
+
slide.chrome !== false;
|
|
247
|
+
|
|
248
|
+
const needsPositioning =
|
|
249
|
+
slide.background !== undefined ||
|
|
250
|
+
slide.layout?.kind === "canvas" ||
|
|
251
|
+
hasWatermark;
|
|
252
|
+
if (needsPositioning) {
|
|
253
|
+
this.#setStyleProperty(slideElement, "position", "relative");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (slide.background !== undefined) {
|
|
257
|
+
this.#setStyleProperty(slideElement, "z-index", "0");
|
|
258
|
+
slideElement.appendChild(
|
|
259
|
+
renderBackground(slide.background, {
|
|
260
|
+
createElement: (tag) => this.#createElement(tag),
|
|
261
|
+
setStyleProperty: (el, prop, val) =>
|
|
262
|
+
this.#setStyleProperty(el, prop, val),
|
|
263
|
+
}),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (slide.layout !== undefined) {
|
|
268
|
+
const layoutRoot = this.#createElement("div");
|
|
269
|
+
layoutRoot.setAttribute("data-velumo-layout-root", "");
|
|
270
|
+
layoutRoot.appendChild(
|
|
271
|
+
renderLayoutNode(slide.layout, {
|
|
272
|
+
createElement: (tag) => this.#createElement(tag),
|
|
273
|
+
createElementNS: (ns, tag) => this.#createElementNS(ns, tag),
|
|
274
|
+
setStyleProperty: (el, prop, val) =>
|
|
275
|
+
this.#setStyleProperty(el, prop, val),
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
slideElement.appendChild(layoutRoot);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const layer of slide.layers ?? []) {
|
|
282
|
+
slideElement.appendChild(this.#renderLayer(slide, layer));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (hasWatermark) {
|
|
286
|
+
const mark = this.#createElement("div");
|
|
287
|
+
mark.setAttribute("data-velumo-watermark", "");
|
|
288
|
+
mark.setAttribute("aria-hidden", "true");
|
|
289
|
+
mark.textContent = watermark;
|
|
290
|
+
slideElement.appendChild(mark);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return slideElement;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#renderLayer(slide: SlideEntry, layer: Layer): DomElementLike {
|
|
297
|
+
const layerElement = this.#createElement("div");
|
|
298
|
+
layerElement.setAttribute("data-velumo-layer-type", layer.type);
|
|
299
|
+
|
|
300
|
+
if (layer.id !== undefined) {
|
|
301
|
+
layerElement.setAttribute("data-velumo-layer-id", layer.id);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.#applyFragmentState(slide, layer, layerElement);
|
|
305
|
+
|
|
306
|
+
const customRenderer = this.#registry?.getLayerRenderer(layer.type);
|
|
307
|
+
if (customRenderer !== undefined) {
|
|
308
|
+
this.#appendCustomLayerResult(
|
|
309
|
+
layerElement,
|
|
310
|
+
customRenderer(this.#createLayerContext(slide, layer)),
|
|
311
|
+
);
|
|
312
|
+
return layerElement;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
switch (layer.type) {
|
|
316
|
+
case "markdown":
|
|
317
|
+
layerElement.append(
|
|
318
|
+
...this.#renderMarkdown(String(layer.content ?? "")),
|
|
319
|
+
);
|
|
320
|
+
return layerElement;
|
|
321
|
+
case "html":
|
|
322
|
+
layerElement.append(
|
|
323
|
+
...renderSafeHtml(String(layer.content ?? ""), {
|
|
324
|
+
createElement: (tag) => this.#createElement(tag),
|
|
325
|
+
createText: (text) => this.#createText(text),
|
|
326
|
+
textElement: (tag, text) => this.#textElement(tag, text),
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
return layerElement;
|
|
330
|
+
default:
|
|
331
|
+
layerElement.textContent = layer.type;
|
|
332
|
+
return layerElement;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#createLayerContext(
|
|
337
|
+
slide: SlideEntry,
|
|
338
|
+
layer: Layer,
|
|
339
|
+
): DomLayerRendererContext {
|
|
340
|
+
const dispatcher = this.#actionDispatcher;
|
|
341
|
+
const runtime = this.#runtime;
|
|
342
|
+
return {
|
|
343
|
+
layer,
|
|
344
|
+
slide,
|
|
345
|
+
manifest: this.#manifest,
|
|
346
|
+
runtime,
|
|
347
|
+
createElement: (tagName) => this.#createElement(tagName),
|
|
348
|
+
createText: (text) => this.#createText(text),
|
|
349
|
+
dispatch: (action) => {
|
|
350
|
+
if (dispatcher === undefined) {
|
|
351
|
+
return; // inert on a minimal / no-action-handler host
|
|
352
|
+
}
|
|
353
|
+
dispatchControlAction(
|
|
354
|
+
dispatcher,
|
|
355
|
+
{
|
|
356
|
+
type: action.type,
|
|
357
|
+
...(action.payload === undefined
|
|
358
|
+
? {}
|
|
359
|
+
: { payload: action.payload }),
|
|
360
|
+
},
|
|
361
|
+
runtime,
|
|
362
|
+
);
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#appendCustomLayerResult(
|
|
368
|
+
layerElement: DomElementLike,
|
|
369
|
+
result: DomLayerRenderResult,
|
|
370
|
+
): void {
|
|
371
|
+
if (result === null || result === undefined) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const children = Array.isArray(result) ? result : [result];
|
|
376
|
+
layerElement.append(...children);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#applyFragmentState(
|
|
380
|
+
slide: SlideEntry,
|
|
381
|
+
layer: Layer,
|
|
382
|
+
layerElement: DomElementLike,
|
|
383
|
+
): void {
|
|
384
|
+
const fragmentIndex = this.#fragmentIndexForLayer(slide, layer);
|
|
385
|
+
|
|
386
|
+
if (fragmentIndex === undefined) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const visible = fragmentIndex + 1 <= this.#runtime.getState().fragmentIndex;
|
|
391
|
+
layerElement.setAttribute(
|
|
392
|
+
"data-velumo-fragment",
|
|
393
|
+
visible ? "visible" : "hidden",
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
#fragmentIndexForLayer(slide: SlideEntry, layer: Layer): number | undefined {
|
|
398
|
+
if (layer.id === undefined) {
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const fragmentIndex = slide.fragments?.findIndex(
|
|
403
|
+
(fragment) => fragment.layerId === layer.id,
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
return fragmentIndex === undefined || fragmentIndex === -1
|
|
407
|
+
? undefined
|
|
408
|
+
: fragmentIndex;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#renderMarkdown(source: string): DomElementLike[] {
|
|
412
|
+
const elements: DomElementLike[] = [];
|
|
413
|
+
|
|
414
|
+
for (const block of source.split(/\n{2,}/)) {
|
|
415
|
+
const lines = block.split(/\n/).filter((line) => line.length > 0);
|
|
416
|
+
|
|
417
|
+
for (const line of lines) {
|
|
418
|
+
if (line.startsWith("## ")) {
|
|
419
|
+
elements.push(this.#textElement("h2", line.slice(3)));
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (line.startsWith("# ")) {
|
|
424
|
+
elements.push(this.#textElement("h1", line.slice(2)));
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
elements.push(this.#textElement("p", line));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return elements;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
#textElement(tagName: string, text: string): DomElementLike {
|
|
436
|
+
const element = this.#createElement(tagName);
|
|
437
|
+
element.textContent = text;
|
|
438
|
+
return element;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#createElement(tagName: string): DomElementLike {
|
|
442
|
+
return this.#root.ownerDocument.createElement(tagName);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#createElementNS(namespace: string, tagName: string): DomElementLike {
|
|
446
|
+
return this.#root.ownerDocument.createElementNS(namespace, tagName);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#createText(text: string): DomNodeLike {
|
|
450
|
+
return this.#root.ownerDocument.createTextNode(text);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
#applyThemeTokenProperties(tokens: ThemeTokens | undefined): void {
|
|
454
|
+
const nextProperties = tokensToCssProperties(tokens);
|
|
455
|
+
|
|
456
|
+
for (const property of this.#themeTokenProperties) {
|
|
457
|
+
if (!nextProperties.has(property)) {
|
|
458
|
+
this.#root.style?.removeProperty(property);
|
|
459
|
+
this.#themeTokenProperties.delete(property);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
for (const [property, value] of nextProperties) {
|
|
464
|
+
this.#setStyleProperty(this.#root, property, value);
|
|
465
|
+
this.#themeTokenProperties.add(property);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
#clearThemeTokenProperties(): void {
|
|
470
|
+
for (const property of this.#themeTokenProperties) {
|
|
471
|
+
this.#root.style?.removeProperty(property);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
this.#themeTokenProperties.clear();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
#setStyleProperty(
|
|
478
|
+
element: {
|
|
479
|
+
readonly style?: { setProperty(name: string, value: string): void };
|
|
480
|
+
},
|
|
481
|
+
property: string,
|
|
482
|
+
value: string,
|
|
483
|
+
): void {
|
|
484
|
+
element.style?.setProperty(property, value);
|
|
485
|
+
}
|
|
486
|
+
}
|