@dustfeather/deckrun 2.0.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 +1073 -0
- package/THIRD-PARTY-NOTICES.md +38 -0
- package/dist/editor-content.js +485 -0
- package/dist/editor.js +3916 -0
- package/dist/fragments.js +71 -0
- package/dist/generate.js +3488 -0
- package/dist/highlights.js +833 -0
- package/dist/index.js +1020 -0
- package/dist/lint.js +330 -0
- package/dist/parser.js +221 -0
- package/dist/pdf.js +200 -0
- package/dist/presentation-options.js +289 -0
- package/dist/preview.js +400 -0
- package/dist/rich-content.js +195 -0
- package/dist/safe-fetch.js +173 -0
- package/dist/sanitize.js +102 -0
- package/dist/themes.js +1041 -0
- package/dist/titles.js +30 -0
- package/package.json +64 -0
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session highlights: select text anywhere deckrun renders a document, paint
|
|
3
|
+
* it in the theme's highlighter colour, and hang a comment off it.
|
|
4
|
+
*
|
|
5
|
+
* Nothing is written to disk and nothing leaves the browser. The store lives
|
|
6
|
+
* in `sessionStorage`, so highlights survive a reload and travel to the tab
|
|
7
|
+
* `present` opens, and vanish the moment that browser session ends. Tabs stay
|
|
8
|
+
* in step over a BroadcastChannel, which is how a highlight made in the
|
|
9
|
+
* editor's preview shows up in the deck already on the projector.
|
|
10
|
+
*
|
|
11
|
+
* The runtime is mounted by whoever owns the page. One mount drives one
|
|
12
|
+
* document — which may be an iframe's — and renders its own chrome (the
|
|
13
|
+
* selection bar, the comment card, the first-use warning) into the *host*
|
|
14
|
+
* document, so a preview scaled down to 40% still gets a readable popup.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Painted into whichever document holds the text being highlighted.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately not a theme colour. A highlight has to be findable at a glance
|
|
20
|
+
* on any of the fourteen palettes, light or dark, which a tint of the accent
|
|
21
|
+
* never is — it reads as part of the design. So it is a real highlighter: an
|
|
22
|
+
* opaque pen yellow carrying its own dark ink, the same on every theme.
|
|
23
|
+
*/
|
|
24
|
+
const MARK_CSS = `mark.dr-hl {
|
|
25
|
+
background: #ffe75e;
|
|
26
|
+
border-radius: 2px;
|
|
27
|
+
padding: 0 0.06em;
|
|
28
|
+
box-shadow: inset 0 -0.14em 0 rgba(173, 129, 0, 0.55);
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
transition: background 0.12s ease;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Body text, links, and every syntax-highlighting class set a colour of their
|
|
34
|
+
own, and most of them are unreadable on a solid pen stroke. Inside a mark
|
|
35
|
+
the ink wins, whatever the text was wearing before. */
|
|
36
|
+
mark.dr-hl, mark.dr-hl * { color: #15161a !important; }
|
|
37
|
+
|
|
38
|
+
mark.dr-hl:hover { background: #ffdb1f; }
|
|
39
|
+
|
|
40
|
+
/* A highlight carrying a comment is a different pen, not a different shade of
|
|
41
|
+
the same one: told apart across a room, not by hovering. */
|
|
42
|
+
mark.dr-hl--note {
|
|
43
|
+
background: #7ae6ff;
|
|
44
|
+
box-shadow: inset 0 -0.14em 0 rgba(0, 119, 156, 0.5);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
mark.dr-hl--note:hover { background: #3ed7ff; }
|
|
48
|
+
|
|
49
|
+
/* Only the closing fragment of a multi-element highlight carries the dot. */
|
|
50
|
+
mark.dr-hl--note[data-dr-end]::after {
|
|
51
|
+
content: "";
|
|
52
|
+
display: inline-block;
|
|
53
|
+
width: 0.4em;
|
|
54
|
+
height: 0.4em;
|
|
55
|
+
margin-left: 0.2em;
|
|
56
|
+
border-radius: 50%;
|
|
57
|
+
vertical-align: super;
|
|
58
|
+
background: #0a5f75;
|
|
59
|
+
}`;
|
|
60
|
+
/** Painted into the host document, which owns the popups. */
|
|
61
|
+
const UI_CSS = `.dr-bar, .dr-note, .dr-warn {
|
|
62
|
+
position: fixed;
|
|
63
|
+
z-index: 2147483000;
|
|
64
|
+
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
65
|
+
color: var(--text, #cdd6f4);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.dr-bar {
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: 2px;
|
|
72
|
+
padding: 3px;
|
|
73
|
+
background: var(--mantle, #181825);
|
|
74
|
+
border: 1px solid var(--surface1, #45475a);
|
|
75
|
+
border-radius: 8px;
|
|
76
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.34);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.dr-bar button, .dr-note button {
|
|
80
|
+
font: inherit;
|
|
81
|
+
font-size: 11px;
|
|
82
|
+
letter-spacing: 0.06em;
|
|
83
|
+
padding: 4px 9px;
|
|
84
|
+
border-radius: 5px;
|
|
85
|
+
border: 1px solid transparent;
|
|
86
|
+
background: transparent;
|
|
87
|
+
color: var(--subtext0, #a6adc8);
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
white-space: nowrap;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.dr-bar button:hover, .dr-note button:hover {
|
|
93
|
+
color: var(--text, #cdd6f4);
|
|
94
|
+
border-color: var(--surface1, #45475a);
|
|
95
|
+
background: var(--surface0, #313244);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.dr-note button.dr-danger:hover {
|
|
99
|
+
color: var(--red, #f38ba8);
|
|
100
|
+
border-color: var(--red, #f38ba8);
|
|
101
|
+
background: transparent;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.dr-note {
|
|
105
|
+
width: 272px;
|
|
106
|
+
padding: 10px 11px 9px;
|
|
107
|
+
background: var(--mantle, #181825);
|
|
108
|
+
border: 1px solid var(--surface1, #45475a);
|
|
109
|
+
border-radius: 10px;
|
|
110
|
+
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.4);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.dr-note__quote {
|
|
114
|
+
font-size: 10.5px;
|
|
115
|
+
line-height: 1.5;
|
|
116
|
+
color: var(--overlay1, #7f849c);
|
|
117
|
+
border-left: 2px solid #ffe75e;
|
|
118
|
+
padding-left: 7px;
|
|
119
|
+
margin-bottom: 8px;
|
|
120
|
+
max-height: 46px;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.dr-note textarea {
|
|
125
|
+
display: block;
|
|
126
|
+
width: 100%;
|
|
127
|
+
height: 76px;
|
|
128
|
+
resize: vertical;
|
|
129
|
+
font: inherit;
|
|
130
|
+
font-size: 12px;
|
|
131
|
+
line-height: 1.5;
|
|
132
|
+
padding: 6px 7px;
|
|
133
|
+
border-radius: 6px;
|
|
134
|
+
border: 1px solid var(--surface1, #45475a);
|
|
135
|
+
background: var(--base, #1e1e2e);
|
|
136
|
+
color: var(--text, #cdd6f4);
|
|
137
|
+
outline: none;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.dr-note textarea:focus { border-color: var(--accent, #89b4fa); }
|
|
141
|
+
|
|
142
|
+
.dr-note__row {
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
gap: 3px;
|
|
146
|
+
margin-top: 7px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.dr-note__row .dr-spacer { flex: 1 1 auto; }
|
|
150
|
+
|
|
151
|
+
.dr-warn {
|
|
152
|
+
left: 50%;
|
|
153
|
+
bottom: 26px;
|
|
154
|
+
transform: translateX(-50%);
|
|
155
|
+
display: flex;
|
|
156
|
+
align-items: flex-start;
|
|
157
|
+
gap: 10px;
|
|
158
|
+
max-width: min(520px, 92vw);
|
|
159
|
+
padding: 10px 12px;
|
|
160
|
+
font-size: 11.5px;
|
|
161
|
+
line-height: 1.6;
|
|
162
|
+
background: var(--mantle, #181825);
|
|
163
|
+
border: 1px solid #ffe75e;
|
|
164
|
+
border-radius: 9px;
|
|
165
|
+
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.4);
|
|
166
|
+
color: var(--subtext0, #a6adc8);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.dr-warn b { color: var(--text, #cdd6f4); font-weight: 600; }
|
|
170
|
+
|
|
171
|
+
.dr-warn button {
|
|
172
|
+
flex: 0 0 auto;
|
|
173
|
+
font: inherit;
|
|
174
|
+
font-size: 14px;
|
|
175
|
+
line-height: 1;
|
|
176
|
+
padding: 0 2px;
|
|
177
|
+
background: transparent;
|
|
178
|
+
border: none;
|
|
179
|
+
color: var(--overlay0, #6c7086);
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.dr-warn button:hover { color: var(--text, #cdd6f4); }`;
|
|
184
|
+
/** The message shown once per browser session, the first time anyone highlights. */
|
|
185
|
+
export const HIGHLIGHT_WARNING = "Highlights and comments live only in this browser session. They travel to the deck you present, and disappear when the tab closes. Nothing is written to disk.";
|
|
186
|
+
/**
|
|
187
|
+
* `window.deckrunHighlights`, ready to be dropped into any deckrun page.
|
|
188
|
+
* Callers get it going with `mount()`; see the options block below.
|
|
189
|
+
*/
|
|
190
|
+
export const HIGHLIGHT_RUNTIME = `(function () {
|
|
191
|
+
'use strict';
|
|
192
|
+
if (window.deckrunHighlights) return;
|
|
193
|
+
|
|
194
|
+
var MARK_CSS = ${JSON.stringify(MARK_CSS)};
|
|
195
|
+
var UI_CSS = ${JSON.stringify(UI_CSS)};
|
|
196
|
+
var WARNING = ${JSON.stringify(HIGHLIGHT_WARNING)};
|
|
197
|
+
var KEY = 'deckrun.highlights';
|
|
198
|
+
var SLIDE_SELECTOR = '#presentation .slide[data-index]';
|
|
199
|
+
var TAB = 'tab' + Math.random().toString(36).slice(2);
|
|
200
|
+
|
|
201
|
+
// ── Store ────────────────────────────────────────────────────────────
|
|
202
|
+
// One session-scoped blob for every document this browser session has
|
|
203
|
+
// touched, mirrored to every other tab of this origin as it changes.
|
|
204
|
+
var store = null;
|
|
205
|
+
var listeners = [];
|
|
206
|
+
|
|
207
|
+
function blank() { return { v: 1, warned: false, docs: {} }; }
|
|
208
|
+
|
|
209
|
+
function load() {
|
|
210
|
+
if (store) return store;
|
|
211
|
+
try {
|
|
212
|
+
var raw = window.sessionStorage.getItem(KEY);
|
|
213
|
+
store = raw ? JSON.parse(raw) : null;
|
|
214
|
+
} catch (e) { store = null; }
|
|
215
|
+
if (!store || typeof store !== 'object' || store.v !== 1 || !store.docs) store = blank();
|
|
216
|
+
return store;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function persist() {
|
|
220
|
+
try { window.sessionStorage.setItem(KEY, JSON.stringify(load())); } catch (e) {}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
var chan = null;
|
|
224
|
+
try { chan = new BroadcastChannel(KEY); } catch (e) { chan = null; }
|
|
225
|
+
|
|
226
|
+
if (chan) {
|
|
227
|
+
chan.onmessage = function (e) {
|
|
228
|
+
var m = e.data;
|
|
229
|
+
if (!m || m.tab === TAB || !m.store || m.store.v !== 1) return;
|
|
230
|
+
store = m.store;
|
|
231
|
+
if (!store.docs) store.docs = {};
|
|
232
|
+
persist();
|
|
233
|
+
emit();
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function broadcast() {
|
|
238
|
+
if (!chan) return;
|
|
239
|
+
try { chan.postMessage({ tab: TAB, store: load() }); } catch (e) {}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function emit() {
|
|
243
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
244
|
+
try { listeners[i](); } catch (e) {}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** A change worth telling the other tabs and every mount about. */
|
|
249
|
+
function commit() { persist(); broadcast(); emit(); }
|
|
250
|
+
|
|
251
|
+
function bucket(docKey, scopeKey, create) {
|
|
252
|
+
var docs = load().docs;
|
|
253
|
+
var doc = docs[docKey];
|
|
254
|
+
if (!doc) { if (!create) return null; doc = docs[docKey] = {}; }
|
|
255
|
+
var list = doc[scopeKey];
|
|
256
|
+
if (!list) { if (!create) return null; list = doc[scopeKey] = []; }
|
|
257
|
+
return list;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function newId() {
|
|
261
|
+
return 'h' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Text addressing ──────────────────────────────────────────────────
|
|
265
|
+
// A highlight is remembered as a character offset into a scope's plain
|
|
266
|
+
// text plus the text itself. Offsets alone would not survive an edit two
|
|
267
|
+
// paragraphs up; the quote is what lets a highlight re-find its home.
|
|
268
|
+
function skip(el) {
|
|
269
|
+
var name = el.nodeName;
|
|
270
|
+
if (name === 'SCRIPT' || name === 'STYLE' || name === 'NOSCRIPT' ||
|
|
271
|
+
name === 'TEXTAREA' || name === 'svg') return true;
|
|
272
|
+
if (el.hasAttribute && el.hasAttribute('data-dr-ui')) return true;
|
|
273
|
+
// Rendered maths and diagrams keep shadow copies of their own source
|
|
274
|
+
// text; splitting either one apart breaks the render.
|
|
275
|
+
return !!(el.classList && (el.classList.contains('katex') || el.classList.contains('mermaid')));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function textNodes(root) {
|
|
279
|
+
var out = [];
|
|
280
|
+
var doc = root.ownerDocument;
|
|
281
|
+
var walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
282
|
+
acceptNode: function (node) {
|
|
283
|
+
if (!node.nodeValue) return NodeFilter.FILTER_REJECT;
|
|
284
|
+
var p = node.parentNode;
|
|
285
|
+
while (p && p !== root) {
|
|
286
|
+
if (p.nodeType === 1 && skip(p)) return NodeFilter.FILTER_REJECT;
|
|
287
|
+
p = p.parentNode;
|
|
288
|
+
}
|
|
289
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
var n;
|
|
293
|
+
while ((n = walker.nextNode())) out.push(n);
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function joinText(nodes) {
|
|
298
|
+
var s = '';
|
|
299
|
+
for (var i = 0; i < nodes.length; i++) s += nodes[i].nodeValue;
|
|
300
|
+
return s;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Character offset of a DOM point, measured over a scope's visible text. */
|
|
304
|
+
function pointOffset(root, nodes, container, offset) {
|
|
305
|
+
var range;
|
|
306
|
+
try {
|
|
307
|
+
range = root.ownerDocument.createRange();
|
|
308
|
+
range.selectNodeContents(root);
|
|
309
|
+
range.setEnd(container, offset);
|
|
310
|
+
} catch (e) { return -1; }
|
|
311
|
+
|
|
312
|
+
var total = 0;
|
|
313
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
314
|
+
var node = nodes[i];
|
|
315
|
+
var len = node.nodeValue.length;
|
|
316
|
+
var before, after;
|
|
317
|
+
try {
|
|
318
|
+
before = range.comparePoint(node, 0);
|
|
319
|
+
after = range.comparePoint(node, len);
|
|
320
|
+
} catch (e) { continue; }
|
|
321
|
+
if (before > 0) break;
|
|
322
|
+
if (after <= 0) { total += len; continue; }
|
|
323
|
+
// The end lands inside this node, which only happens when the point
|
|
324
|
+
// itself is a text position: an element boundary never splits a node.
|
|
325
|
+
total += (container === node) ? offset : 0;
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
return total;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Where a remembered highlight sits now, or null if its text is gone. */
|
|
332
|
+
function locate(text, item) {
|
|
333
|
+
var quote = item.text || '';
|
|
334
|
+
if (!quote) return null;
|
|
335
|
+
if (text.substr(item.start, quote.length) === quote) {
|
|
336
|
+
return { start: item.start, end: item.start + quote.length };
|
|
337
|
+
}
|
|
338
|
+
var best = -1, bestGap = Infinity, at = text.indexOf(quote);
|
|
339
|
+
while (at !== -1) {
|
|
340
|
+
var gap = Math.abs(at - item.start);
|
|
341
|
+
if (gap < bestGap) { bestGap = gap; best = at; }
|
|
342
|
+
at = text.indexOf(quote, at + 1);
|
|
343
|
+
}
|
|
344
|
+
if (best < 0) return null;
|
|
345
|
+
return { start: best, end: best + quote.length };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function unwrap(root) {
|
|
349
|
+
var marks = root.querySelectorAll('mark.dr-hl');
|
|
350
|
+
for (var i = 0; i < marks.length; i++) {
|
|
351
|
+
var mark = marks[i];
|
|
352
|
+
var parent = mark.parentNode;
|
|
353
|
+
if (!parent) continue;
|
|
354
|
+
while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
|
|
355
|
+
parent.removeChild(mark);
|
|
356
|
+
parent.normalize();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Paints one highlight, which may straddle any number of elements. */
|
|
361
|
+
function wrapRange(root, start, end, item) {
|
|
362
|
+
var doc = root.ownerDocument;
|
|
363
|
+
var nodes = textNodes(root);
|
|
364
|
+
var pieces = [];
|
|
365
|
+
var pos = 0;
|
|
366
|
+
for (var i = 0; i < nodes.length && pos < end; i++) {
|
|
367
|
+
var node = nodes[i];
|
|
368
|
+
var len = node.nodeValue.length;
|
|
369
|
+
var from = Math.max(start, pos) - pos;
|
|
370
|
+
var to = Math.min(end, pos + len) - pos;
|
|
371
|
+
// Whitespace between blocks is invisible, and wrapping it can land a
|
|
372
|
+
// mark somewhere the parser will not keep it, such as inside a table.
|
|
373
|
+
if (from < to && node.nodeValue.slice(from, to).trim()) {
|
|
374
|
+
pieces.push({ node: node, from: from, to: to });
|
|
375
|
+
}
|
|
376
|
+
pos += len;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
for (var p = 0; p < pieces.length; p++) {
|
|
380
|
+
var piece = pieces[p];
|
|
381
|
+
var target = piece.node;
|
|
382
|
+
if (piece.to < target.nodeValue.length) target.splitText(piece.to);
|
|
383
|
+
if (piece.from > 0) target = target.splitText(piece.from);
|
|
384
|
+
var mark = doc.createElement('mark');
|
|
385
|
+
mark.className = 'dr-hl' + (item.note ? ' dr-hl--note' : '');
|
|
386
|
+
mark.setAttribute('data-dr-hl', item.id);
|
|
387
|
+
if (item.note) mark.setAttribute('title', item.note);
|
|
388
|
+
if (p === pieces.length - 1) mark.setAttribute('data-dr-end', '1');
|
|
389
|
+
var holder = target.parentNode;
|
|
390
|
+
if (!holder) continue;
|
|
391
|
+
holder.replaceChild(mark, target);
|
|
392
|
+
mark.appendChild(target);
|
|
393
|
+
}
|
|
394
|
+
return pieces.length > 0;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── Mount ────────────────────────────────────────────────────────────
|
|
398
|
+
/**
|
|
399
|
+
* opts.doc document holding the text (omit when opts.frame is given)
|
|
400
|
+
* opts.frame iframe whose document holds the text, re-resolved per render
|
|
401
|
+
* opts.docKey which document's highlights these are
|
|
402
|
+
* opts.scopes 'slides' | 'doc' | function(doc) -> [{ key, el }]
|
|
403
|
+
* opts.onWarn host's own way of showing the first-use warning
|
|
404
|
+
* opts.readOnly true to only paint highlights made elsewhere (editor's
|
|
405
|
+
* preview) and skip the create-on-select bar — for the deck
|
|
406
|
+
* someone is actually presenting, where selecting text to
|
|
407
|
+
* click through slides should not pop up "highlight".
|
|
408
|
+
*/
|
|
409
|
+
function mount(opts) {
|
|
410
|
+
opts = opts || {};
|
|
411
|
+
var frame = opts.frame || null;
|
|
412
|
+
var uiDoc = frame ? frame.ownerDocument : (opts.doc || document);
|
|
413
|
+
var scopes = opts.scopes || 'slides';
|
|
414
|
+
var docKey = opts.docKey || 'default';
|
|
415
|
+
var readOnly = !!opts.readOnly;
|
|
416
|
+
var bound = null; // document the listeners and observer sit on
|
|
417
|
+
var observer = null;
|
|
418
|
+
var queued = false;
|
|
419
|
+
var applying = false;
|
|
420
|
+
var pending = null; // the selection the bar is offering to highlight
|
|
421
|
+
var openId = null; // highlight whose comment card is open
|
|
422
|
+
var bar = null, card = null;
|
|
423
|
+
|
|
424
|
+
function target() {
|
|
425
|
+
if (opts.doc) return opts.doc;
|
|
426
|
+
if (!frame) return null;
|
|
427
|
+
try { return frame.contentDocument; } catch (e) { return null; }
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ── Styling ──
|
|
431
|
+
function styleInto(doc, id, css) {
|
|
432
|
+
if (!doc || !doc.head || doc.getElementById(id)) return;
|
|
433
|
+
var tag = doc.createElement('style');
|
|
434
|
+
tag.id = id;
|
|
435
|
+
tag.setAttribute('data-dr-ui', '1');
|
|
436
|
+
tag.textContent = css;
|
|
437
|
+
doc.head.appendChild(tag);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── Scopes ──
|
|
441
|
+
function scopeList() {
|
|
442
|
+
var doc = target();
|
|
443
|
+
if (!doc || !doc.body) return [];
|
|
444
|
+
if (typeof scopes === 'function') return scopes(doc) || [];
|
|
445
|
+
if (scopes === 'doc') return [{ key: 'doc', el: doc.body }];
|
|
446
|
+
var out = [];
|
|
447
|
+
var els = doc.querySelectorAll(SLIDE_SELECTOR);
|
|
448
|
+
for (var i = 0; i < els.length; i++) {
|
|
449
|
+
out.push({ key: 's' + els[i].getAttribute('data-index'), el: els[i] });
|
|
450
|
+
}
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function scopeOf(node) {
|
|
455
|
+
var list = scopeList();
|
|
456
|
+
var el = node && node.nodeType === 1 ? node : (node ? node.parentNode : null);
|
|
457
|
+
for (var i = 0; i < list.length; i++) {
|
|
458
|
+
if (el && list[i].el.contains(el)) return list[i];
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ── Render ──
|
|
464
|
+
function paintScope(scope) {
|
|
465
|
+
unwrap(scope.el);
|
|
466
|
+
var items = bucket(docKey, scope.key, false);
|
|
467
|
+
if (!items || !items.length) return false;
|
|
468
|
+
var moved = false;
|
|
469
|
+
for (var i = 0; i < items.length; i++) {
|
|
470
|
+
var item = items[i];
|
|
471
|
+
var nodes = textNodes(scope.el);
|
|
472
|
+
var found = locate(joinText(nodes), item);
|
|
473
|
+
if (!found) continue;
|
|
474
|
+
if (found.start !== item.start) { item.start = found.start; moved = true; }
|
|
475
|
+
wrapRange(scope.el, found.start, found.end, item);
|
|
476
|
+
}
|
|
477
|
+
return moved;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function render() {
|
|
481
|
+
var doc = target();
|
|
482
|
+
if (!doc || !doc.body) return;
|
|
483
|
+
bind(doc);
|
|
484
|
+
styleInto(doc, 'dr-hl-style', MARK_CSS);
|
|
485
|
+
|
|
486
|
+
applying = true;
|
|
487
|
+
var moved = false;
|
|
488
|
+
try {
|
|
489
|
+
var list = scopeList();
|
|
490
|
+
for (var i = 0; i < list.length; i++) {
|
|
491
|
+
if (paintScope(list[i])) moved = true;
|
|
492
|
+
}
|
|
493
|
+
} catch (e) {
|
|
494
|
+
} finally {
|
|
495
|
+
applying = false;
|
|
496
|
+
// Our own wrapping queued records; drop them so painting the marks
|
|
497
|
+
// does not read as a change that needs painting again.
|
|
498
|
+
if (observer) observer.takeRecords();
|
|
499
|
+
}
|
|
500
|
+
if (moved) persist();
|
|
501
|
+
placeBar();
|
|
502
|
+
placeCard();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function schedule() {
|
|
506
|
+
if (queued) return;
|
|
507
|
+
queued = true;
|
|
508
|
+
setTimeout(function () { queued = false; render(); }, 60);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ── Binding ──
|
|
512
|
+
function bind(doc) {
|
|
513
|
+
if (bound === doc) return;
|
|
514
|
+
bound = doc;
|
|
515
|
+
if (observer) observer.disconnect();
|
|
516
|
+
if (!readOnly) {
|
|
517
|
+
doc.addEventListener('mouseup', onSelect, true);
|
|
518
|
+
doc.addEventListener('keyup', onSelect, true);
|
|
519
|
+
}
|
|
520
|
+
doc.addEventListener('click', onClickMark, true);
|
|
521
|
+
doc.addEventListener('scroll', reposition, true);
|
|
522
|
+
if (doc.defaultView) doc.defaultView.addEventListener('resize', reposition);
|
|
523
|
+
observer = new MutationObserver(function () { if (!applying) schedule(); });
|
|
524
|
+
observer.observe(doc.documentElement, { childList: true, subtree: true, characterData: true });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function reposition() { placeBar(); placeCard(); }
|
|
528
|
+
|
|
529
|
+
// ── Coordinates ──
|
|
530
|
+
/** Rect in the host document's viewport, undoing any iframe scaling. */
|
|
531
|
+
function hostRect(rect) {
|
|
532
|
+
if (!frame) {
|
|
533
|
+
return { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom,
|
|
534
|
+
width: rect.width, height: rect.height };
|
|
535
|
+
}
|
|
536
|
+
var box = frame.getBoundingClientRect();
|
|
537
|
+
var kx = 1, ky = 1;
|
|
538
|
+
try {
|
|
539
|
+
var win = frame.contentWindow;
|
|
540
|
+
if (win && win.innerWidth) kx = box.width / win.innerWidth;
|
|
541
|
+
if (win && win.innerHeight) ky = box.height / win.innerHeight;
|
|
542
|
+
} catch (e) {}
|
|
543
|
+
return {
|
|
544
|
+
left: box.left + rect.left * kx,
|
|
545
|
+
top: box.top + rect.top * ky,
|
|
546
|
+
right: box.left + rect.right * kx,
|
|
547
|
+
bottom: box.top + rect.bottom * ky,
|
|
548
|
+
width: rect.width * kx,
|
|
549
|
+
height: rect.height * ky
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Anchors a popup above its rect, flipping and clamping to stay on screen. */
|
|
554
|
+
function place(el, rect) {
|
|
555
|
+
el.style.left = '0px';
|
|
556
|
+
el.style.top = '0px';
|
|
557
|
+
var size = el.getBoundingClientRect();
|
|
558
|
+
var vw = uiDoc.documentElement.clientWidth;
|
|
559
|
+
var vh = uiDoc.documentElement.clientHeight;
|
|
560
|
+
var left = rect.left + rect.width / 2 - size.width / 2;
|
|
561
|
+
var top = rect.top - size.height - 9;
|
|
562
|
+
if (top < 8) top = Math.min(rect.bottom + 9, vh - size.height - 8);
|
|
563
|
+
el.style.left = Math.round(Math.max(8, Math.min(left, vw - size.width - 8))) + 'px';
|
|
564
|
+
el.style.top = Math.round(Math.max(8, top)) + 'px';
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function marksFor(id) {
|
|
568
|
+
var doc = target();
|
|
569
|
+
if (!doc) return [];
|
|
570
|
+
return doc.querySelectorAll('mark[data-dr-hl="' + id + '"]');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function rectOfId(id) {
|
|
574
|
+
var found = marksFor(id);
|
|
575
|
+
if (!found.length) return null;
|
|
576
|
+
return hostRect(found[0].getBoundingClientRect());
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ── Selection bar ──
|
|
580
|
+
function onSelect(e) {
|
|
581
|
+
if (e && e.type === 'keyup' && !e.shiftKey && e.key !== 'Shift') return;
|
|
582
|
+
setTimeout(readSelection, 0);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function readSelection() {
|
|
586
|
+
var doc = target();
|
|
587
|
+
if (!doc) return;
|
|
588
|
+
var sel = doc.getSelection ? doc.getSelection() : null;
|
|
589
|
+
if (!sel || sel.isCollapsed || !sel.rangeCount) { hideBar(); return; }
|
|
590
|
+
var range = sel.getRangeAt(0);
|
|
591
|
+
var text = range.toString();
|
|
592
|
+
if (!text || !text.trim()) { hideBar(); return; }
|
|
593
|
+
|
|
594
|
+
var scope = scopeOf(range.commonAncestorContainer);
|
|
595
|
+
if (!scope) { hideBar(); return; }
|
|
596
|
+
|
|
597
|
+
var nodes = textNodes(scope.el);
|
|
598
|
+
var start = pointOffset(scope.el, nodes, range.startContainer, range.startOffset);
|
|
599
|
+
var end = pointOffset(scope.el, nodes, range.endContainer, range.endOffset);
|
|
600
|
+
if (start < 0 || end <= start) { hideBar(); return; }
|
|
601
|
+
|
|
602
|
+
pending = {
|
|
603
|
+
scope: scope.key,
|
|
604
|
+
start: start,
|
|
605
|
+
text: joinText(nodes).slice(start, end),
|
|
606
|
+
rect: hostRect(range.getBoundingClientRect())
|
|
607
|
+
};
|
|
608
|
+
showBar();
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function showBar() {
|
|
612
|
+
if (!bar) {
|
|
613
|
+
bar = uiDoc.createElement('div');
|
|
614
|
+
bar.className = 'dr-bar';
|
|
615
|
+
bar.setAttribute('data-dr-ui', '1');
|
|
616
|
+
bar.appendChild(button('highlight', function () { create(false); }));
|
|
617
|
+
bar.appendChild(button('highlight + comment', function () { create(true); }));
|
|
618
|
+
bar.addEventListener('mousedown', function (e) { e.preventDefault(); });
|
|
619
|
+
uiDoc.body.appendChild(bar);
|
|
620
|
+
}
|
|
621
|
+
bar.style.display = 'flex';
|
|
622
|
+
placeBar();
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function placeBar() {
|
|
626
|
+
if (!bar || bar.style.display === 'none' || !pending) return;
|
|
627
|
+
place(bar, pending.rect);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function hideBar() {
|
|
631
|
+
pending = null;
|
|
632
|
+
if (bar) bar.style.display = 'none';
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function button(label, onClick, className) {
|
|
636
|
+
var el = uiDoc.createElement('button');
|
|
637
|
+
el.type = 'button';
|
|
638
|
+
el.textContent = label;
|
|
639
|
+
if (className) el.className = className;
|
|
640
|
+
el.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); onClick(); });
|
|
641
|
+
return el;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ── Creating and editing ──
|
|
645
|
+
function create(withComment) {
|
|
646
|
+
if (!pending) return;
|
|
647
|
+
var item = { id: newId(), start: pending.start, text: pending.text, note: '', at: Date.now() };
|
|
648
|
+
var scopeKey = pending.scope;
|
|
649
|
+
bucket(docKey, scopeKey, true).push(item);
|
|
650
|
+
commit();
|
|
651
|
+
|
|
652
|
+
var doc = target();
|
|
653
|
+
try { if (doc && doc.getSelection) doc.getSelection().removeAllRanges(); } catch (e) {}
|
|
654
|
+
hideBar();
|
|
655
|
+
render();
|
|
656
|
+
warnOnce();
|
|
657
|
+
if (withComment) openCard(item.id);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function find(id) {
|
|
661
|
+
var docs = load().docs[docKey];
|
|
662
|
+
if (!docs) return null;
|
|
663
|
+
for (var scopeKey in docs) {
|
|
664
|
+
if (!Object.prototype.hasOwnProperty.call(docs, scopeKey)) continue;
|
|
665
|
+
var list = docs[scopeKey];
|
|
666
|
+
for (var i = 0; i < list.length; i++) {
|
|
667
|
+
if (list[i].id === id) return { item: list[i], scope: scopeKey, list: list, at: i };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function onClickMark(e) {
|
|
674
|
+
var el = e.target;
|
|
675
|
+
while (el && el.nodeType === 1 && !(el.classList && el.classList.contains('dr-hl'))) {
|
|
676
|
+
el = el.parentNode;
|
|
677
|
+
}
|
|
678
|
+
if (!el || el.nodeType !== 1 || !el.classList.contains('dr-hl')) return;
|
|
679
|
+
if (!scopeOf(el)) return;
|
|
680
|
+
e.preventDefault();
|
|
681
|
+
e.stopPropagation();
|
|
682
|
+
hideBar();
|
|
683
|
+
openCard(el.getAttribute('data-dr-hl'));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function openCard(id) {
|
|
687
|
+
var hit = find(id);
|
|
688
|
+
if (!hit) return;
|
|
689
|
+
closeCard();
|
|
690
|
+
openId = id;
|
|
691
|
+
|
|
692
|
+
card = uiDoc.createElement('div');
|
|
693
|
+
card.className = 'dr-note';
|
|
694
|
+
card.setAttribute('data-dr-ui', '1');
|
|
695
|
+
|
|
696
|
+
var quote = uiDoc.createElement('div');
|
|
697
|
+
quote.className = 'dr-note__quote';
|
|
698
|
+
quote.textContent = hit.item.text;
|
|
699
|
+
card.appendChild(quote);
|
|
700
|
+
|
|
701
|
+
var input = uiDoc.createElement('textarea');
|
|
702
|
+
input.placeholder = 'Add a comment';
|
|
703
|
+
input.value = hit.item.note || '';
|
|
704
|
+
card.appendChild(input);
|
|
705
|
+
|
|
706
|
+
var row = uiDoc.createElement('div');
|
|
707
|
+
row.className = 'dr-note__row';
|
|
708
|
+
row.appendChild(button('remove', function () { remove(id); }, 'dr-danger'));
|
|
709
|
+
var spacer = uiDoc.createElement('span');
|
|
710
|
+
spacer.className = 'dr-spacer';
|
|
711
|
+
row.appendChild(spacer);
|
|
712
|
+
row.appendChild(button('close', function () { closeCard(); }));
|
|
713
|
+
row.appendChild(button('save', function () { saveNote(id, input.value); }));
|
|
714
|
+
card.appendChild(row);
|
|
715
|
+
|
|
716
|
+
// The deck and the editor both listen for bare keys on the document;
|
|
717
|
+
// typing a comment must not black out the screen or open the palette.
|
|
718
|
+
card.addEventListener('keydown', function (e) {
|
|
719
|
+
e.stopPropagation();
|
|
720
|
+
if (e.key === 'Escape') { e.preventDefault(); closeCard(); }
|
|
721
|
+
else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); saveNote(id, input.value); }
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
uiDoc.body.appendChild(card);
|
|
725
|
+
placeCard();
|
|
726
|
+
input.focus();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function placeCard() {
|
|
730
|
+
if (!card || !openId) return;
|
|
731
|
+
var rect = rectOfId(openId);
|
|
732
|
+
if (!rect) { closeCard(); return; }
|
|
733
|
+
place(card, rect);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function closeCard() {
|
|
737
|
+
if (card && card.parentNode) card.parentNode.removeChild(card);
|
|
738
|
+
card = null;
|
|
739
|
+
openId = null;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function saveNote(id, value) {
|
|
743
|
+
var hit = find(id);
|
|
744
|
+
if (!hit) { closeCard(); return; }
|
|
745
|
+
hit.item.note = (value || '').trim();
|
|
746
|
+
commit();
|
|
747
|
+
closeCard();
|
|
748
|
+
render();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function remove(id) {
|
|
752
|
+
var hit = find(id);
|
|
753
|
+
if (hit) {
|
|
754
|
+
hit.list.splice(hit.at, 1);
|
|
755
|
+
commit();
|
|
756
|
+
}
|
|
757
|
+
closeCard();
|
|
758
|
+
render();
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ── First-use warning ──
|
|
762
|
+
function warnOnce() {
|
|
763
|
+
var state = load();
|
|
764
|
+
if (state.warned) return;
|
|
765
|
+
state.warned = true;
|
|
766
|
+
persist();
|
|
767
|
+
broadcast();
|
|
768
|
+
if (opts.onWarn) { opts.onWarn(WARNING); return; }
|
|
769
|
+
|
|
770
|
+
var box = uiDoc.createElement('div');
|
|
771
|
+
box.className = 'dr-warn';
|
|
772
|
+
box.setAttribute('data-dr-ui', '1');
|
|
773
|
+
var text = uiDoc.createElement('span');
|
|
774
|
+
text.innerHTML = '<b>Nothing is saved.</b> ';
|
|
775
|
+
text.appendChild(uiDoc.createTextNode(WARNING));
|
|
776
|
+
box.appendChild(text);
|
|
777
|
+
var close = uiDoc.createElement('button');
|
|
778
|
+
close.type = 'button';
|
|
779
|
+
close.textContent = '\\u00d7';
|
|
780
|
+
close.addEventListener('click', function () { if (box.parentNode) box.parentNode.removeChild(box); });
|
|
781
|
+
box.appendChild(close);
|
|
782
|
+
uiDoc.body.appendChild(box);
|
|
783
|
+
setTimeout(function () { if (box.parentNode) box.parentNode.removeChild(box); }, 11000);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// ── Wiring ──
|
|
787
|
+
styleInto(uiDoc, 'dr-hl-ui-style', UI_CSS);
|
|
788
|
+
|
|
789
|
+
uiDoc.addEventListener('mousedown', function (e) {
|
|
790
|
+
if (bar && bar.contains(e.target)) return;
|
|
791
|
+
if (card && card.contains(e.target)) return;
|
|
792
|
+
if (frame && frame.contains(e.target)) return;
|
|
793
|
+
hideBar();
|
|
794
|
+
closeCard();
|
|
795
|
+
}, true);
|
|
796
|
+
|
|
797
|
+
uiDoc.addEventListener('keydown', function (e) {
|
|
798
|
+
if (e.key === 'Escape' && (bar || card)) { hideBar(); closeCard(); }
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
listeners.push(schedule);
|
|
802
|
+
// A frame that has not loaded yet has no document to paint into, and a
|
|
803
|
+
// reassigned srcdoc replaces the one that was there.
|
|
804
|
+
if (frame) frame.addEventListener('load', function () { render(); });
|
|
805
|
+
|
|
806
|
+
var api = {
|
|
807
|
+
refresh: function () { render(); },
|
|
808
|
+
setDocKey: function (key) {
|
|
809
|
+
if (key === docKey) return;
|
|
810
|
+
docKey = key || 'default';
|
|
811
|
+
hideBar();
|
|
812
|
+
closeCard();
|
|
813
|
+
render();
|
|
814
|
+
},
|
|
815
|
+
docKey: function () { return docKey; }
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
render();
|
|
819
|
+
return api;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
window.deckrunHighlights = {
|
|
823
|
+
mount: mount,
|
|
824
|
+
warning: WARNING,
|
|
825
|
+
/** Drops a document's highlights, for when the document itself is gone. */
|
|
826
|
+
forget: function (docKey) {
|
|
827
|
+
var docs = load().docs;
|
|
828
|
+
if (!docs[docKey]) return;
|
|
829
|
+
delete docs[docKey];
|
|
830
|
+
commit();
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
})();`;
|