@texuli/deck 0.0.0-stage → 0.5.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/LICENSE +21 -0
- package/README.md +183 -2
- package/dist/authoring/builder.d.ts +351 -0
- package/dist/authoring/builder.js +64 -0
- package/dist/authoring/index.d.ts +6 -0
- package/dist/authoring/index.js +6 -0
- package/dist/authoring/lint.d.ts +36 -0
- package/dist/authoring/lint.js +325 -0
- package/dist/authoring/outlines.d.ts +34 -0
- package/dist/authoring/outlines.js +255 -0
- package/dist/authoring/patch.d.ts +23 -0
- package/dist/authoring/patch.js +166 -0
- package/dist/authoring/select-layouts.d.ts +19 -0
- package/dist/authoring/select-layouts.js +90 -0
- package/dist/engine/auto-slide.d.ts +32 -0
- package/dist/engine/auto-slide.js +67 -0
- package/dist/engine/canvas.d.ts +56 -0
- package/dist/engine/canvas.js +48 -0
- package/dist/engine/controller.d.ts +126 -0
- package/dist/engine/controller.js +248 -0
- package/dist/engine/controls.d.ts +128 -0
- package/dist/engine/controls.js +460 -0
- package/dist/engine/hash.d.ts +42 -0
- package/dist/engine/hash.js +86 -0
- package/dist/engine/index.d.ts +88 -0
- package/dist/engine/index.js +88 -0
- package/dist/engine/messages.d.ts +138 -0
- package/dist/engine/messages.js +140 -0
- package/dist/engine/motion.d.ts +64 -0
- package/dist/engine/motion.js +112 -0
- package/dist/engine/navigation.d.ts +87 -0
- package/dist/engine/navigation.js +156 -0
- package/dist/engine/presenter.d.ts +58 -0
- package/dist/engine/presenter.js +157 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/markdown/attributes.d.ts +24 -0
- package/dist/markdown/attributes.js +94 -0
- package/dist/markdown/attrs-closed.d.ts +52 -0
- package/dist/markdown/attrs-closed.js +520 -0
- package/dist/markdown/blocks.d.ts +112 -0
- package/dist/markdown/blocks.js +766 -0
- package/dist/markdown/frontmatter.d.ts +30 -0
- package/dist/markdown/frontmatter.js +165 -0
- package/dist/markdown/index.d.ts +29 -0
- package/dist/markdown/index.js +29 -0
- package/dist/markdown/issues.d.ts +68 -0
- package/dist/markdown/issues.js +83 -0
- package/dist/markdown/json.d.ts +21 -0
- package/dist/markdown/json.js +236 -0
- package/dist/markdown/limits.d.ts +32 -0
- package/dist/markdown/limits.js +32 -0
- package/dist/markdown/parse.d.ts +31 -0
- package/dist/markdown/parse.js +283 -0
- package/dist/markdown/scan.d.ts +26 -0
- package/dist/markdown/scan.js +138 -0
- package/dist/markdown/slides.d.ts +32 -0
- package/dist/markdown/slides.js +854 -0
- package/dist/markdown/text.d.ts +76 -0
- package/dist/markdown/text.js +447 -0
- package/dist/markdown/twin.d.ts +41 -0
- package/dist/markdown/twin.js +374 -0
- package/dist/markdown/write.d.ts +17 -0
- package/dist/markdown/write.js +688 -0
- package/dist/math/index.d.ts +14 -0
- package/dist/math/index.js +14 -0
- package/dist/math/parse.d.ts +9 -0
- package/dist/math/parse.js +648 -0
- package/dist/math/symbols.d.ts +44 -0
- package/dist/math/symbols.js +333 -0
- package/dist/math/types.d.ts +59 -0
- package/dist/math/types.js +69 -0
- package/dist/model/backgrounds/contrast.d.ts +77 -0
- package/dist/model/backgrounds/contrast.js +186 -0
- package/dist/model/backgrounds/index.d.ts +7 -0
- package/dist/model/backgrounds/index.js +7 -0
- package/dist/model/backgrounds/resolve.d.ts +86 -0
- package/dist/model/backgrounds/resolve.js +215 -0
- package/dist/model/backgrounds/schema.d.ts +178 -0
- package/dist/model/backgrounds/schema.js +203 -0
- package/dist/model/codes.d.ts +73 -0
- package/dist/model/codes.js +67 -0
- package/dist/model/deck.d.ts +392 -0
- package/dist/model/deck.js +338 -0
- package/dist/model/index.d.ts +19 -0
- package/dist/model/index.js +19 -0
- package/dist/model/layouts/catalog.d.ts +433 -0
- package/dist/model/layouts/catalog.js +495 -0
- package/dist/model/layouts/index.d.ts +5 -0
- package/dist/model/layouts/index.js +5 -0
- package/dist/model/layouts/props.d.ts +302 -0
- package/dist/model/layouts/props.js +272 -0
- package/dist/model/loader.d.ts +37 -0
- package/dist/model/loader.js +317 -0
- package/dist/model/migrate.d.ts +34 -0
- package/dist/model/migrate.js +453 -0
- package/dist/model/motion/index.d.ts +6 -0
- package/dist/model/motion/index.js +6 -0
- package/dist/model/motion/transition.d.ts +96 -0
- package/dist/model/motion/transition.js +127 -0
- package/dist/model/parse.d.ts +57 -0
- package/dist/model/parse.js +307 -0
- package/dist/model/plain.d.ts +40 -0
- package/dist/model/plain.js +275 -0
- package/dist/model/shared.d.ts +63 -0
- package/dist/model/shared.js +86 -0
- package/dist/model/steps/index.d.ts +7 -0
- package/dist/model/steps/index.js +7 -0
- package/dist/model/steps/phase.d.ts +34 -0
- package/dist/model/steps/phase.js +50 -0
- package/dist/model/steps/slide-steps.d.ts +129 -0
- package/dist/model/steps/slide-steps.js +256 -0
- package/dist/model/style/index.d.ts +8 -0
- package/dist/model/style/index.js +8 -0
- package/dist/model/style/resolve.d.ts +87 -0
- package/dist/model/style/resolve.js +174 -0
- package/dist/model/style/schema.d.ts +85 -0
- package/dist/model/style/schema.js +53 -0
- package/dist/model/style/theme.d.ts +69 -0
- package/dist/model/style/theme.js +84 -0
- package/dist/schema/builders.d.ts +146 -0
- package/dist/schema/builders.js +285 -0
- package/dist/schema/diagnostics.d.ts +78 -0
- package/dist/schema/diagnostics.js +81 -0
- package/dist/schema/index.d.ts +15 -0
- package/dist/schema/index.js +14 -0
- package/dist/schema/internal.d.ts +40 -0
- package/dist/schema/internal.js +209 -0
- package/dist/schema/json-schema.d.ts +12 -0
- package/dist/schema/json-schema.js +213 -0
- package/dist/schema/types.d.ts +166 -0
- package/dist/schema/types.js +1 -0
- package/dist/schema/validate.d.ts +51 -0
- package/dist/schema/validate.js +765 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.js +2 -0
- package/dist/templates/templates.d.ts +11 -0
- package/dist/templates/templates.js +47 -0
- package/package.json +60 -4
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-free deck navigation over a model-v2-shaped deck.
|
|
3
|
+
*
|
|
4
|
+
* Locations are pure data. Hidden slides are skipped by sequential moves;
|
|
5
|
+
* `goTo` of a hidden or unknown id falls back to the first visible slide.
|
|
6
|
+
* Vertical stacks come from `outline.sections` with `stack: true`: next/prev
|
|
7
|
+
* walk steps, then vertical siblings, then the surrounding horizontal order.
|
|
8
|
+
* `stepCount` is the maximum step index on the slide (number of advances);
|
|
9
|
+
* navigable steps are `0 … stepCount` inclusive.
|
|
10
|
+
*/
|
|
11
|
+
import { layoutSteppablePaths } from "../model/layouts/index.js";
|
|
12
|
+
import { slideSteps } from "../model/steps/index.js";
|
|
13
|
+
/**
|
|
14
|
+
* Count distinct positive model-v2 step markers plus explicit step labels.
|
|
15
|
+
* Never throws; unknown shapes contribute nothing. The result is the maximum
|
|
16
|
+
* step index (0 when the slide has no advances).
|
|
17
|
+
*/
|
|
18
|
+
export function modelSlideStepCount(slide) {
|
|
19
|
+
return slideSteps(slide, {
|
|
20
|
+
steppablePaths: layoutSteppablePaths(slide.layout ?? 'free'),
|
|
21
|
+
}).count;
|
|
22
|
+
}
|
|
23
|
+
function clamp(value, min, max) {
|
|
24
|
+
if (!Number.isFinite(value))
|
|
25
|
+
return min;
|
|
26
|
+
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create a pure navigator over a deck-shaped object. The deck is read once;
|
|
30
|
+
* later mutations of the input are ignored.
|
|
31
|
+
*/
|
|
32
|
+
export function createDeckNavigator(deck, options = {}) {
|
|
33
|
+
const slides = deck.slides ?? [];
|
|
34
|
+
const steps = slides.map((slide) => Math.max(0, Math.floor(options.stepCount?.(slide) ?? modelSlideStepCount(slide)) || 0));
|
|
35
|
+
const visible = slides
|
|
36
|
+
.map((slide, index) => ({ slide, index }))
|
|
37
|
+
.filter(({ slide }) => slide.hidden !== true);
|
|
38
|
+
const byId = new Map(slides.map((slide, index) => [slide.id, index]));
|
|
39
|
+
const stackBySlide = new Map();
|
|
40
|
+
for (const section of deck.outline?.sections ?? []) {
|
|
41
|
+
if (!section.stack)
|
|
42
|
+
continue;
|
|
43
|
+
const ids = section.slideIds ?? [];
|
|
44
|
+
ids.forEach((id, index) => {
|
|
45
|
+
stackBySlide.set(id, { sectionId: section.id, index, ids });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const fallback = visible[0] ?? (slides[0] ? { slide: slides[0], index: 0 } : undefined);
|
|
49
|
+
let current = fallback?.index ?? 0;
|
|
50
|
+
let step = clamp(options.initialStep ?? 0, 0, steps[current] ?? 0);
|
|
51
|
+
const resolveStart = (id) => {
|
|
52
|
+
if (!id)
|
|
53
|
+
return;
|
|
54
|
+
const initial = visible.find(({ slide }) => slide.id === id);
|
|
55
|
+
if (initial) {
|
|
56
|
+
current = initial.index;
|
|
57
|
+
step = clamp(options.initialStep ?? 0, 0, steps[current] ?? 0);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
if (options.initialSlideId)
|
|
61
|
+
resolveStart(options.initialSlideId);
|
|
62
|
+
else
|
|
63
|
+
resolveStart(deck.navigation?.start);
|
|
64
|
+
const make = () => {
|
|
65
|
+
const slide = slides[current];
|
|
66
|
+
if (!slide)
|
|
67
|
+
return { slideIndex: 0, slideId: '', step: 0, stepCount: 0 };
|
|
68
|
+
const stack = stackBySlide.get(slide.id);
|
|
69
|
+
return {
|
|
70
|
+
slideIndex: current,
|
|
71
|
+
slideId: slide.id,
|
|
72
|
+
step,
|
|
73
|
+
stepCount: steps[current] ?? 0,
|
|
74
|
+
...(stack ? { sectionId: stack.sectionId, verticalIndex: stack.index } : {}),
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
const set = (index, nextStep = 0) => {
|
|
78
|
+
current = index;
|
|
79
|
+
step = clamp(nextStep, 0, steps[current] ?? 0);
|
|
80
|
+
return make();
|
|
81
|
+
};
|
|
82
|
+
const atVisible = (index) => visible[index]?.index;
|
|
83
|
+
const moveVertical = (direction, fromId) => {
|
|
84
|
+
const stack = stackBySlide.get(fromId);
|
|
85
|
+
if (!stack)
|
|
86
|
+
return undefined;
|
|
87
|
+
for (let offset = stack.index + direction; offset >= 0 && offset < stack.ids.length; offset += direction) {
|
|
88
|
+
const nextIndex = byId.get(stack.ids[offset] ?? '');
|
|
89
|
+
if (nextIndex !== undefined && slides[nextIndex]?.hidden !== true)
|
|
90
|
+
return nextIndex;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
};
|
|
94
|
+
const moveAcross = (direction) => {
|
|
95
|
+
const now = make();
|
|
96
|
+
if (direction > 0 && now.step < now.stepCount)
|
|
97
|
+
return set(current, step + 1);
|
|
98
|
+
if (direction < 0 && now.step > 0)
|
|
99
|
+
return set(current, step - 1);
|
|
100
|
+
const vertical = moveVertical(direction, now.slideId);
|
|
101
|
+
if (vertical !== undefined) {
|
|
102
|
+
return set(vertical, direction > 0 ? 0 : (steps[vertical] ?? 0));
|
|
103
|
+
}
|
|
104
|
+
const visibleIndex = visible.findIndex(({ index }) => index === current);
|
|
105
|
+
let target = atVisible(visibleIndex + direction);
|
|
106
|
+
if (target === undefined && deck.navigation?.loop === true && visible.length > 0) {
|
|
107
|
+
target = direction > 0 ? visible[0]?.index : visible[visible.length - 1]?.index;
|
|
108
|
+
}
|
|
109
|
+
if (target === undefined)
|
|
110
|
+
return make();
|
|
111
|
+
return set(target, direction > 0 ? 0 : (steps[target] ?? 0));
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
getLocation: make,
|
|
115
|
+
visibleSlides: () => visible.map(({ slide }) => slide),
|
|
116
|
+
next: () => moveAcross(1),
|
|
117
|
+
prev: () => moveAcross(-1),
|
|
118
|
+
first: () => (visible[0] ? set(visible[0].index, 0) : make()),
|
|
119
|
+
last: () => {
|
|
120
|
+
const last = visible[visible.length - 1];
|
|
121
|
+
return last ? set(last.index, steps[last.index] ?? 0) : make();
|
|
122
|
+
},
|
|
123
|
+
goTo: (target, targetStep = 0) => {
|
|
124
|
+
let index;
|
|
125
|
+
let resolvedStep = targetStep;
|
|
126
|
+
if (typeof target === 'string') {
|
|
127
|
+
index = byId.get(target);
|
|
128
|
+
}
|
|
129
|
+
else if (typeof target === 'number') {
|
|
130
|
+
if (visible.length === 0)
|
|
131
|
+
return make();
|
|
132
|
+
index = visible[clamp(target, 0, visible.length - 1)]?.index;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
index = byId.get(target.slideId);
|
|
136
|
+
resolvedStep = target.step;
|
|
137
|
+
}
|
|
138
|
+
if (index === undefined || slides[index]?.hidden === true) {
|
|
139
|
+
return fallback ? set(fallback.index, 0) : make();
|
|
140
|
+
}
|
|
141
|
+
return set(index, resolvedStep);
|
|
142
|
+
},
|
|
143
|
+
goToVertical: (sectionId, verticalIndex, targetStep = 0) => {
|
|
144
|
+
const section = deck.outline?.sections?.find((item) => item.id === sectionId && item.stack);
|
|
145
|
+
const id = section?.slideIds[clamp(verticalIndex, 0, (section?.slideIds.length ?? 1) - 1)];
|
|
146
|
+
const index = id ? byId.get(id) : undefined;
|
|
147
|
+
if (index === undefined || slides[index]?.hidden === true)
|
|
148
|
+
return make();
|
|
149
|
+
return set(index, targetStep);
|
|
150
|
+
},
|
|
151
|
+
stepCountFor: (slideId) => {
|
|
152
|
+
const index = byId.get(slideId);
|
|
153
|
+
return index === undefined ? 0 : (steps[index] ?? 0);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closed, versioned presenter protocol v1.
|
|
3
|
+
*
|
|
4
|
+
* Message shape: `{ type, slideId, step }` only (`hello` | `sync` | `request`).
|
|
5
|
+
* Channel name: `texuli-deck-presenter/1:<namespace>`.
|
|
6
|
+
* No `window`, `document` or `BroadcastChannel` is touched at import time;
|
|
7
|
+
* transports are created only when a transport function is called.
|
|
8
|
+
*/
|
|
9
|
+
export declare const PRESENTER_MESSAGE_TYPES: readonly ["hello", "sync", "request"];
|
|
10
|
+
export type PresenterMessageType = (typeof PRESENTER_MESSAGE_TYPES)[number];
|
|
11
|
+
export interface PresenterMessage {
|
|
12
|
+
type: PresenterMessageType;
|
|
13
|
+
slideId: string;
|
|
14
|
+
step: number;
|
|
15
|
+
}
|
|
16
|
+
/** Protocol prefix; the full channel name is `${prefix}${namespace}`. */
|
|
17
|
+
export declare const PRESENTER_PROTOCOL_PREFIX = "texuli-deck-presenter/1:";
|
|
18
|
+
export declare const PRESENTER_PROTOCOL_VERSION: 1;
|
|
19
|
+
/** Sanitize a deck namespace for a channel name. */
|
|
20
|
+
export declare function sanitizePresenterNamespace(namespace: string): string;
|
|
21
|
+
/** Channel name for one deck namespace (same-origin BroadcastChannel). */
|
|
22
|
+
export declare function presenterChannelName(namespace: string): string;
|
|
23
|
+
/** Type guard: closed shape, lowercase slide id, step 0…32. */
|
|
24
|
+
export declare function isPresenterMessage(value: unknown): value is PresenterMessage;
|
|
25
|
+
/** Parse an untrusted payload. Returns `null` when malformed (never throws). */
|
|
26
|
+
export declare function parsePresenterMessage(value: unknown): PresenterMessage | null;
|
|
27
|
+
/**
|
|
28
|
+
* Build a protocol message. Returns `null` when the arguments are invalid
|
|
29
|
+
* (never throws; the stage decides how to handle a rejected build).
|
|
30
|
+
*/
|
|
31
|
+
export declare function presenterMessage(type: PresenterMessageType, slideId: string, step: number): PresenterMessage | null;
|
|
32
|
+
export interface PresenterTransport {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
post(message: PresenterMessage): void;
|
|
35
|
+
subscribe(listener: (message: PresenterMessage) => void): () => void;
|
|
36
|
+
close(): void;
|
|
37
|
+
}
|
|
38
|
+
/** Duck-typed channel (BroadcastChannel or a test double). */
|
|
39
|
+
export interface PresenterChannelLike {
|
|
40
|
+
postMessage(value: unknown): void;
|
|
41
|
+
addEventListener(type: 'message', listener: (event: {
|
|
42
|
+
data: unknown;
|
|
43
|
+
}) => void): void;
|
|
44
|
+
removeEventListener(type: 'message', listener: (event: {
|
|
45
|
+
data: unknown;
|
|
46
|
+
}) => void): void;
|
|
47
|
+
close(): void;
|
|
48
|
+
}
|
|
49
|
+
/** Wrap an existing channel-like object. Does not construct globals. */
|
|
50
|
+
export declare function createPresenterTransport(channel: PresenterChannelLike, name?: string): PresenterTransport;
|
|
51
|
+
/** In-memory bus for unit tests (same process, no DOM). */
|
|
52
|
+
export declare function createMemoryPresenterTransport(namespace?: string): PresenterTransport;
|
|
53
|
+
/**
|
|
54
|
+
* Optional BroadcastChannel transport. Returns `null` when the host has no
|
|
55
|
+
* BroadcastChannel or construction fails. The factory is injected so the
|
|
56
|
+
* module never reads `globalThis.BroadcastChannel` at import time.
|
|
57
|
+
*/
|
|
58
|
+
export declare function createBroadcastPresenterTransport(namespace: string, channelFactory?: new (name: string) => PresenterChannelLike): PresenterTransport | null;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closed, versioned presenter protocol v1.
|
|
3
|
+
*
|
|
4
|
+
* Message shape: `{ type, slideId, step }` only (`hello` | `sync` | `request`).
|
|
5
|
+
* Channel name: `texuli-deck-presenter/1:<namespace>`.
|
|
6
|
+
* No `window`, `document` or `BroadcastChannel` is touched at import time;
|
|
7
|
+
* transports are created only when a transport function is called.
|
|
8
|
+
*/
|
|
9
|
+
export const PRESENTER_MESSAGE_TYPES = ['hello', 'sync', 'request'];
|
|
10
|
+
/** Protocol prefix; the full channel name is `${prefix}${namespace}`. */
|
|
11
|
+
export const PRESENTER_PROTOCOL_PREFIX = 'texuli-deck-presenter/1:';
|
|
12
|
+
export const PRESENTER_PROTOCOL_VERSION = 1;
|
|
13
|
+
const SLIDE_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
14
|
+
const MAX_STEP = 32;
|
|
15
|
+
/** Sanitize a deck namespace for a channel name. */
|
|
16
|
+
export function sanitizePresenterNamespace(namespace) {
|
|
17
|
+
// Bounded and linear: cap the host string before any pattern runs, then
|
|
18
|
+
// trim the edge dashes with index scans (no `^-+|-+$` backtracking).
|
|
19
|
+
const raw = typeof namespace === 'string' ? namespace.slice(0, 512).trim() : '';
|
|
20
|
+
const dashed = raw.toLowerCase().replace(/[^a-z0-9._:-]+/g, '-');
|
|
21
|
+
let start = 0;
|
|
22
|
+
let end = dashed.length;
|
|
23
|
+
while (start < end && dashed[start] === '-')
|
|
24
|
+
start += 1;
|
|
25
|
+
while (end > start && dashed[end - 1] === '-')
|
|
26
|
+
end -= 1;
|
|
27
|
+
const cleaned = dashed.slice(start, end).slice(0, 96);
|
|
28
|
+
return cleaned || 'default';
|
|
29
|
+
}
|
|
30
|
+
/** Channel name for one deck namespace (same-origin BroadcastChannel). */
|
|
31
|
+
export function presenterChannelName(namespace) {
|
|
32
|
+
return `${PRESENTER_PROTOCOL_PREFIX}${sanitizePresenterNamespace(namespace)}`;
|
|
33
|
+
}
|
|
34
|
+
/** Type guard: closed shape, lowercase slide id, step 0…32. */
|
|
35
|
+
export function isPresenterMessage(value) {
|
|
36
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
37
|
+
return false;
|
|
38
|
+
const message = value;
|
|
39
|
+
const keys = Object.keys(message);
|
|
40
|
+
if (keys.length !== 3)
|
|
41
|
+
return false;
|
|
42
|
+
if (!PRESENTER_MESSAGE_TYPES.includes(message.type))
|
|
43
|
+
return false;
|
|
44
|
+
if (typeof message.slideId !== 'string' || !SLIDE_ID.test(message.slideId))
|
|
45
|
+
return false;
|
|
46
|
+
if (typeof message.step !== 'number' || !Number.isInteger(message.step))
|
|
47
|
+
return false;
|
|
48
|
+
if (message.step < 0 || message.step > MAX_STEP)
|
|
49
|
+
return false;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/** Parse an untrusted payload. Returns `null` when malformed (never throws). */
|
|
53
|
+
export function parsePresenterMessage(value) {
|
|
54
|
+
return isPresenterMessage(value)
|
|
55
|
+
? { type: value.type, slideId: value.slideId, step: value.step }
|
|
56
|
+
: null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build a protocol message. Returns `null` when the arguments are invalid
|
|
60
|
+
* (never throws; the stage decides how to handle a rejected build).
|
|
61
|
+
*/
|
|
62
|
+
export function presenterMessage(type, slideId, step) {
|
|
63
|
+
return parsePresenterMessage({ type, slideId, step });
|
|
64
|
+
}
|
|
65
|
+
/** Wrap an existing channel-like object. Does not construct globals. */
|
|
66
|
+
export function createPresenterTransport(channel, name = 'channel') {
|
|
67
|
+
const listeners = new Set();
|
|
68
|
+
const onMessage = (event) => {
|
|
69
|
+
const message = parsePresenterMessage(event.data);
|
|
70
|
+
if (!message)
|
|
71
|
+
return;
|
|
72
|
+
for (const listener of [...listeners])
|
|
73
|
+
listener(message);
|
|
74
|
+
};
|
|
75
|
+
channel.addEventListener('message', onMessage);
|
|
76
|
+
return {
|
|
77
|
+
name,
|
|
78
|
+
post(message) {
|
|
79
|
+
if (!isPresenterMessage(message))
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
channel.postMessage(message);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Closed or structured-clone failure — drop silently.
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
subscribe(listener) {
|
|
89
|
+
listeners.add(listener);
|
|
90
|
+
return () => {
|
|
91
|
+
listeners.delete(listener);
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
close() {
|
|
95
|
+
listeners.clear();
|
|
96
|
+
channel.removeEventListener('message', onMessage);
|
|
97
|
+
try {
|
|
98
|
+
channel.close();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// already closed
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** In-memory bus for unit tests (same process, no DOM). */
|
|
107
|
+
export function createMemoryPresenterTransport(namespace = 'test') {
|
|
108
|
+
const name = presenterChannelName(namespace);
|
|
109
|
+
const listeners = new Set();
|
|
110
|
+
return {
|
|
111
|
+
name,
|
|
112
|
+
post(message) {
|
|
113
|
+
if (!isPresenterMessage(message))
|
|
114
|
+
return;
|
|
115
|
+
const copy = {
|
|
116
|
+
type: message.type,
|
|
117
|
+
slideId: message.slideId,
|
|
118
|
+
step: message.step,
|
|
119
|
+
};
|
|
120
|
+
for (const listener of [...listeners])
|
|
121
|
+
listener(copy);
|
|
122
|
+
},
|
|
123
|
+
subscribe(handler) {
|
|
124
|
+
listeners.add(handler);
|
|
125
|
+
return () => {
|
|
126
|
+
listeners.delete(handler);
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
close() {
|
|
130
|
+
listeners.clear();
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Optional BroadcastChannel transport. Returns `null` when the host has no
|
|
136
|
+
* BroadcastChannel or construction fails. The factory is injected so the
|
|
137
|
+
* module never reads `globalThis.BroadcastChannel` at import time.
|
|
138
|
+
*/
|
|
139
|
+
export function createBroadcastPresenterTransport(namespace, channelFactory) {
|
|
140
|
+
const Factory = channelFactory ??
|
|
141
|
+
(typeof globalThis !== 'undefined' &&
|
|
142
|
+
typeof globalThis.BroadcastChannel === 'function'
|
|
143
|
+
? globalThis
|
|
144
|
+
.BroadcastChannel
|
|
145
|
+
: undefined);
|
|
146
|
+
if (typeof Factory !== 'function')
|
|
147
|
+
return null;
|
|
148
|
+
const name = presenterChannelName(namespace);
|
|
149
|
+
let channel;
|
|
150
|
+
try {
|
|
151
|
+
channel = new Factory(name);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return createPresenterTransport(channel, name);
|
|
157
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @texuli/deck: the framework-free, dependency-free deck engine (ADR-009).
|
|
3
|
+
* In development (PLAN_008). This root re-exports the deck model v2
|
|
4
|
+
* (`./model`) and the owned schema kit (`./schema`); import the engine,
|
|
5
|
+
* Markdown, math, authoring and templates from their own subpaths so a host
|
|
6
|
+
* loads only what it uses.
|
|
7
|
+
*/
|
|
8
|
+
export * from './model/index.js';
|
|
9
|
+
export * from './schema/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @texuli/deck: the framework-free, dependency-free deck engine (ADR-009).
|
|
3
|
+
* In development (PLAN_008). This root re-exports the deck model v2
|
|
4
|
+
* (`./model`) and the owned schema kit (`./schema`); import the engine,
|
|
5
|
+
* Markdown, math, authoring and templates from their own subpaths so a host
|
|
6
|
+
* loads only what it uses.
|
|
7
|
+
*/
|
|
8
|
+
export * from "./model/index.js";
|
|
9
|
+
export * from "./schema/index.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The attribute lexer for `<!-- .slide: … -->`, `<!-- .element: … -->` and
|
|
3
|
+
* `{#id name=value flag}` brace lists. One left-to-right pass; values are
|
|
4
|
+
* quoted (`"…"`, `'…'`, no escapes) or bare tokens. The closed attribute sets
|
|
5
|
+
* themselves live with the code that reads them.
|
|
6
|
+
*/
|
|
7
|
+
import type { IssueSink } from './issues.js';
|
|
8
|
+
export interface Attr {
|
|
9
|
+
name: string;
|
|
10
|
+
/** `true` for a bare flag (`hidden`, `numbers`). */
|
|
11
|
+
value: string | true;
|
|
12
|
+
line: number;
|
|
13
|
+
/** 1-based column of the attribute name. */
|
|
14
|
+
column: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Read attributes from `text`, which starts at `column` on `line`. In brace
|
|
18
|
+
* mode `#slug` is an `id` and `.name` is a `class`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function readAttributes(text: string, line: number, column: number, sink: IssueSink, brace?: boolean): Attr[];
|
|
21
|
+
/** An attribute value as written in canonical output: bare when it is a plain token. */
|
|
22
|
+
export declare function writeAttribute(name: string, value: string | number | true): string;
|
|
23
|
+
/** True when a value can be written as an attribute at all (one quote style fits). */
|
|
24
|
+
export declare function attributeWritable(value: string): boolean;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { DECK_MARKDOWN_LIMITS } from "./limits.js";
|
|
2
|
+
const NAME_START = /[A-Za-z_]/;
|
|
3
|
+
const NAME_CHAR = /[A-Za-z0-9_.:-]/;
|
|
4
|
+
const BARE_CHAR = /[^\s"'=<>`{}]/;
|
|
5
|
+
/**
|
|
6
|
+
* Read attributes from `text`, which starts at `column` on `line`. In brace
|
|
7
|
+
* mode `#slug` is an `id` and `.name` is a `class`.
|
|
8
|
+
*/
|
|
9
|
+
export function readAttributes(text, line, column, sink, brace = false) {
|
|
10
|
+
const out = [];
|
|
11
|
+
let i = 0;
|
|
12
|
+
const bad = (message) => {
|
|
13
|
+
sink.report('md-attr-value', message, { line, column: column + i });
|
|
14
|
+
};
|
|
15
|
+
while (i < text.length) {
|
|
16
|
+
const c = text[i] ?? '';
|
|
17
|
+
if (c === ' ' || c === '\t') {
|
|
18
|
+
i++;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (out.length >= DECK_MARKDOWN_LIMITS.attributes) {
|
|
22
|
+
bad(`At most ${DECK_MARKDOWN_LIMITS.attributes} attributes per comment or brace list.`);
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
const start = i;
|
|
26
|
+
if (brace && (c === '#' || c === '.')) {
|
|
27
|
+
i++;
|
|
28
|
+
while (i < text.length && NAME_CHAR.test(text[i] ?? ''))
|
|
29
|
+
i++;
|
|
30
|
+
const value = text.slice(start + 1, i);
|
|
31
|
+
if (value === '') {
|
|
32
|
+
bad(`\`${c}\` needs a name after it.`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
out.push({ name: c === '#' ? 'id' : 'class', value, line, column: column + start });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!NAME_START.test(c)) {
|
|
39
|
+
bad(`Unexpected \`${c}\`; attributes look like name="value".`);
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
while (i < text.length && NAME_CHAR.test(text[i] ?? ''))
|
|
43
|
+
i++;
|
|
44
|
+
const name = text.slice(start, i);
|
|
45
|
+
if (text[i] !== '=') {
|
|
46
|
+
out.push({ name, value: true, line, column: column + start });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
i++;
|
|
50
|
+
const quote = text[i];
|
|
51
|
+
let value;
|
|
52
|
+
if (quote === '"' || quote === "'") {
|
|
53
|
+
const end = text.indexOf(quote, i + 1);
|
|
54
|
+
if (end === -1) {
|
|
55
|
+
bad(`The value of "${name.slice(0, 40)}" has no closing ${quote}.`);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
value = text.slice(i + 1, end);
|
|
59
|
+
i = end + 1;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const valueStart = i;
|
|
63
|
+
while (i < text.length && BARE_CHAR.test(text[i] ?? ''))
|
|
64
|
+
i++;
|
|
65
|
+
value = text.slice(valueStart, i);
|
|
66
|
+
if (value === '') {
|
|
67
|
+
bad(`"${name.slice(0, 40)}=" needs a value.`);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (value.length > DECK_MARKDOWN_LIMITS.attributeValue) {
|
|
72
|
+
bad(`An attribute value is at most ${DECK_MARKDOWN_LIMITS.attributeValue} characters.`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
out.push({ name, value, line, column: column + start });
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/** An attribute value as written in canonical output: bare when it is a plain token. */
|
|
80
|
+
export function writeAttribute(name, value) {
|
|
81
|
+
if (value === true)
|
|
82
|
+
return name;
|
|
83
|
+
const text = String(value);
|
|
84
|
+
if (/^[A-Za-z0-9._/:#+-]+$/.test(text))
|
|
85
|
+
return `${name}=${text}`;
|
|
86
|
+
return text.includes('"') ? `${name}='${text}'` : `${name}="${text}"`;
|
|
87
|
+
}
|
|
88
|
+
/** True when a value can be written as an attribute at all (one quote style fits). */
|
|
89
|
+
export function attributeWritable(value) {
|
|
90
|
+
return (value.length <= DECK_MARKDOWN_LIMITS.attributeValue &&
|
|
91
|
+
!value.includes('\n') &&
|
|
92
|
+
!value.includes('-->') &&
|
|
93
|
+
!(value.includes('"') && value.includes("'")));
|
|
94
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { BLOCK_REVEALS, type FragmentEffect, type FragmentTone } from '../model/shared.js';
|
|
2
|
+
import type { Attr } from './attributes.js';
|
|
3
|
+
import type { IssueSink } from './issues.js';
|
|
4
|
+
/** A fragment request read from attributes. */
|
|
5
|
+
export interface MarkSpec {
|
|
6
|
+
/** A fragment was asked for (`fragment`, `class="fragment"`, `step` or `reveal`). */
|
|
7
|
+
fragment: boolean;
|
|
8
|
+
effect?: FragmentEffect;
|
|
9
|
+
tone?: FragmentTone | 'neutral';
|
|
10
|
+
reveal?: (typeof BLOCK_REVEALS)[number];
|
|
11
|
+
/** An explicit step number. */
|
|
12
|
+
step?: number;
|
|
13
|
+
line: number;
|
|
14
|
+
column: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ElementRead {
|
|
17
|
+
mark?: MarkSpec;
|
|
18
|
+
id?: string;
|
|
19
|
+
/** Other allowed attributes, by name. */
|
|
20
|
+
other: Map<string, Attr>;
|
|
21
|
+
}
|
|
22
|
+
/** reveal.js and deck effect names → a deck fragment effect (and tone). */
|
|
23
|
+
export declare function mapEffect(name: string): {
|
|
24
|
+
effect: FragmentEffect;
|
|
25
|
+
tone?: FragmentTone;
|
|
26
|
+
approximate?: boolean;
|
|
27
|
+
} | null;
|
|
28
|
+
/**
|
|
29
|
+
* Read `.element` or brace attributes. `allowed` names the extra attributes
|
|
30
|
+
* this target accepts (width, height, avatar…); the fragment attributes and
|
|
31
|
+
* `id` are always read.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readElement(attrs: readonly Attr[], allowed: ReadonlySet<string>, sink: IssueSink): ElementRead;
|
|
34
|
+
/** What `.slide` attributes set on a slide (before ids and layouts are resolved). */
|
|
35
|
+
export interface SlideAttrRead {
|
|
36
|
+
id?: string;
|
|
37
|
+
layout?: string;
|
|
38
|
+
master?: string;
|
|
39
|
+
title?: string;
|
|
40
|
+
theme?: string;
|
|
41
|
+
transition?: string;
|
|
42
|
+
hidden?: boolean;
|
|
43
|
+
steps?: string[];
|
|
44
|
+
background?: unknown;
|
|
45
|
+
section?: string;
|
|
46
|
+
sectionTitle?: string;
|
|
47
|
+
sectionEnd?: boolean;
|
|
48
|
+
/** Where each field was written (for the source map). */
|
|
49
|
+
at: Map<string, Attr>;
|
|
50
|
+
}
|
|
51
|
+
/** Read `.slide` attributes: aliases mapped, refusals reported, the background assembled. */
|
|
52
|
+
export declare function readSlideAttrs(attrs: readonly Attr[], sink: IssueSink): SlideAttrRead;
|