@bytesbybrandon/living-docs 0.2.1 → 0.3.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/README.md +6 -4
- package/docs/guide/cli.md +61 -0
- package/docs/guide/document-model.md +117 -0
- package/docs/guide/guarantees.md +31 -0
- package/docs/guide/mcp.md +47 -0
- package/docs/guide/pipeline.md +36 -0
- package/docs/guide/reactive-workflow.md +21 -0
- package/docs/guide/style-tokens.md +60 -0
- package/docs/guide/timeline.md +75 -0
- package/package.json +14 -2
- package/samples/agent-skills-audit.json +38 -1
- package/samples/end-of-harness-engineering.json +30 -7
- package/samples/harness-engineering.json +32 -2
- package/src/axi/cli.js +53 -7
- package/src/export/video.js +150 -0
- package/src/ingest/cues.js +81 -0
- package/src/ingest/index.js +9 -3
- package/src/ingest/style.js +57 -0
- package/src/mcp/server.js +32 -8
- package/src/model/markdown.js +7 -2
- package/src/model/schema.js +64 -12
- package/src/model/validate.js +82 -4
- package/src/render/blocks.js +9 -5
- package/src/render/index.js +16 -5
- package/src/render/style.js +1 -1
- package/src/render/svg.js +13 -16
- package/src/render/themes/hud.js +18 -7
- package/src/render/themes/variants.js +43 -0
- package/src/render/timeline.js +304 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// Timeline cues: one clock, every frame a pure function of t.
|
|
2
|
+
// The live page plays the cues once (or loops); video export calls window.__ld.seek(t) per frame.
|
|
3
|
+
|
|
4
|
+
const DEFAULT_DUR = 1.2;
|
|
5
|
+
const STAGE_ONLY_SECONDS = 6;
|
|
6
|
+
|
|
7
|
+
export function timelineDuration(model) {
|
|
8
|
+
const timeline = model.timeline;
|
|
9
|
+
if (!timeline) return STAGE_ONLY_SECONDS;
|
|
10
|
+
if (timeline.duration) return timeline.duration;
|
|
11
|
+
const end = Math.max(0, ...timeline.cues.map(cue => cue.at + (cue.dur || DEFAULT_DUR)));
|
|
12
|
+
return Math.round((end + 1.5) * 1000) / 1000;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function payload(model) {
|
|
16
|
+
const cues = (model.timeline?.cues || [])
|
|
17
|
+
.map(cue => ({ at: cue.at, dur: cue.dur || DEFAULT_DUR, do: cue.do, target: cue.target, ...(cue.zoom ? { zoom: cue.zoom } : {}) }))
|
|
18
|
+
.sort((a, b) => a.at - b.at);
|
|
19
|
+
return { duration: timelineDuration(model), loop: !!model.timeline?.loop, cues };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PROPS = [['r', 1], ['f', 0], ['dim', 0], ['hl', 0], ['ha', 0], ['hr', 0]];
|
|
23
|
+
|
|
24
|
+
const CSS = `
|
|
25
|
+
${PROPS.map(([name, initial]) => `@property --ld-${name}{syntax:'<number>';inherits:false;initial-value:${initial}}`).join('\n')}
|
|
26
|
+
.ld-mark{--ld-hlc:color-mix(in srgb,var(--ld-accent) calc(var(--ld-ha)*42%),transparent);background-image:linear-gradient(var(--ld-hlc),var(--ld-hlc));background-repeat:no-repeat;background-size:calc(var(--ld-hl)*100%) 100%;text-decoration-line:underline;text-decoration-color:color-mix(in srgb,var(--ld-accent) calc(var(--ld-hr)*75%),transparent);text-decoration-thickness:1.5px;text-underline-offset:.24em;border-radius:2px}
|
|
27
|
+
.ld-t:not(svg *):not(.ld-mark){opacity:calc(var(--ld-r)*(1 - var(--ld-dim)*.4 + var(--ld-f)*.4));outline:calc(var(--ld-f)*2px) solid color-mix(in srgb,var(--ld-accent) calc(var(--ld-f)*100%),transparent);outline-offset:-2px;box-shadow:inset 0 0 calc(var(--ld-f)*30px) color-mix(in srgb,var(--ld-accent) calc(var(--ld-f)*26%),transparent),inset 0 0 0 9999px color-mix(in srgb,var(--ld-accent) calc(var(--ld-ha)*9%),transparent)}
|
|
28
|
+
svg .ld-t{filter:opacity(calc(var(--ld-r)*(1 - var(--ld-dim)*.4 + var(--ld-f)*.4))) drop-shadow(0 0 calc(var(--ld-f)*5px) color-mix(in srgb,var(--ld-accent) calc(var(--ld-f)*100%),transparent))}
|
|
29
|
+
.ld-count{font-variant-numeric:tabular-nums}
|
|
30
|
+
::highlight(ld-untyped){color:transparent}
|
|
31
|
+
.scene-label.is-matched{color:var(--ld-accent)}
|
|
32
|
+
html.ld-stage,html.ld-stage body{overflow:hidden!important}
|
|
33
|
+
html.ld-stage ::-webkit-scrollbar{display:none}
|
|
34
|
+
.ld-cam{transform-origin:0 0}
|
|
35
|
+
@media print{.ld-t{opacity:1!important;filter:none!important;outline:none!important;box-shadow:none!important}.ld-mark{background:none!important}.ld-cam{transform:none!important}}`;
|
|
36
|
+
|
|
37
|
+
const RUNTIME = `
|
|
38
|
+
(function(){
|
|
39
|
+
var dataEl = document.getElementById('ld-timeline');
|
|
40
|
+
if (!dataEl) return;
|
|
41
|
+
var data = JSON.parse(dataEl.textContent);
|
|
42
|
+
var root = document.documentElement;
|
|
43
|
+
var stage = root.classList.contains('ld-stage');
|
|
44
|
+
var duration = data.duration;
|
|
45
|
+
var PROPS = ${JSON.stringify(PROPS.map(([name]) => name))};
|
|
46
|
+
function clamp(x){ return x < 0 ? 0 : x > 1 ? 1 : x; }
|
|
47
|
+
function ease(x){ x = clamp(x); return x < .5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; }
|
|
48
|
+
function lerp(a, b, p){ return a + (b - a) * p; }
|
|
49
|
+
function resolve(target){
|
|
50
|
+
if (target === 'page') return null;
|
|
51
|
+
if (target.indexOf('mark:') === 0) return document.querySelector('[data-mark="' + target.slice(5) + '"]');
|
|
52
|
+
var parts = target.split('/');
|
|
53
|
+
var block = document.querySelector('[data-cue="' + parts[0] + '"]');
|
|
54
|
+
if (!block || parts.length < 2) return block;
|
|
55
|
+
var list = block.querySelectorAll('[data-part]');
|
|
56
|
+
for (var i = 0; i < list.length; i++) if (list[i].getAttribute('data-part') === parts[1]) return list[i];
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function textNodes(el){
|
|
60
|
+
var out = [], walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT), node;
|
|
61
|
+
while ((node = walker.nextNode())) out.push({ node: node, text: node.nodeValue });
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
function siblingsOf(el){
|
|
65
|
+
var block = el.closest('[data-cue]');
|
|
66
|
+
return block ? block.querySelectorAll('[data-part]') : [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
var items = [], cams = [], scenes = [], managed = [];
|
|
70
|
+
function manage(el){ if (!el.__ld) { el.__ld = {}; managed.push(el); el.classList.add('ld-t'); } }
|
|
71
|
+
data.cues.forEach(function(cue){
|
|
72
|
+
var el = resolve(cue.target);
|
|
73
|
+
if (cue.do === 'camera') { cams.push({ cue: cue, el: el }); return; }
|
|
74
|
+
if (!el) return;
|
|
75
|
+
if (cue.do === 'activate') {
|
|
76
|
+
var scene = el.closest('.scene');
|
|
77
|
+
if (!scene) return;
|
|
78
|
+
if (scenes.indexOf(scene) < 0) {
|
|
79
|
+
scenes.push(scene);
|
|
80
|
+
scene.__ldMoves = [];
|
|
81
|
+
scene.__ldParts = ['.scene-crosshair', '.scene-active-label', '.scene-badge', '.scene-card'].map(function(sel){ return scene.querySelector(sel); });
|
|
82
|
+
var cross = scene.__ldParts[0];
|
|
83
|
+
scene.__ldHome = cross ? { x: parseFloat(cross.style.left), y: parseFloat(cross.style.top) } : { x: 50, y: 50 };
|
|
84
|
+
scene.__ldText = scene.__ldParts[1] ? scene.__ldParts[1].textContent : '';
|
|
85
|
+
}
|
|
86
|
+
scene.__ldMoves.push({ cue: cue, el: el });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
manage(el);
|
|
90
|
+
if (el.hasAttribute('data-part')) Array.prototype.forEach.call(siblingsOf(el), manage);
|
|
91
|
+
if (cue.do === 'reveal') { el.classList.add('ld-rv'); el.__ldAt = el.__ldAt == null ? cue.at : Math.min(el.__ldAt, cue.at); }
|
|
92
|
+
// Panels count and type their body only, so the panel number and title stay put.
|
|
93
|
+
var body = el.querySelector('.panel-body') || el;
|
|
94
|
+
if (cue.do === 'count') { el.classList.add('ld-count'); el.__ldText = el.__ldText || textNodes(body); }
|
|
95
|
+
if (cue.do === 'type') el.__ldText = el.__ldText || textNodes(body);
|
|
96
|
+
items.push({ cue: cue, el: el });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
var cam = null;
|
|
100
|
+
if (stage) {
|
|
101
|
+
cam = document.createElement('div');
|
|
102
|
+
cam.className = 'ld-cam';
|
|
103
|
+
while (document.body.firstChild) cam.appendChild(document.body.firstChild);
|
|
104
|
+
document.body.appendChild(cam);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function put(el, name, value){ if (!(el.__ld[name] >= value)) el.__ld[name] = value; }
|
|
108
|
+
function countText(src, p){
|
|
109
|
+
return src.replace(/\\d[\\d,]*(?:\\.\\d+)?/g, function(m){
|
|
110
|
+
var dec = (m.split('.')[1] || '').length;
|
|
111
|
+
var v = parseFloat(m.replace(/,/g, '')) * p;
|
|
112
|
+
return m.indexOf(',') >= 0
|
|
113
|
+
? v.toLocaleString('en-US', { minimumFractionDigits: dec, maximumFractionDigits: dec })
|
|
114
|
+
: v.toFixed(dec);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var ONCE = { reveal: 1, count: 1, type: 1 };
|
|
119
|
+
// settled: a looping live page keeps reveals, counts, and typing at their end state after the first pass.
|
|
120
|
+
// shift(item): live-only delay for a block that was still below the fold when its cue came up.
|
|
121
|
+
function applyCues(t0, settled, shift){
|
|
122
|
+
var i, el;
|
|
123
|
+
for (i = 0; i < managed.length; i++) managed[i].__ld = {};
|
|
124
|
+
for (i = 0; i < items.length; i++) {
|
|
125
|
+
var cue = items[i].cue, at = cue.at, dur = cue.dur;
|
|
126
|
+
el = items[i].el;
|
|
127
|
+
var t = shift ? t0 - shift(items[i]) : t0;
|
|
128
|
+
var ct = settled && ONCE[cue.do] ? Infinity : t;
|
|
129
|
+
var p = clamp((ct - at) / dur);
|
|
130
|
+
if (cue.do === 'highlight') {
|
|
131
|
+
put(el, 'hl', ease((t - at) / Math.min(.9, dur * .6)));
|
|
132
|
+
put(el, 'ha', t < at ? 0 : clamp((at + dur - t) / .45));
|
|
133
|
+
put(el, 'hr', clamp((t - (at + dur - .45)) / .45));
|
|
134
|
+
} else if (cue.do === 'focus') {
|
|
135
|
+
var env = Math.min(clamp((t - at) / .35), clamp((at + dur - t) / .45));
|
|
136
|
+
put(el, 'f', env);
|
|
137
|
+
if (el.hasAttribute('data-part')) Array.prototype.forEach.call(siblingsOf(el), function(part){ put(part, 'dim', env); });
|
|
138
|
+
} else if (cue.do === 'reveal') {
|
|
139
|
+
put(el, 'r', ease((ct - at) / Math.min(dur, .9)));
|
|
140
|
+
} else if (cue.do === 'count') {
|
|
141
|
+
put(el, 'count', ease(p));
|
|
142
|
+
} else if (cue.do === 'type') {
|
|
143
|
+
put(el, 'type', p);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
var untyped = [];
|
|
147
|
+
for (i = 0; i < managed.length; i++) {
|
|
148
|
+
el = managed[i];
|
|
149
|
+
var state = el.__ld;
|
|
150
|
+
for (var k = 0; k < PROPS.length; k++) {
|
|
151
|
+
var name = PROPS[k];
|
|
152
|
+
if (state[name] == null) el.style.removeProperty('--ld-' + name);
|
|
153
|
+
else el.style.setProperty('--ld-' + name, state[name].toFixed(4));
|
|
154
|
+
}
|
|
155
|
+
if (state.count != null) el.__ldText.forEach(function(n){ n.node.nodeValue = countText(n.text, state.count); });
|
|
156
|
+
if (state.type != null && window.Highlight && window.CSS && CSS.highlights) {
|
|
157
|
+
var left = Math.round(state.type * el.__ldText.reduce(function(sum, n){ return sum + n.text.length; }, 0));
|
|
158
|
+
el.__ldText.forEach(function(n){
|
|
159
|
+
if (left < n.text.length) { var r = document.createRange(); r.setStart(n.node, Math.max(0, left)); r.setEnd(n.node, n.text.length); untyped.push(r); }
|
|
160
|
+
left -= n.text.length;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (window.Highlight && window.CSS && CSS.highlights) {
|
|
165
|
+
if (untyped.length) CSS.highlights.set('ld-untyped', new Highlight(...untyped)); else CSS.highlights.delete('ld-untyped');
|
|
166
|
+
}
|
|
167
|
+
scenes.forEach(function(scene){ moveScene(scene, t0, shift); });
|
|
168
|
+
if (cam) moveCamera(t0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function moveScene(scene, t0, shift){
|
|
172
|
+
var pos = { x: scene.__ldHome.x, y: scene.__ldHome.y }, text = scene.__ldText, away = 0, current = null;
|
|
173
|
+
scene.__ldMoves.forEach(function(move){
|
|
174
|
+
var t = shift ? t0 - shift(move) : t0;
|
|
175
|
+
if (t < move.cue.at) return;
|
|
176
|
+
var p = ease((t - move.cue.at) / move.cue.dur);
|
|
177
|
+
var to = { x: parseFloat(move.el.style.left), y: parseFloat(move.el.style.top) };
|
|
178
|
+
pos = { x: lerp(pos.x, to.x, p), y: lerp(pos.y, to.y, p) };
|
|
179
|
+
if (p >= .5) text = move.el.getAttribute('data-part');
|
|
180
|
+
away = Math.max(away, p);
|
|
181
|
+
current = move.el;
|
|
182
|
+
});
|
|
183
|
+
scene.__ldMoves.forEach(function(move){ move.el.classList.toggle('is-matched', move.el === current); });
|
|
184
|
+
var parts = scene.__ldParts;
|
|
185
|
+
for (var i = 0; i < 3; i++) if (parts[i]) { parts[i].style.left = pos.x.toFixed(3) + '%'; parts[i].style.top = pos.y.toFixed(3) + '%'; }
|
|
186
|
+
if (parts[1]) parts[1].classList.toggle('to-left', pos.x > 55);
|
|
187
|
+
if (parts[2]) parts[2].classList.toggle('to-left', pos.x > 55);
|
|
188
|
+
if (parts[1]) parts[1].textContent = text;
|
|
189
|
+
if (parts[3]) parts[3].style.opacity = String(1 - away);
|
|
190
|
+
scene.__ldActive = pos;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function rectOf(el){
|
|
194
|
+
var r = el.getBoundingClientRect(), c = cam.getBoundingClientRect();
|
|
195
|
+
return { x: r.left - c.left, y: r.top - c.top, w: r.width, h: r.height };
|
|
196
|
+
}
|
|
197
|
+
function moveCamera(t){
|
|
198
|
+
cam.style.transform = 'none';
|
|
199
|
+
var W = window.innerWidth, H = window.innerHeight, pageW = cam.scrollWidth, pageH = cam.scrollHeight;
|
|
200
|
+
var view = { z: 1, x: 0, y: 0 };
|
|
201
|
+
cams.forEach(function(item){
|
|
202
|
+
if (t < item.cue.at) return;
|
|
203
|
+
var goal = { z: 1, x: 0, y: 0 };
|
|
204
|
+
if (item.el) {
|
|
205
|
+
var r = rectOf(item.el);
|
|
206
|
+
var z = Math.max(1, Math.min(3, item.cue.zoom || 1));
|
|
207
|
+
goal = { z: z, x: W / 2 - (r.x + r.w / 2) * z, y: r.h * z > H * .9 ? 24 - r.y * z : H / 2 - (r.y + r.h / 2) * z };
|
|
208
|
+
goal.x = Math.min(0, Math.max(W - pageW * z, goal.x));
|
|
209
|
+
goal.y = Math.min(0, Math.max(H - pageH * z, goal.y));
|
|
210
|
+
}
|
|
211
|
+
var p = ease((t - item.cue.at) / item.cue.dur);
|
|
212
|
+
view = { z: lerp(view.z, goal.z, p), x: lerp(view.x, goal.x, p), y: lerp(view.y, goal.y, p) };
|
|
213
|
+
});
|
|
214
|
+
cam.style.transform = 'translate(' + view.x.toFixed(2) + 'px,' + view.y.toFixed(2) + 'px) scale(' + view.z.toFixed(4) + ')';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Ambient motion (theme canvases, CSS figure motion) runs on the same clock.
|
|
218
|
+
function applyAmbient(t, live){
|
|
219
|
+
var hooks = window.__ldHooks || [];
|
|
220
|
+
for (var i = 0; i < hooks.length; i++) hooks[i](t * 1000);
|
|
221
|
+
if (!document.getAnimations) return;
|
|
222
|
+
var list = document.getAnimations();
|
|
223
|
+
for (var j = 0; j < list.length; j++) {
|
|
224
|
+
// Live pages leave hover and focus transitions to the browser.
|
|
225
|
+
if (live && window.CSSTransition && list[j] instanceof CSSTransition) continue;
|
|
226
|
+
var target = list[j].effect && list[j].effect.target;
|
|
227
|
+
var owner = target && target.closest ? target.closest('.ld-rv') : null;
|
|
228
|
+
var offset = owner && owner.__ldAt != null ? owner.__ldAt : 0;
|
|
229
|
+
list[j].pause();
|
|
230
|
+
list[j].currentTime = Math.max(0, t - offset) * 1000;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
var shown = 0;
|
|
235
|
+
function seek(t){ shown = t; applyCues(t); applyAmbient(t); }
|
|
236
|
+
window.__ld = { duration: duration, seek: seek };
|
|
237
|
+
window.addEventListener('beforeprint', function(){ seek(duration); });
|
|
238
|
+
|
|
239
|
+
if (window.__ldCapture) { seek(0); return; }
|
|
240
|
+
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
241
|
+
if (reduce) { seek(duration); return; }
|
|
242
|
+
// Live playback waits for the reader: a block whose cue comes up while it is still below the fold
|
|
243
|
+
// defers its own cues, in order, until it scrolls into view. Everything else keeps playing.
|
|
244
|
+
var gates = items.slice();
|
|
245
|
+
scenes.forEach(function(scene){ gates = gates.concat(scene.__ldMoves); });
|
|
246
|
+
gates.forEach(function(g){ g.root = g.el.closest('[data-cue]') || g.el; g.root.__ldDelay = 0; });
|
|
247
|
+
function shift(g){ return g.root.__ldDelay; }
|
|
248
|
+
function belowFold(el){ return el.getBoundingClientRect().top > window.innerHeight; }
|
|
249
|
+
var last = null, wall = 0, cueT = 0, cycle = 0, running = true, paused = false;
|
|
250
|
+
function frame(now){
|
|
251
|
+
if (paused) { running = false; return; }
|
|
252
|
+
var dt = last === null ? 0 : Math.min(.1, (now - last) / 1000);
|
|
253
|
+
last = now;
|
|
254
|
+
wall += dt;
|
|
255
|
+
var waiting = [], maxDelay = 0;
|
|
256
|
+
cueT += dt;
|
|
257
|
+
gates.forEach(function(g){
|
|
258
|
+
var local = cueT - g.root.__ldDelay;
|
|
259
|
+
if (local >= g.cue.at && local < g.cue.at + g.cue.dur && waiting.indexOf(g.root) < 0 && belowFold(g.root)) {
|
|
260
|
+
waiting.push(g.root);
|
|
261
|
+
g.root.__ldDelay = cueT - g.cue.at; // hold exactly at the cue start
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
gates.forEach(function(g){ maxDelay = Math.max(maxDelay, g.root.__ldDelay); });
|
|
265
|
+
if (data.loop && cueT >= duration + maxDelay) {
|
|
266
|
+
cueT -= duration + maxDelay;
|
|
267
|
+
cycle++;
|
|
268
|
+
gates.forEach(function(g){ g.root.__ldDelay = 0; });
|
|
269
|
+
}
|
|
270
|
+
applyCues(Math.min(cueT, duration + maxDelay), cycle > 0, shift);
|
|
271
|
+
applyAmbient(wall, true);
|
|
272
|
+
shown = cueT;
|
|
273
|
+
if (data.loop || cueT < duration + maxDelay || (window.__ldHooks || []).length) requestAnimationFrame(frame); else running = false;
|
|
274
|
+
}
|
|
275
|
+
function start(){ if (!running) { running = true; last = null; requestAnimationFrame(frame); } }
|
|
276
|
+
// Live controls for a host page: pause() hands the clock to seek(t), play() resumes from the last shown time.
|
|
277
|
+
window.__ld.time = function(){ return shown; };
|
|
278
|
+
window.__ld.pause = function(){ paused = true; };
|
|
279
|
+
window.__ld.play = function(){
|
|
280
|
+
if (!paused && running) return;
|
|
281
|
+
cueT = Math.min(shown, duration);
|
|
282
|
+
gates.forEach(function(g){ g.root.__ldDelay = 0; });
|
|
283
|
+
paused = false;
|
|
284
|
+
start();
|
|
285
|
+
};
|
|
286
|
+
window.__ld.replay = function(){
|
|
287
|
+
cueT = 0; cycle = 0; shown = 0;
|
|
288
|
+
gates.forEach(function(g){ g.root.__ldDelay = 0; });
|
|
289
|
+
paused = false;
|
|
290
|
+
start();
|
|
291
|
+
};
|
|
292
|
+
seek(0);
|
|
293
|
+
requestAnimationFrame(frame);
|
|
294
|
+
})();
|
|
295
|
+
`;
|
|
296
|
+
|
|
297
|
+
/** Markup appended before </body> when a model has a timeline or renders as a video stage. */
|
|
298
|
+
export function timelineAssets(model) {
|
|
299
|
+
const json = JSON.stringify(payload(model)).replace(/</g, '\\u003c');
|
|
300
|
+
return `<style>${CSS}</style>
|
|
301
|
+
<script type="application/json" id="ld-timeline">${json}</script>
|
|
302
|
+
<script>${RUNTIME}</script>
|
|
303
|
+
`;
|
|
304
|
+
}
|