htmlcraft 0.0.0-stage → 0.1.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 +77 -2
- package/SECURITY.md +32 -0
- package/THIRD-PARTY-LICENSES.md +1401 -0
- package/dist/cli.js +108625 -0
- package/dist/editor/assets/index-BklY9zS4.css +1 -0
- package/dist/editor/assets/index-CcXgWYEN.js +737 -0
- package/dist/editor/index.html +57 -0
- package/dist/mcp.js +99001 -0
- package/dist/skill/SKILL.md +254 -0
- package/dist/skill/examples/basic.html +742 -0
- package/dist/skill/examples/notes-transitions.html +736 -0
- package/dist/skill/examples/table-chart.html +754 -0
- package/dist/skill/references/authoring.md +160 -0
- package/dist/skill/references/contract.md +177 -0
- package/dist/skill/references/mcp-tools.md +397 -0
- package/dist/skill/references/troubleshooting.md +188 -0
- package/package.json +51 -4
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Basic deck</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: light; }
|
|
9
|
+
html, body { margin: 0; padding: 0; background: #0b0d12; }
|
|
10
|
+
.slide {
|
|
11
|
+
width: 1600px;
|
|
12
|
+
height: 900px;
|
|
13
|
+
position: relative;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
background: #f7f5ef;
|
|
16
|
+
color: #17181c;
|
|
17
|
+
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
|
18
|
+
}
|
|
19
|
+
@media screen {
|
|
20
|
+
[data-ppt-deck]:not([data-ppt-ready]) > [data-slide-id] ~ [data-slide-id] { display: none; }
|
|
21
|
+
}
|
|
22
|
+
@media print {
|
|
23
|
+
[data-ppt-deck] > [data-slide-id] { break-after: page; }
|
|
24
|
+
}
|
|
25
|
+
.kicker { margin: 0 0 18px; font-size: 22px; letter-spacing: .18em; text-transform: uppercase; color: #9a6b1f; }
|
|
26
|
+
.title { margin: 0; font-size: 96px; line-height: 1.05; }
|
|
27
|
+
.body { margin: 28px 0 0; font-size: 32px; line-height: 1.5; color: #4a4f59; }
|
|
28
|
+
.accent { width: 200px; height: 10px; background: #e8a33d; }
|
|
29
|
+
.list { margin: 0; padding-left: 40px; font-size: 34px; line-height: 1.7; }
|
|
30
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #9a6b1f; }
|
|
31
|
+
</style>
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<main data-ppt-deck>
|
|
35
|
+
<section class="slide" data-slide-id="s1" data-width="1600" data-height="900" data-ppt-transition="fade">
|
|
36
|
+
<div data-ppt-el="e1" style="position: absolute; left: 140px; top: 160px; width: 1200px;">
|
|
37
|
+
<p class="kicker" data-ppt-el="e1-kicker">htmlcraft · htmldeck/v1</p>
|
|
38
|
+
<h1 class="title" data-ppt-el="e1-title">A deck your agent can edit</h1>
|
|
39
|
+
<p class="body" data-ppt-el="e1-body">Every object is plain HTML with a stable id. Change text, styles, or structure, then validate before saving.</p>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="accent" data-ppt-el="e2" style="position: absolute; left: 140px; top: 620px;"></div>
|
|
42
|
+
<aside data-ppt-notes hidden>Open with the promise: the deck is ordinary HTML, so an agent and a person can edit the same file.</aside>
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<section class="slide" data-slide-id="s2" data-width="1600" data-height="900" data-ppt-transition="none">
|
|
46
|
+
<h2 data-ppt-el="e3" style="position: absolute; left: 140px; top: 130px; margin: 0; font-size: 64px;">How a deck is built</h2>
|
|
47
|
+
<ul class="list" data-ppt-el="e4" style="position: absolute; left: 140px; top: 280px; width: 1180px;">
|
|
48
|
+
<li>One <code><section></code> per slide with a unique <code>data-slide-id</code>.</li>
|
|
49
|
+
<li>Every top-level object carries <code>data-ppt-el</code>.</li>
|
|
50
|
+
<li>Speaker notes are a direct-child <code><aside data-ppt-notes hidden></code>.</li>
|
|
51
|
+
</ul>
|
|
52
|
+
<aside data-ppt-notes hidden>Keep object ids stable. The editor and MCP tools address objects by these ids.</aside>
|
|
53
|
+
</section>
|
|
54
|
+
|
|
55
|
+
<section class="slide" data-slide-id="s3" data-width="1600" data-height="900" data-ppt-transition="slide">
|
|
56
|
+
<h2 data-ppt-el="e5" style="position: absolute; left: 140px; top: 130px; margin: 0; font-size: 64px;">Next step</h2>
|
|
57
|
+
<p data-ppt-el="e6" style="position: absolute; left: 140px; top: 280px; width: 1180px; margin: 0; font-size: 32px; line-height: 1.5; color: #4a4f59;">Validate with <code>htmlcraft validate deck.html</code>, then open the editor or export a self-contained file.</p>
|
|
58
|
+
<aside data-ppt-notes hidden>Point people to the validate command before they share the deck.</aside>
|
|
59
|
+
</section>
|
|
60
|
+
</main>
|
|
61
|
+
<script data-ppt-runtime="1">
|
|
62
|
+
(() => {
|
|
63
|
+
var slides = Array.prototype.slice.call(document.querySelectorAll('[data-slide-id]'));
|
|
64
|
+
if (!slides.length) return;
|
|
65
|
+
var index = 0;
|
|
66
|
+
var runtimeStyle = 'data-ppt-runtime-style';
|
|
67
|
+
var originalDisplay = slides.map(function (slide) { return slide.style.display; });
|
|
68
|
+
var sizes = slides.map(function (slide) { return measureSlide(slide); });
|
|
69
|
+
var controls = null;
|
|
70
|
+
var pageIndicator = null;
|
|
71
|
+
var previousButton = null;
|
|
72
|
+
var nextButton = null;
|
|
73
|
+
var sessionId = createSessionId();
|
|
74
|
+
var presentation = null;
|
|
75
|
+
|
|
76
|
+
var RUNTIME_STRINGS = {
|
|
77
|
+
en: {
|
|
78
|
+
group: 'Slide navigation',
|
|
79
|
+
previous: 'Previous slide',
|
|
80
|
+
next: 'Next slide',
|
|
81
|
+
page: '{index} / {total}',
|
|
82
|
+
presenterTitle: 'Presenter view',
|
|
83
|
+
waiting: 'Waiting for the main window…',
|
|
84
|
+
notConnected: 'Not connected yet.',
|
|
85
|
+
notes: 'Notes',
|
|
86
|
+
noNotes: 'No notes for this slide.',
|
|
87
|
+
nextSlide: 'Next slide',
|
|
88
|
+
nextPreview: 'Next slide preview',
|
|
89
|
+
lastSlidePlaceholder: '(Last slide)',
|
|
90
|
+
connected: 'Connected',
|
|
91
|
+
disconnected: 'Disconnected',
|
|
92
|
+
fullscreenUnsupported: 'This browser does not support fullscreen. Press F11.',
|
|
93
|
+
fullscreenDenied: 'Fullscreen was blocked. Press F11.',
|
|
94
|
+
presenterBlocked: 'The browser blocked the presenter window. Allow popups and press P.',
|
|
95
|
+
presenterUnsupported: 'This browser does not support presenter view.'
|
|
96
|
+
},
|
|
97
|
+
'zh-Hant': {
|
|
98
|
+
group: '投影片導覽',
|
|
99
|
+
previous: '上一頁',
|
|
100
|
+
next: '下一頁',
|
|
101
|
+
page: '第 {index} / {total} 頁',
|
|
102
|
+
presenterTitle: '簡報者檢視',
|
|
103
|
+
waiting: '等待主視窗…',
|
|
104
|
+
notConnected: '尚未連線。',
|
|
105
|
+
notes: '備註',
|
|
106
|
+
noNotes: '此頁沒有備註。',
|
|
107
|
+
nextSlide: '下一頁',
|
|
108
|
+
nextPreview: '下一頁預覽',
|
|
109
|
+
lastSlidePlaceholder: '(最後一頁)',
|
|
110
|
+
connected: '已連線',
|
|
111
|
+
disconnected: '已中斷',
|
|
112
|
+
fullscreenUnsupported: '此瀏覽器不支援全螢幕;請按 F11 手動全螢幕。',
|
|
113
|
+
fullscreenDenied: '瀏覽器未允許全螢幕;請按 F11 手動全螢幕。',
|
|
114
|
+
presenterBlocked: '瀏覽器封鎖了簡報視窗。請允許此網站開啟彈出視窗,再按 P 重試。',
|
|
115
|
+
presenterUnsupported: '此瀏覽器不支援簡報者檢視。'
|
|
116
|
+
},
|
|
117
|
+
'zh-Hans': {
|
|
118
|
+
group: '幻灯片导航',
|
|
119
|
+
previous: '上一页',
|
|
120
|
+
next: '下一页',
|
|
121
|
+
page: '第 {index} / {total} 页',
|
|
122
|
+
presenterTitle: '演讲者视图',
|
|
123
|
+
waiting: '等待主窗口…',
|
|
124
|
+
notConnected: '尚未连接。',
|
|
125
|
+
notes: '备注',
|
|
126
|
+
noNotes: '此页没有备注。',
|
|
127
|
+
nextSlide: '下一页',
|
|
128
|
+
nextPreview: '下一页预览',
|
|
129
|
+
lastSlidePlaceholder: '(最后一页)',
|
|
130
|
+
connected: '已连接',
|
|
131
|
+
disconnected: '已断开',
|
|
132
|
+
fullscreenUnsupported: '此浏览器不支持全屏;请按 F11 手动全屏。',
|
|
133
|
+
fullscreenDenied: '浏览器未允许全屏;请按 F11 手动全屏。',
|
|
134
|
+
presenterBlocked: '浏览器阻止了演讲者窗口。请允许此网站打开弹出窗口,再按 P 重试。',
|
|
135
|
+
presenterUnsupported: '此浏览器不支持演讲者视图。'
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function runtimeLocale() {
|
|
140
|
+
var override = document.documentElement.getAttribute('data-ppt-locale');
|
|
141
|
+
var value = String(override || navigator.language || 'en').toLowerCase();
|
|
142
|
+
var subtags = value.split('-');
|
|
143
|
+
if (subtags[0] !== 'zh') return 'en';
|
|
144
|
+
if (subtags.indexOf('hant') !== -1) return 'zh-Hant';
|
|
145
|
+
if (subtags.indexOf('hans') !== -1) return 'zh-Hans';
|
|
146
|
+
if (value === 'zh-tw' || value === 'zh-hk' || value === 'zh-mo') {
|
|
147
|
+
return 'zh-Hant';
|
|
148
|
+
}
|
|
149
|
+
return 'zh-Hans';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var strings = RUNTIME_STRINGS[runtimeLocale()];
|
|
153
|
+
|
|
154
|
+
function pageLabel(pageIndex, total) {
|
|
155
|
+
return strings.page
|
|
156
|
+
.replace('{index}', String(pageIndex))
|
|
157
|
+
.replace('{total}', String(total));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
Array.prototype.forEach.call(document.querySelectorAll('[data-ppt-deck]'), function (deck) {
|
|
161
|
+
deck.setAttribute('data-ppt-ready', '1');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
document.documentElement.setAttribute('data-ppt-runtime-fit', '1');
|
|
165
|
+
|
|
166
|
+
function measureSlide(slide) {
|
|
167
|
+
return {
|
|
168
|
+
width: dimensionOf(slide, 'data-width', 'width', 1600),
|
|
169
|
+
height: dimensionOf(slide, 'data-height', 'height', 900)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function dimensionOf(slide, attribute, property, fallback) {
|
|
174
|
+
var value = Number(slide.getAttribute(attribute));
|
|
175
|
+
if (!(value > 0)) value = parseFloat(window.getComputedStyle(slide)[property]);
|
|
176
|
+
if (!(value > 0)) value = property === 'width' ? slide.offsetWidth : slide.offsetHeight;
|
|
177
|
+
return value > 0 ? value : fallback;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function layoutActiveSlide() {
|
|
181
|
+
var slide = slides[index];
|
|
182
|
+
if (!slide) return;
|
|
183
|
+
var size = sizes[index];
|
|
184
|
+
var viewportWidth = window.innerWidth || document.documentElement.clientWidth || size.width;
|
|
185
|
+
var viewportHeight = window.innerHeight || document.documentElement.clientHeight || size.height;
|
|
186
|
+
var scale = Math.min(viewportWidth / size.width, viewportHeight / size.height);
|
|
187
|
+
if (!(scale > 0) || !isFinite(scale)) scale = 1;
|
|
188
|
+
slide.style.setProperty('--ppt-fit-scale', String(scale));
|
|
189
|
+
slide.style.setProperty('--ppt-fit-left', (viewportWidth - size.width * scale) / 2 + 'px');
|
|
190
|
+
slide.style.setProperty('--ppt-fit-top', (viewportHeight - size.height * scale) / 2 + 'px');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildControls() {
|
|
194
|
+
if (slides.length < 2) return;
|
|
195
|
+
controls = document.createElement('div');
|
|
196
|
+
controls.setAttribute('data-ppt-runtime-controls', '1');
|
|
197
|
+
controls.setAttribute('data-testid', 'runtime-controls');
|
|
198
|
+
controls.setAttribute('role', 'group');
|
|
199
|
+
controls.setAttribute('aria-label', strings.group);
|
|
200
|
+
previousButton = document.createElement('button');
|
|
201
|
+
previousButton.type = 'button';
|
|
202
|
+
previousButton.setAttribute('data-testid', 'runtime-prev');
|
|
203
|
+
previousButton.setAttribute('aria-label', strings.previous);
|
|
204
|
+
previousButton.textContent = strings.previous;
|
|
205
|
+
pageIndicator = document.createElement('span');
|
|
206
|
+
pageIndicator.setAttribute('data-testid', 'runtime-page');
|
|
207
|
+
pageIndicator.setAttribute('aria-live', 'polite');
|
|
208
|
+
nextButton = document.createElement('button');
|
|
209
|
+
nextButton.type = 'button';
|
|
210
|
+
nextButton.setAttribute('data-testid', 'runtime-next');
|
|
211
|
+
nextButton.setAttribute('aria-label', strings.next);
|
|
212
|
+
nextButton.textContent = strings.next;
|
|
213
|
+
previousButton.addEventListener('click', function () { show(index - 1, true); });
|
|
214
|
+
nextButton.addEventListener('click', function () { show(index + 1, true); });
|
|
215
|
+
controls.appendChild(previousButton);
|
|
216
|
+
controls.appendChild(pageIndicator);
|
|
217
|
+
controls.appendChild(nextButton);
|
|
218
|
+
document.body.appendChild(controls);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function renderControls() {
|
|
222
|
+
if (!controls) return;
|
|
223
|
+
pageIndicator.textContent = pageLabel(index + 1, slides.length);
|
|
224
|
+
previousButton.disabled = index <= 0;
|
|
225
|
+
nextButton.disabled = index >= slides.length - 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function createSessionId() {
|
|
229
|
+
if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID();
|
|
230
|
+
var values = new Uint8Array(16);
|
|
231
|
+
if (window.crypto && window.crypto.getRandomValues) window.crypto.getRandomValues(values);
|
|
232
|
+
else for (var i = 0; i < 16; i += 1) values[i] = Math.floor(Math.random() * 256);
|
|
233
|
+
var text = '';
|
|
234
|
+
for (var j = 0; j < values.length; j += 1) text += ('0' + values[j].toString(16)).slice(-2);
|
|
235
|
+
return text;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isEditing(target) {
|
|
239
|
+
var element = target;
|
|
240
|
+
while (element) {
|
|
241
|
+
var tag = element.tagName;
|
|
242
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || element.isContentEditable) return true;
|
|
243
|
+
if (element === document.body) return false;
|
|
244
|
+
element = element.parentElement;
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isInteractive(target) {
|
|
250
|
+
var element = target;
|
|
251
|
+
while (element && element !== document.body) {
|
|
252
|
+
var tag = element.tagName;
|
|
253
|
+
if (tag === 'A' || tag === 'a' || tag === 'BUTTON' || tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA' || tag === 'LABEL' || tag === 'SUMMARY' || tag === 'DETAILS') return true;
|
|
254
|
+
if ((tag === 'VIDEO' || tag === 'AUDIO') && element.hasAttribute('controls')) return true;
|
|
255
|
+
var role = element.getAttribute('role');
|
|
256
|
+
if (role === 'button' || role === 'link') return true;
|
|
257
|
+
if (element.isContentEditable) return true;
|
|
258
|
+
element = element.parentElement;
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function isControl(target) {
|
|
264
|
+
var element = target;
|
|
265
|
+
while (element && element !== document.body) {
|
|
266
|
+
if (element.hasAttribute && element.hasAttribute('data-ppt-runtime-controls')) return true;
|
|
267
|
+
element = element.parentElement;
|
|
268
|
+
}
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function transitionOf(slide) {
|
|
273
|
+
var value = slide.getAttribute('data-ppt-transition');
|
|
274
|
+
return value === 'fade' || value === 'slide' ? value : 'none';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function animateSlide(slide) {
|
|
278
|
+
if (!window.matchMedia || !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
279
|
+
var kind = transitionOf(slide);
|
|
280
|
+
if (kind !== 'none') {
|
|
281
|
+
slide.classList.remove('htmlppt-runtime-fade', 'htmlppt-runtime-slide');
|
|
282
|
+
void slide.offsetWidth;
|
|
283
|
+
slide.classList.add('htmlppt-runtime-' + kind);
|
|
284
|
+
window.setTimeout(function () {
|
|
285
|
+
slide.classList.remove('htmlppt-runtime-' + kind);
|
|
286
|
+
}, 500);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function postState() {
|
|
292
|
+
if (!presentation) return;
|
|
293
|
+
presentation.channel.postMessage({
|
|
294
|
+
sessionId: presentation.sessionId,
|
|
295
|
+
type: 'state',
|
|
296
|
+
index: index,
|
|
297
|
+
total: slides.length
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function show(next, animate) {
|
|
302
|
+
var clamped = Math.max(0, Math.min(slides.length - 1, next));
|
|
303
|
+
var changed = clamped !== index;
|
|
304
|
+
index = clamped;
|
|
305
|
+
slides.forEach(function (slide, i) {
|
|
306
|
+
slide.style.display = i === index ? originalDisplay[i] : 'none';
|
|
307
|
+
});
|
|
308
|
+
layoutActiveSlide();
|
|
309
|
+
renderControls();
|
|
310
|
+
if (changed && animate !== false) animateSlide(slides[index]);
|
|
311
|
+
history.replaceState(null, '', '#/' + (index + 1));
|
|
312
|
+
postState();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function notice(message) {
|
|
316
|
+
var previous = document.querySelector('[data-ppt-runtime-notice]');
|
|
317
|
+
if (previous) previous.remove();
|
|
318
|
+
var element = document.createElement('div');
|
|
319
|
+
element.setAttribute('data-ppt-runtime-notice', '1');
|
|
320
|
+
element.setAttribute('data-testid', 'runtime-notice');
|
|
321
|
+
element.setAttribute('role', 'status');
|
|
322
|
+
element.textContent = message;
|
|
323
|
+
element.style.cssText = 'position:fixed;left:16px;top:16px;z-index:2147483647;max-width:380px;padding:8px 12px;border:1px solid rgba(245,158,11,.5);background:rgba(69,26,3,.92);color:#fef3c7;font:12px/1.5 system-ui,sans-serif;border-radius:6px';
|
|
324
|
+
document.body.appendChild(element);
|
|
325
|
+
window.setTimeout(function () {
|
|
326
|
+
element.remove();
|
|
327
|
+
}, 4000);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function toggleFullscreen() {
|
|
331
|
+
if (document.fullscreenElement) {
|
|
332
|
+
var exit = document.exitFullscreen();
|
|
333
|
+
if (exit && exit.catch) exit.catch(function () {});
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
var request = document.documentElement.requestFullscreen;
|
|
337
|
+
if (!request) {
|
|
338
|
+
notice(strings.fullscreenUnsupported);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
var result;
|
|
342
|
+
try {
|
|
343
|
+
result = request.call(document.documentElement);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
notice(strings.fullscreenDenied);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (result && result.catch) {
|
|
349
|
+
result.catch(function () {
|
|
350
|
+
notice(strings.fullscreenDenied);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
var PRESENTER_CSS = 'html,body{margin:0;background:#0b0d12;color:#e4e4e7;font:14px/1.5 system-ui,sans-serif}' +
|
|
356
|
+
'body{padding:20px;display:flex;flex-direction:column;gap:16px;min-height:100vh;box-sizing:border-box}' +
|
|
357
|
+
'header{display:flex;justify-content:space-between;align-items:flex-end;border-bottom:1px solid #27272a;padding-bottom:12px}' +
|
|
358
|
+
'h1{font-size:14px;margin:0;letter-spacing:.05em}' +
|
|
359
|
+
'h2{font-size:11px;text-transform:uppercase;letter-spacing:.14em;color:#71717a;margin:0 0 6px}' +
|
|
360
|
+
'.presenter-meta{display:flex;gap:16px;font-size:12px;color:#a1a1aa;margin:4px 0 0}' +
|
|
361
|
+
'[data-testid="presenter-status"]{color:#fcd34d}' +
|
|
362
|
+
'[data-testid="presenter-notes"]{min-height:96px;white-space:pre-wrap;border:1px solid #27272a;background:#18181b;padding:12px;border-radius:6px;margin:0}' +
|
|
363
|
+
'[data-testid="presenter-next-preview"]{display:flex;gap:16px;align-items:center;border:1px solid #27272a;background:#18181b;padding:12px;border-radius:6px;min-height:204px;box-sizing:border-box}' +
|
|
364
|
+
'.preview-holder{width:320px;height:180px;overflow:hidden;border-radius:4px;background:#fff;flex:none}' +
|
|
365
|
+
'.preview-holder iframe{width:1600px;height:900px;border:0;transform:scale(.2);transform-origin:top left;pointer-events:none;background:#fff}' +
|
|
366
|
+
'.presenter-actions{display:flex;gap:8px}' +
|
|
367
|
+
'.presenter-actions button{border:1px solid #3f3f46;background:transparent;color:#e4e4e7;border-radius:4px;padding:6px 12px;font-size:12px;cursor:pointer}' +
|
|
368
|
+
'.presenter-actions button:disabled{opacity:.4;cursor:default}';
|
|
369
|
+
|
|
370
|
+
function element(doc, tag, attributes, text) {
|
|
371
|
+
var node = doc.createElement(tag);
|
|
372
|
+
if (attributes) {
|
|
373
|
+
Object.keys(attributes).forEach(function (name) {
|
|
374
|
+
node.setAttribute(name, attributes[name]);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (text !== undefined) node.textContent = text;
|
|
378
|
+
return node;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function buildPresenterWindow(win) {
|
|
382
|
+
var doc = win.document;
|
|
383
|
+
doc.documentElement.lang = runtimeLocale();
|
|
384
|
+
doc.title = strings.presenterTitle;
|
|
385
|
+
while (doc.body.firstChild) doc.body.removeChild(doc.body.firstChild);
|
|
386
|
+
doc.body.setAttribute('data-ppt-strings', JSON.stringify(strings));
|
|
387
|
+
doc.head.appendChild(element(doc, 'style', null, PRESENTER_CSS));
|
|
388
|
+
var root = element(doc, 'div', { 'data-testid': 'presenter-view' });
|
|
389
|
+
var header = element(doc, 'header');
|
|
390
|
+
var titleBlock = element(doc, 'div');
|
|
391
|
+
titleBlock.appendChild(element(doc, 'h1', null, strings.presenterTitle));
|
|
392
|
+
var pageMeta = element(doc, 'p', { class: 'presenter-meta' });
|
|
393
|
+
pageMeta.appendChild(element(doc, 'span', { 'data-testid': 'presenter-page' }, pageLabel(0, 0)));
|
|
394
|
+
titleBlock.appendChild(pageMeta);
|
|
395
|
+
header.appendChild(titleBlock);
|
|
396
|
+
var meta = element(doc, 'div', { class: 'presenter-meta' });
|
|
397
|
+
meta.appendChild(element(doc, 'span', { 'data-testid': 'presenter-status' }, strings.waiting));
|
|
398
|
+
meta.appendChild(element(doc, 'span', { 'data-testid': 'presenter-timer' }, '00:00'));
|
|
399
|
+
header.appendChild(meta);
|
|
400
|
+
|
|
401
|
+
var notesSection = element(doc, 'section');
|
|
402
|
+
notesSection.appendChild(element(doc, 'h2', null, strings.notes));
|
|
403
|
+
notesSection.appendChild(element(doc, 'p', { 'data-testid': 'presenter-notes' }, strings.notConnected));
|
|
404
|
+
|
|
405
|
+
var nextSection = element(doc, 'section');
|
|
406
|
+
nextSection.appendChild(element(doc, 'h2', null, strings.nextSlide));
|
|
407
|
+
var nextPreview = element(doc, 'div', { 'data-testid': 'presenter-next-preview' });
|
|
408
|
+
nextPreview.appendChild(element(doc, 'span', null, strings.lastSlidePlaceholder));
|
|
409
|
+
nextSection.appendChild(nextPreview);
|
|
410
|
+
|
|
411
|
+
var actions = element(doc, 'div', { class: 'presenter-actions' });
|
|
412
|
+
var previous = element(doc, 'button', { type: 'button', 'data-testid': 'presenter-prev' }, strings.previous);
|
|
413
|
+
previous.disabled = true;
|
|
414
|
+
var next = element(doc, 'button', { type: 'button', 'data-testid': 'presenter-next' }, strings.next);
|
|
415
|
+
next.disabled = true;
|
|
416
|
+
actions.appendChild(previous);
|
|
417
|
+
actions.appendChild(next);
|
|
418
|
+
|
|
419
|
+
root.appendChild(header);
|
|
420
|
+
root.appendChild(notesSection);
|
|
421
|
+
root.appendChild(nextSection);
|
|
422
|
+
root.appendChild(actions);
|
|
423
|
+
doc.body.appendChild(root);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function PRESENTER_CLIENT() {
|
|
427
|
+
var strings = {};
|
|
428
|
+
try {
|
|
429
|
+
var parsed = JSON.parse(document.body.getAttribute('data-ppt-strings') || '{}');
|
|
430
|
+
if (parsed && typeof parsed === 'object') strings = parsed;
|
|
431
|
+
} catch (error) {
|
|
432
|
+
strings = {};
|
|
433
|
+
}
|
|
434
|
+
function label(key, fallback) {
|
|
435
|
+
return typeof strings[key] === 'string' && strings[key] ? strings[key] : fallback;
|
|
436
|
+
}
|
|
437
|
+
function pageText(pageIndex, total) {
|
|
438
|
+
var format = label('page', '');
|
|
439
|
+
if (!format) return String(pageIndex) + ' / ' + String(total);
|
|
440
|
+
return format
|
|
441
|
+
.replace('{index}', String(pageIndex))
|
|
442
|
+
.replace('{total}', String(total));
|
|
443
|
+
}
|
|
444
|
+
var session = document.body.getAttribute('data-session-id');
|
|
445
|
+
if (!session) return;
|
|
446
|
+
var status = document.querySelector('[data-testid="presenter-status"]');
|
|
447
|
+
var timer = document.querySelector('[data-testid="presenter-timer"]');
|
|
448
|
+
var page = document.querySelector('[data-testid="presenter-page"]');
|
|
449
|
+
var notes = document.querySelector('[data-testid="presenter-notes"]');
|
|
450
|
+
var preview = document.querySelector('[data-testid="presenter-next-preview"]');
|
|
451
|
+
var prevButton = document.querySelector('[data-testid="presenter-prev"]');
|
|
452
|
+
var nextButton = document.querySelector('[data-testid="presenter-next"]');
|
|
453
|
+
var slides = [];
|
|
454
|
+
var headHtml = '';
|
|
455
|
+
var bodyFallback = false;
|
|
456
|
+
var previewKey = '';
|
|
457
|
+
var index = 0;
|
|
458
|
+
var total = 0;
|
|
459
|
+
var lastState = 0;
|
|
460
|
+
var started = Date.now();
|
|
461
|
+
var channel = new BroadcastChannel('htmlppt-presenter-' + session);
|
|
462
|
+
|
|
463
|
+
function setStatus(text) {
|
|
464
|
+
if (status) status.textContent = text;
|
|
465
|
+
}
|
|
466
|
+
function request() {
|
|
467
|
+
channel.postMessage({ sessionId: session, type: 'request' });
|
|
468
|
+
}
|
|
469
|
+
function notesOf(slide) {
|
|
470
|
+
if (!slide) return '';
|
|
471
|
+
var aside = slide.querySelector('[data-ppt-notes]');
|
|
472
|
+
return aside ? (aside.textContent || '').trim() : '';
|
|
473
|
+
}
|
|
474
|
+
function titleOf(slide) {
|
|
475
|
+
if (!slide) return '';
|
|
476
|
+
var heading = slide.querySelector('h1, h2, h3, [data-ppt-title]');
|
|
477
|
+
return heading ? (heading.textContent || '').replace(/\s+/g, ' ').trim() : '';
|
|
478
|
+
}
|
|
479
|
+
function renderPreview() {
|
|
480
|
+
if (!preview) return;
|
|
481
|
+
var next = slides[index + 1];
|
|
482
|
+
var key = index + '|' + bodyFallback + '|' + headHtml + '|' + (next ? next.outerHTML : '');
|
|
483
|
+
if (key === previewKey) return;
|
|
484
|
+
previewKey = key;
|
|
485
|
+
while (preview.firstChild) preview.removeChild(preview.firstChild);
|
|
486
|
+
if (!next) {
|
|
487
|
+
preview.appendChild(document.createTextNode(label('lastSlidePlaceholder', '(Last slide)')));
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
var holder = document.createElement('div');
|
|
491
|
+
holder.className = 'preview-holder';
|
|
492
|
+
var width = Number(next.getAttribute('data-width')) || 1600;
|
|
493
|
+
var height = Number(next.getAttribute('data-height')) || 900;
|
|
494
|
+
var scale = 320 / width;
|
|
495
|
+
holder.style.height = Math.round(height * scale) + 'px';
|
|
496
|
+
var frame = document.createElement('iframe');
|
|
497
|
+
frame.setAttribute('sandbox', '');
|
|
498
|
+
frame.setAttribute('title', label('nextPreview', 'Next slide preview'));
|
|
499
|
+
frame.style.width = width + 'px';
|
|
500
|
+
frame.style.height = height + 'px';
|
|
501
|
+
frame.style.transform = 'scale(' + scale + ')';
|
|
502
|
+
frame.srcdoc = bodyFallback
|
|
503
|
+
? '<!doctype html><html><head>' + headHtml + '</head><body>' + next.innerHTML + '</body></html>'
|
|
504
|
+
: '<!doctype html><html><head>' + headHtml + '</head><body><main data-ppt-deck>' + next.outerHTML + '</main></body></html>';
|
|
505
|
+
holder.appendChild(frame);
|
|
506
|
+
var caption = document.createElement('span');
|
|
507
|
+
caption.textContent = (index + 2) + '. ' + titleOf(next);
|
|
508
|
+
preview.appendChild(holder);
|
|
509
|
+
preview.appendChild(caption);
|
|
510
|
+
}
|
|
511
|
+
function render() {
|
|
512
|
+
if (notes) notes.textContent = notesOf(slides[index]) || label('noNotes', 'No notes for this slide.');
|
|
513
|
+
if (page) page.textContent = pageText(total === 0 ? 0 : index + 1, total);
|
|
514
|
+
renderPreview();
|
|
515
|
+
if (prevButton) prevButton.disabled = index <= 0;
|
|
516
|
+
if (nextButton) nextButton.disabled = total === 0 || index >= total - 1;
|
|
517
|
+
}
|
|
518
|
+
channel.onmessage = function (event) {
|
|
519
|
+
var message = event.data;
|
|
520
|
+
if (!message || message.sessionId !== session) return;
|
|
521
|
+
if (message.type === 'snapshot') {
|
|
522
|
+
var parsed = new DOMParser().parseFromString(message.html, 'text/html');
|
|
523
|
+
var found = parsed.querySelectorAll('[data-slide-id]');
|
|
524
|
+
slides = Array.prototype.slice.call(found.length ? found : [parsed.body]);
|
|
525
|
+
bodyFallback = found.length === 0;
|
|
526
|
+
headHtml = parsed.head ? parsed.head.innerHTML : '';
|
|
527
|
+
index = message.index;
|
|
528
|
+
total = message.total;
|
|
529
|
+
lastState = Date.now();
|
|
530
|
+
setStatus(label('connected', 'Connected'));
|
|
531
|
+
render();
|
|
532
|
+
} else if (message.type === 'state') {
|
|
533
|
+
index = message.index;
|
|
534
|
+
total = message.total;
|
|
535
|
+
lastState = Date.now();
|
|
536
|
+
setStatus(label('connected', 'Connected'));
|
|
537
|
+
render();
|
|
538
|
+
} else if (message.type === 'bye') {
|
|
539
|
+
setStatus(label('disconnected', 'Disconnected'));
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
function sendSlide(next) {
|
|
543
|
+
if (next < 0 || next >= total) return;
|
|
544
|
+
channel.postMessage({ sessionId: session, type: 'slide', index: next });
|
|
545
|
+
}
|
|
546
|
+
if (prevButton) prevButton.onclick = function () { sendSlide(index - 1); };
|
|
547
|
+
if (nextButton) nextButton.onclick = function () { sendSlide(index + 1); };
|
|
548
|
+
var poll = window.setInterval(request, 2000);
|
|
549
|
+
var clock = window.setInterval(function () {
|
|
550
|
+
var elapsed = Math.floor((Date.now() - started) / 1000);
|
|
551
|
+
var minutes = Math.floor(elapsed / 60);
|
|
552
|
+
var seconds = elapsed % 60;
|
|
553
|
+
if (timer) timer.textContent = ('0' + minutes).slice(-2) + ':' + ('0' + seconds).slice(-2);
|
|
554
|
+
if (lastState !== 0 && Date.now() - lastState > 6000) setStatus(label('disconnected', 'Disconnected'));
|
|
555
|
+
}, 1000);
|
|
556
|
+
window.addEventListener('pagehide', function () {
|
|
557
|
+
try {
|
|
558
|
+
channel.postMessage({ sessionId: session, type: 'bye' });
|
|
559
|
+
} catch (error) {}
|
|
560
|
+
channel.close();
|
|
561
|
+
window.clearInterval(poll);
|
|
562
|
+
window.clearInterval(clock);
|
|
563
|
+
});
|
|
564
|
+
request();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function snapshotHtml() {
|
|
568
|
+
var clone = document.documentElement.cloneNode(true);
|
|
569
|
+
clone.removeAttribute('data-ppt-runtime-fit');
|
|
570
|
+
var copied = clone.querySelectorAll('[data-slide-id]');
|
|
571
|
+
for (var i = 0; i < copied.length && i < originalDisplay.length; i += 1) {
|
|
572
|
+
copied[i].style.display = originalDisplay[i];
|
|
573
|
+
}
|
|
574
|
+
return '<!doctype html>' + clone.outerHTML;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function sendSnapshot() {
|
|
578
|
+
if (!presentation) return;
|
|
579
|
+
presentation.channel.postMessage({
|
|
580
|
+
sessionId: presentation.sessionId,
|
|
581
|
+
type: 'snapshot',
|
|
582
|
+
html: snapshotHtml(),
|
|
583
|
+
index: index,
|
|
584
|
+
total: slides.length
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function closePresenter(notify) {
|
|
589
|
+
if (!presentation) return;
|
|
590
|
+
var current = presentation;
|
|
591
|
+
presentation = null;
|
|
592
|
+
window.clearInterval(current.closedTimer);
|
|
593
|
+
try {
|
|
594
|
+
if (notify) current.channel.postMessage({ sessionId: current.sessionId, type: 'bye' });
|
|
595
|
+
} catch (error) {}
|
|
596
|
+
try {
|
|
597
|
+
current.channel.close();
|
|
598
|
+
} catch (error) {}
|
|
599
|
+
if (notify && current.win && !current.win.closed) {
|
|
600
|
+
try {
|
|
601
|
+
current.win.close();
|
|
602
|
+
} catch (error) {}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function openPresenter() {
|
|
607
|
+
if (presentation && presentation.win && !presentation.win.closed) {
|
|
608
|
+
presentation.win.focus();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (presentation) closePresenter(false);
|
|
612
|
+
var win = window.open('', 'htmlppt-presenter-' + sessionId, 'popup=yes,width=960,height=720');
|
|
613
|
+
if (!win) {
|
|
614
|
+
notice(strings.presenterBlocked);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
var channel;
|
|
618
|
+
try {
|
|
619
|
+
channel = new BroadcastChannel('htmlppt-presenter-' + sessionId);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
win.close();
|
|
622
|
+
notice(strings.presenterUnsupported);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
presentation = { sessionId: sessionId, channel: channel, win: win, closedTimer: null };
|
|
626
|
+
buildPresenterWindow(win);
|
|
627
|
+
win.document.body.setAttribute('data-session-id', sessionId);
|
|
628
|
+
var client = win.document.createElement('script');
|
|
629
|
+
client.textContent = '(' + PRESENTER_CLIENT.toString() + ')();';
|
|
630
|
+
win.document.body.appendChild(client);
|
|
631
|
+
var sentSnapshot = false;
|
|
632
|
+
channel.onmessage = function (event) {
|
|
633
|
+
var message = event.data;
|
|
634
|
+
if (!message || message.sessionId !== sessionId) return;
|
|
635
|
+
if (message.type === 'request') {
|
|
636
|
+
if (!sentSnapshot) {
|
|
637
|
+
sentSnapshot = true;
|
|
638
|
+
sendSnapshot();
|
|
639
|
+
}
|
|
640
|
+
postState();
|
|
641
|
+
} else if (message.type === 'slide') {
|
|
642
|
+
var requested = message.index;
|
|
643
|
+
if (typeof requested === 'number' && Math.floor(requested) === requested && requested >= 0 && requested < slides.length) {
|
|
644
|
+
show(requested, true);
|
|
645
|
+
}
|
|
646
|
+
} else if (message.type === 'bye') {
|
|
647
|
+
closePresenter(true);
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
presentation.closedTimer = window.setInterval(function () {
|
|
651
|
+
if (win.closed) closePresenter(false);
|
|
652
|
+
}, 1000);
|
|
653
|
+
sentSnapshot = true;
|
|
654
|
+
sendSnapshot();
|
|
655
|
+
postState();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
document.addEventListener('keydown', function (event) {
|
|
659
|
+
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || isEditing(event.target)) return;
|
|
660
|
+
if (event.key === ' ' && isInteractive(event.target)) return;
|
|
661
|
+
if (event.key === 'ArrowRight' || event.key === ' ' || event.key === 'PageDown') {
|
|
662
|
+
event.preventDefault();
|
|
663
|
+
show(index + 1, true);
|
|
664
|
+
} else if (event.key === 'ArrowLeft' || event.key === 'PageUp') {
|
|
665
|
+
event.preventDefault();
|
|
666
|
+
show(index - 1, true);
|
|
667
|
+
} else if (event.key === 'Home') {
|
|
668
|
+
event.preventDefault();
|
|
669
|
+
show(0, true);
|
|
670
|
+
} else if (event.key === 'End') {
|
|
671
|
+
event.preventDefault();
|
|
672
|
+
show(slides.length - 1, true);
|
|
673
|
+
} else if (event.key === 'f' || event.key === 'F') {
|
|
674
|
+
event.preventDefault();
|
|
675
|
+
toggleFullscreen();
|
|
676
|
+
} else if (event.key === 'p' || event.key === 'P') {
|
|
677
|
+
event.preventDefault();
|
|
678
|
+
openPresenter();
|
|
679
|
+
} else if (event.key === 'Escape' && document.fullscreenElement) {
|
|
680
|
+
var exit = document.exitFullscreen();
|
|
681
|
+
if (exit && exit.catch) exit.catch(function () {});
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
document.addEventListener('click', function (event) {
|
|
686
|
+
if (event.defaultPrevented || isInteractive(event.target) || isControl(event.target)) return;
|
|
687
|
+
show(index + 1, true);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
window.addEventListener('resize', layoutActiveSlide);
|
|
691
|
+
|
|
692
|
+
window.addEventListener('hashchange', function () {
|
|
693
|
+
var match = /#\/(\d+)/.exec(location.hash);
|
|
694
|
+
if (match) show(Number(match[1]) - 1, true);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
if (!document.querySelector('[' + runtimeStyle + ']')) {
|
|
698
|
+
var style = document.createElement('style');
|
|
699
|
+
style.setAttribute(runtimeStyle, '1');
|
|
700
|
+
style.textContent = '@media print{[data-slide-id]{break-after:page;page-break-after:always}}' +
|
|
701
|
+
'[data-ppt-notes]{display:none!important}' +
|
|
702
|
+
'@media screen{' +
|
|
703
|
+
'html[data-ppt-runtime-fit="1"],html[data-ppt-runtime-fit="1"] body{overflow:hidden}' +
|
|
704
|
+
'html[data-ppt-runtime-fit="1"] [data-slide-id]{position:fixed!important;left:var(--ppt-fit-left,0);top:var(--ppt-fit-top,0);margin:0!important;transform:scale(var(--ppt-fit-scale,1));transform-origin:top left}' +
|
|
705
|
+
'}' +
|
|
706
|
+
'[data-ppt-runtime-controls]{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);z-index:2147483646;display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:999px;border:1px solid rgba(255,255,255,.22);background:rgba(12,14,20,.72);color:#f4f4f5;font:13px/1.2 system-ui,sans-serif;backdrop-filter:blur(8px)}' +
|
|
707
|
+
'[data-ppt-runtime-controls] button{border:1px solid rgba(255,255,255,.28);background:transparent;color:inherit;font:inherit;padding:5px 12px;border-radius:999px;cursor:pointer}' +
|
|
708
|
+
'[data-ppt-runtime-controls] button:hover:not(:disabled){border-color:rgba(255,255,255,.6);background:rgba(255,255,255,.1)}' +
|
|
709
|
+
'[data-ppt-runtime-controls] button:disabled{opacity:.35;cursor:default}' +
|
|
710
|
+
'[data-ppt-runtime-controls] [data-testid="runtime-page"]{min-width:48px;text-align:center;font-variant-numeric:tabular-nums;color:rgba(244,244,245,.85)}' +
|
|
711
|
+
'@media print{[data-ppt-runtime-controls]{display:none!important}}' +
|
|
712
|
+
'@keyframes htmlppt-runtime-fade{from{opacity:0}to{opacity:1}}' +
|
|
713
|
+
'@keyframes htmlppt-runtime-slide{from{opacity:.4;translate:6% 0}to{opacity:1;translate:0 0}}' +
|
|
714
|
+
'.htmlppt-runtime-fade{animation:htmlppt-runtime-fade 450ms ease-out}' +
|
|
715
|
+
'.htmlppt-runtime-slide{animation:htmlppt-runtime-slide 450ms ease-out}' +
|
|
716
|
+
'@media (prefers-reduced-motion: reduce){.htmlppt-runtime-fade,.htmlppt-runtime-slide{animation:none}}';
|
|
717
|
+
document.head.appendChild(style);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
var printState = null;
|
|
721
|
+
window.addEventListener('beforeprint', function () {
|
|
722
|
+
if (printState) return;
|
|
723
|
+
printState = slides.map(function (slide) { return slide.style.display; });
|
|
724
|
+
slides.forEach(function (slide, i) { slide.style.display = originalDisplay[i]; });
|
|
725
|
+
});
|
|
726
|
+
window.addEventListener('afterprint', function () {
|
|
727
|
+
printState = null;
|
|
728
|
+
show(index, false);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
window.addEventListener('pagehide', function () {
|
|
732
|
+
closePresenter(true);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
buildControls();
|
|
736
|
+
|
|
737
|
+
var initial = /#\/(\d+)/.exec(location.hash);
|
|
738
|
+
show(initial ? Number(initial[1]) - 1 : 0, false);
|
|
739
|
+
})();
|
|
740
|
+
</script>
|
|
741
|
+
</body>
|
|
742
|
+
</html>
|