@farberg/reveal-template 1.1.29 → 1.1.31
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.
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
<ul class="menu"><ul>
|
|
9
9
|
|
|
10
|
+
---
|
|
11
|
+
## Code Example
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Create a Kafka cluster using the operator
|
|
15
|
+
kubectl apply -f https://raw.githubusercontent.com/strimzi/strimzi-kafka-operator/refs/heads/main/examples/kafka/kafka-ephemeral.yaml
|
|
16
|
+
```
|
|
17
|
+
|
|
10
18
|
---
|
|
11
19
|
## Mermaid Example
|
|
12
20
|
|
package/package.json
CHANGED
|
@@ -119,6 +119,231 @@ const dirTreeFactory = ({ zip, strToU8 }) => ({
|
|
|
119
119
|
background: #dafbe1;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/* Preview button — same hover behaviour as the copy button */
|
|
123
|
+
.dirtree-preview-btn {
|
|
124
|
+
display: inline-flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
margin-left: 6px;
|
|
127
|
+
padding: 0px 6px;
|
|
128
|
+
background: transparent;
|
|
129
|
+
border: 1px solid #d0d7de;
|
|
130
|
+
border-radius: 4px;
|
|
131
|
+
color: #57606a;
|
|
132
|
+
font-size: 0.78em;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
opacity: 0 !important;
|
|
135
|
+
pointer-events: none !important;
|
|
136
|
+
transition: opacity 0.15s, background 0.15s, color 0.15s;
|
|
137
|
+
user-select: none;
|
|
138
|
+
line-height: 1.6;
|
|
139
|
+
font-family: inherit;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
li.dirtree:hover > .dirtree-preview-btn {
|
|
143
|
+
opacity: 1 !important;
|
|
144
|
+
pointer-events: auto !important;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.dirtree-preview-btn:hover {
|
|
148
|
+
background: #f6f8fa;
|
|
149
|
+
border-color: #8c959f;
|
|
150
|
+
color: #1f2328;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Preview modal */
|
|
154
|
+
.dirtree-modal-overlay {
|
|
155
|
+
position: fixed;
|
|
156
|
+
inset: 0;
|
|
157
|
+
background: rgba(27, 31, 36, 0.55);
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
justify-content: center;
|
|
161
|
+
z-index: 10000;
|
|
162
|
+
padding: 4vh 4vw;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.dirtree-modal {
|
|
166
|
+
background: #ffffff;
|
|
167
|
+
border: 1px solid #d0d7de;
|
|
168
|
+
border-radius: 10px;
|
|
169
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
max-width: 1100px;
|
|
173
|
+
width: 100%;
|
|
174
|
+
max-height: 92vh;
|
|
175
|
+
overflow: hidden;
|
|
176
|
+
text-align: left;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.dirtree-modal-header {
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
justify-content: space-between;
|
|
183
|
+
gap: 12px;
|
|
184
|
+
padding: 10px 14px;
|
|
185
|
+
border-bottom: 1px solid #d0d7de;
|
|
186
|
+
background: #f6f8fa;
|
|
187
|
+
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.dirtree-modal-title {
|
|
191
|
+
color: #1f2328;
|
|
192
|
+
font-size: 0.95rem;
|
|
193
|
+
font-weight: 600;
|
|
194
|
+
word-break: break-all;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.dirtree-modal-actions {
|
|
198
|
+
display: flex;
|
|
199
|
+
align-items: center;
|
|
200
|
+
gap: 8px;
|
|
201
|
+
flex-shrink: 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.dirtree-modal-btn {
|
|
205
|
+
padding: 2px 10px;
|
|
206
|
+
background: transparent;
|
|
207
|
+
border: 1px solid #d0d7de;
|
|
208
|
+
border-radius: 6px;
|
|
209
|
+
color: #57606a;
|
|
210
|
+
font-size: 0.85rem;
|
|
211
|
+
cursor: pointer;
|
|
212
|
+
font-family: inherit;
|
|
213
|
+
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.dirtree-modal-btn:hover {
|
|
217
|
+
background: #ffffff;
|
|
218
|
+
border-color: #8c959f;
|
|
219
|
+
color: #1f2328;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.dirtree-modal-btn.copied {
|
|
223
|
+
color: #116329;
|
|
224
|
+
border-color: #116329;
|
|
225
|
+
background: #dafbe1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.dirtree-modal-body {
|
|
229
|
+
margin: 0;
|
|
230
|
+
padding: 0;
|
|
231
|
+
overflow: auto;
|
|
232
|
+
background: #ffffff;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.dirtree-modal-body pre {
|
|
236
|
+
margin: 0;
|
|
237
|
+
box-shadow: none;
|
|
238
|
+
background: transparent;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.dirtree-modal-body code {
|
|
242
|
+
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace;
|
|
243
|
+
/* Default size; the A−/A+ buttons override this via inline style. */
|
|
244
|
+
font-size: 16px;
|
|
245
|
+
line-height: 1.55;
|
|
246
|
+
color: #24292e;
|
|
247
|
+
white-space: pre;
|
|
248
|
+
tab-size: 4;
|
|
249
|
+
padding: 14px 16px;
|
|
250
|
+
display: block;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* ── Light syntax theme (GitHub), scoped to the preview modal ──
|
|
254
|
+
The slides use the global dark zenburn theme; these rules win by
|
|
255
|
+
specificity (.dirtree-modal-body .hljs-* beats .hljs-*) so only
|
|
256
|
+
the modal renders light. */
|
|
257
|
+
.dirtree-modal-body code.hljs {
|
|
258
|
+
background: transparent;
|
|
259
|
+
color: #24292e;
|
|
260
|
+
}
|
|
261
|
+
.dirtree-modal-body .hljs-comment,
|
|
262
|
+
.dirtree-modal-body .hljs-quote {
|
|
263
|
+
color: #6a737d;
|
|
264
|
+
}
|
|
265
|
+
.dirtree-modal-body .hljs-keyword,
|
|
266
|
+
.dirtree-modal-body .hljs-selector-tag,
|
|
267
|
+
.dirtree-modal-body .hljs-doctag {
|
|
268
|
+
color: #d73a49;
|
|
269
|
+
}
|
|
270
|
+
.dirtree-modal-body .hljs-string,
|
|
271
|
+
.dirtree-modal-body .hljs-meta .hljs-string,
|
|
272
|
+
.dirtree-modal-body .hljs-regexp {
|
|
273
|
+
color: #032f62;
|
|
274
|
+
}
|
|
275
|
+
.dirtree-modal-body .hljs-number,
|
|
276
|
+
.dirtree-modal-body .hljs-literal,
|
|
277
|
+
.dirtree-modal-body .hljs-attr,
|
|
278
|
+
.dirtree-modal-body .hljs-attribute,
|
|
279
|
+
.dirtree-modal-body .hljs-variable,
|
|
280
|
+
.dirtree-modal-body .hljs-template-variable {
|
|
281
|
+
color: #005cc5;
|
|
282
|
+
}
|
|
283
|
+
.dirtree-modal-body .hljs-title,
|
|
284
|
+
.dirtree-modal-body .hljs-section,
|
|
285
|
+
.dirtree-modal-body .hljs-name,
|
|
286
|
+
.dirtree-modal-body .hljs-selector-id,
|
|
287
|
+
.dirtree-modal-body .hljs-selector-class {
|
|
288
|
+
color: #6f42c1;
|
|
289
|
+
}
|
|
290
|
+
.dirtree-modal-body .hljs-built_in,
|
|
291
|
+
.dirtree-modal-body .hljs-type,
|
|
292
|
+
.dirtree-modal-body .hljs-symbol,
|
|
293
|
+
.dirtree-modal-body .hljs-bullet,
|
|
294
|
+
.dirtree-modal-body .hljs-link {
|
|
295
|
+
color: #e36209;
|
|
296
|
+
}
|
|
297
|
+
.dirtree-modal-body .hljs-meta {
|
|
298
|
+
color: #6a737d;
|
|
299
|
+
}
|
|
300
|
+
.dirtree-modal-body .hljs-emphasis { font-style: italic; }
|
|
301
|
+
.dirtree-modal-body .hljs-strong { font-weight: bold; }
|
|
302
|
+
|
|
303
|
+
/* ── Dark syntax theme (GitHub Dark), scoped + .dark modifier ──
|
|
304
|
+
Higher specificity than the light rules above so toggling the
|
|
305
|
+
.dark class on the body switches the whole modal. */
|
|
306
|
+
.dirtree-modal-body.dark { background: #0d1117; }
|
|
307
|
+
.dirtree-modal-body.dark code { color: #c9d1d9; }
|
|
308
|
+
.dirtree-modal-body.dark code.hljs { background: transparent; color: #c9d1d9; }
|
|
309
|
+
.dirtree-modal-body.dark .hljs-comment,
|
|
310
|
+
.dirtree-modal-body.dark .hljs-quote,
|
|
311
|
+
.dirtree-modal-body.dark .hljs-meta {
|
|
312
|
+
color: #8b949e;
|
|
313
|
+
}
|
|
314
|
+
.dirtree-modal-body.dark .hljs-keyword,
|
|
315
|
+
.dirtree-modal-body.dark .hljs-selector-tag,
|
|
316
|
+
.dirtree-modal-body.dark .hljs-doctag {
|
|
317
|
+
color: #ff7b72;
|
|
318
|
+
}
|
|
319
|
+
.dirtree-modal-body.dark .hljs-string,
|
|
320
|
+
.dirtree-modal-body.dark .hljs-meta .hljs-string,
|
|
321
|
+
.dirtree-modal-body.dark .hljs-regexp {
|
|
322
|
+
color: #a5d6ff;
|
|
323
|
+
}
|
|
324
|
+
.dirtree-modal-body.dark .hljs-number,
|
|
325
|
+
.dirtree-modal-body.dark .hljs-literal,
|
|
326
|
+
.dirtree-modal-body.dark .hljs-attr,
|
|
327
|
+
.dirtree-modal-body.dark .hljs-attribute,
|
|
328
|
+
.dirtree-modal-body.dark .hljs-variable,
|
|
329
|
+
.dirtree-modal-body.dark .hljs-template-variable {
|
|
330
|
+
color: #79c0ff;
|
|
331
|
+
}
|
|
332
|
+
.dirtree-modal-body.dark .hljs-title,
|
|
333
|
+
.dirtree-modal-body.dark .hljs-section,
|
|
334
|
+
.dirtree-modal-body.dark .hljs-name,
|
|
335
|
+
.dirtree-modal-body.dark .hljs-selector-id,
|
|
336
|
+
.dirtree-modal-body.dark .hljs-selector-class {
|
|
337
|
+
color: #d2a8ff;
|
|
338
|
+
}
|
|
339
|
+
.dirtree-modal-body.dark .hljs-built_in,
|
|
340
|
+
.dirtree-modal-body.dark .hljs-type,
|
|
341
|
+
.dirtree-modal-body.dark .hljs-symbol,
|
|
342
|
+
.dirtree-modal-body.dark .hljs-bullet,
|
|
343
|
+
.dirtree-modal-body.dark .hljs-link {
|
|
344
|
+
color: #ffa657;
|
|
345
|
+
}
|
|
346
|
+
|
|
122
347
|
/* Download button */
|
|
123
348
|
.dirtree-download-btn {
|
|
124
349
|
position: absolute !important;
|
|
@@ -224,6 +449,194 @@ const dirTreeFactory = ({ zip, strToU8 }) => ({
|
|
|
224
449
|
return btn;
|
|
225
450
|
}
|
|
226
451
|
|
|
452
|
+
// Reveal's highlight plugin owns the hljs instance (window.hljs is not set).
|
|
453
|
+
// Returns it if the plugin is loaded, else null.
|
|
454
|
+
function getHljs() {
|
|
455
|
+
const plugin = deck.getPlugin('highlight');
|
|
456
|
+
return (plugin && plugin.hljs) || null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Maps file extensions to highlight.js language classes so the modal can
|
|
460
|
+
// syntax-highlight the preview when reveal's highlight plugin is loaded.
|
|
461
|
+
// Unknown extensions fall back to plain text.
|
|
462
|
+
function hljsLanguage(ext) {
|
|
463
|
+
const map = {
|
|
464
|
+
js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript',
|
|
465
|
+
json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'ini', xml: 'xml', html: 'xml',
|
|
466
|
+
css: 'css', scss: 'scss', md: 'markdown',
|
|
467
|
+
sh: 'bash', bash: 'bash', zsh: 'bash',
|
|
468
|
+
py: 'python', rb: 'ruby', go: 'go', rs: 'rust', java: 'java', sql: 'sql',
|
|
469
|
+
dockerfile: 'dockerfile',
|
|
470
|
+
};
|
|
471
|
+
return map[ext] || null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Single, lazily-created modal reused for every preview. Closing hides it
|
|
475
|
+
// (display:none) rather than removing it from the DOM.
|
|
476
|
+
let modalRefs = null;
|
|
477
|
+
|
|
478
|
+
// View preferences persist across previews within a session.
|
|
479
|
+
const FONT_MIN = 11, FONT_MAX = 32, FONT_STEP = 2;
|
|
480
|
+
let previewFontPx = 16;
|
|
481
|
+
let previewTheme = 'light'; // 'light' | 'dark'
|
|
482
|
+
|
|
483
|
+
function ensureModal() {
|
|
484
|
+
if (modalRefs) return modalRefs;
|
|
485
|
+
|
|
486
|
+
const overlay = document.createElement('div');
|
|
487
|
+
overlay.classList.add('dirtree-modal-overlay');
|
|
488
|
+
overlay.style.display = 'none';
|
|
489
|
+
|
|
490
|
+
const modal = document.createElement('div');
|
|
491
|
+
modal.classList.add('dirtree-modal');
|
|
492
|
+
|
|
493
|
+
const header = document.createElement('div');
|
|
494
|
+
header.classList.add('dirtree-modal-header');
|
|
495
|
+
|
|
496
|
+
const title = document.createElement('span');
|
|
497
|
+
title.classList.add('dirtree-modal-title');
|
|
498
|
+
|
|
499
|
+
const actions = document.createElement('div');
|
|
500
|
+
actions.classList.add('dirtree-modal-actions');
|
|
501
|
+
|
|
502
|
+
const body = document.createElement('div');
|
|
503
|
+
body.classList.add('dirtree-modal-body');
|
|
504
|
+
const pre = document.createElement('pre');
|
|
505
|
+
const code = document.createElement('code');
|
|
506
|
+
pre.appendChild(code);
|
|
507
|
+
body.appendChild(pre);
|
|
508
|
+
|
|
509
|
+
// Applies the current font-size and theme preferences to the modal.
|
|
510
|
+
function applyView() {
|
|
511
|
+
code.style.fontSize = previewFontPx + 'px';
|
|
512
|
+
body.classList.toggle('dark', previewTheme === 'dark');
|
|
513
|
+
themeBtn.textContent = previewTheme === 'dark' ? '☀ light' : '🌙 dark';
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const fontDecBtn = document.createElement('button');
|
|
517
|
+
fontDecBtn.classList.add('dirtree-modal-btn');
|
|
518
|
+
fontDecBtn.title = 'Decrease font size';
|
|
519
|
+
fontDecBtn.textContent = 'A−';
|
|
520
|
+
fontDecBtn.addEventListener('click', () => {
|
|
521
|
+
previewFontPx = Math.max(FONT_MIN, previewFontPx - FONT_STEP);
|
|
522
|
+
applyView();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const fontIncBtn = document.createElement('button');
|
|
526
|
+
fontIncBtn.classList.add('dirtree-modal-btn');
|
|
527
|
+
fontIncBtn.title = 'Increase font size';
|
|
528
|
+
fontIncBtn.textContent = 'A+';
|
|
529
|
+
fontIncBtn.addEventListener('click', () => {
|
|
530
|
+
previewFontPx = Math.min(FONT_MAX, previewFontPx + FONT_STEP);
|
|
531
|
+
applyView();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
const themeBtn = document.createElement('button');
|
|
535
|
+
themeBtn.classList.add('dirtree-modal-btn');
|
|
536
|
+
themeBtn.title = 'Toggle light / dark theme';
|
|
537
|
+
themeBtn.addEventListener('click', () => {
|
|
538
|
+
previewTheme = previewTheme === 'dark' ? 'light' : 'dark';
|
|
539
|
+
applyView();
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const copyBtn = document.createElement('button');
|
|
543
|
+
copyBtn.classList.add('dirtree-modal-btn');
|
|
544
|
+
copyBtn.textContent = 'copy';
|
|
545
|
+
|
|
546
|
+
const closeBtn = document.createElement('button');
|
|
547
|
+
closeBtn.classList.add('dirtree-modal-btn');
|
|
548
|
+
closeBtn.textContent = '✕ close';
|
|
549
|
+
|
|
550
|
+
actions.appendChild(fontDecBtn);
|
|
551
|
+
actions.appendChild(fontIncBtn);
|
|
552
|
+
actions.appendChild(themeBtn);
|
|
553
|
+
actions.appendChild(copyBtn);
|
|
554
|
+
actions.appendChild(closeBtn);
|
|
555
|
+
header.appendChild(title);
|
|
556
|
+
header.appendChild(actions);
|
|
557
|
+
|
|
558
|
+
modal.appendChild(header);
|
|
559
|
+
modal.appendChild(body);
|
|
560
|
+
overlay.appendChild(modal);
|
|
561
|
+
document.body.appendChild(overlay);
|
|
562
|
+
|
|
563
|
+
function close() {
|
|
564
|
+
overlay.style.display = 'none';
|
|
565
|
+
// Re-enable reveal's keyboard navigation that we paused while open.
|
|
566
|
+
deck.configure({ keyboard: true });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Close on backdrop click, but not when clicking inside the modal.
|
|
570
|
+
overlay.addEventListener('click', e => { if (e.target === overlay) close(); });
|
|
571
|
+
closeBtn.addEventListener('click', close);
|
|
572
|
+
document.addEventListener('keydown', e => {
|
|
573
|
+
if (overlay.style.display !== 'none' && e.key === 'Escape') {
|
|
574
|
+
e.stopPropagation();
|
|
575
|
+
close();
|
|
576
|
+
}
|
|
577
|
+
}, true);
|
|
578
|
+
|
|
579
|
+
copyBtn.addEventListener('click', () => {
|
|
580
|
+
writeToClipboard(code.textContent).then(() => {
|
|
581
|
+
copyBtn.textContent = '✓ copied';
|
|
582
|
+
copyBtn.classList.add('copied');
|
|
583
|
+
setTimeout(() => {
|
|
584
|
+
copyBtn.textContent = 'copy';
|
|
585
|
+
copyBtn.classList.remove('copied');
|
|
586
|
+
}, 1500);
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
applyView(); // initialise font-size, theme class and toggle label
|
|
591
|
+
|
|
592
|
+
modalRefs = { overlay, title, code, body, close, applyView };
|
|
593
|
+
return modalRefs;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function openPreview(filePath, fileName, ext) {
|
|
597
|
+
const { overlay, title, code, body, applyView } = ensureModal();
|
|
598
|
+
title.textContent = fileName;
|
|
599
|
+
code.textContent = 'Loading…';
|
|
600
|
+
// Reset highlight state so the reused element can be re-highlighted.
|
|
601
|
+
code.removeAttribute('class');
|
|
602
|
+
code.removeAttribute('data-highlighted');
|
|
603
|
+
applyView(); // re-apply font-size/theme (class reset above clears nothing inline, but keeps them in sync)
|
|
604
|
+
body.scrollTop = 0;
|
|
605
|
+
|
|
606
|
+
// Pause reveal navigation so arrow keys scroll the code, not the slides.
|
|
607
|
+
deck.configure({ keyboard: false });
|
|
608
|
+
overlay.style.display = 'flex';
|
|
609
|
+
|
|
610
|
+
fetch(filePath, { credentials: 'include' })
|
|
611
|
+
.then(r => {
|
|
612
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
613
|
+
return r.text();
|
|
614
|
+
})
|
|
615
|
+
.then(text => {
|
|
616
|
+
code.textContent = text;
|
|
617
|
+
const hljs = getHljs();
|
|
618
|
+
const lang = hljsLanguage(fileName.toLowerCase() === 'dockerfile' ? 'dockerfile' : ext);
|
|
619
|
+
if (hljs) {
|
|
620
|
+
// Hint the language when known; otherwise let hljs auto-detect.
|
|
621
|
+
if (lang) code.classList.add('language-' + lang);
|
|
622
|
+
try { hljs.highlightElement(code); } catch (e) { /* leave as plain text */ }
|
|
623
|
+
}
|
|
624
|
+
})
|
|
625
|
+
.catch(err => { code.textContent = 'Failed to load file: ' + err.message; });
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function makePreviewButton(filePath, fileName, ext) {
|
|
629
|
+
const btn = document.createElement('button');
|
|
630
|
+
btn.classList.add('dirtree-preview-btn');
|
|
631
|
+
btn.title = 'Preview file contents';
|
|
632
|
+
btn.textContent = 'view';
|
|
633
|
+
btn.addEventListener('click', e => {
|
|
634
|
+
e.preventDefault();
|
|
635
|
+
openPreview(filePath, fileName, ext);
|
|
636
|
+
});
|
|
637
|
+
return btn;
|
|
638
|
+
}
|
|
639
|
+
|
|
227
640
|
function getFileList(el) {
|
|
228
641
|
return el.innerText.split('\n').filter(e => e.trim().length > 0);
|
|
229
642
|
}
|
|
@@ -346,6 +759,7 @@ const dirTreeFactory = ({ zip, strToU8 }) => ({
|
|
|
346
759
|
|
|
347
760
|
li.appendChild(icon);
|
|
348
761
|
li.appendChild(a);
|
|
762
|
+
li.appendChild(makePreviewButton(finalPath, o, ext));
|
|
349
763
|
li.appendChild(makeCopyButton(finalPath));
|
|
350
764
|
el.appendChild(li);
|
|
351
765
|
}
|
|
@@ -10,9 +10,9 @@ Parameters:
|
|
|
10
10
|
|
|
11
11
|
Example
|
|
12
12
|
|
|
13
|
-
<a data-code='bash'
|
|
14
|
-
data-begin="# Begin indicator in the file"
|
|
15
|
-
data-end="# End indicator in the file"
|
|
13
|
+
<a data-code='bash'
|
|
14
|
+
data-begin="# Begin indicator in the file"
|
|
15
|
+
data-end="# End indicator in the file"
|
|
16
16
|
data-link
|
|
17
17
|
href="link/to/the/file.sh">Source code</a>
|
|
18
18
|
*/
|
|
@@ -26,6 +26,9 @@ function showError(el, err) {
|
|
|
26
26
|
function showCode(el, language, code, link, outdent) {
|
|
27
27
|
var newEl = document.createElement('pre');
|
|
28
28
|
newEl.setAttribute('class', `language-${language}`);
|
|
29
|
+
// Keep the exact, un-escaped source so the copy button can reproduce it
|
|
30
|
+
// verbatim regardless of how we later restructure the DOM for display.
|
|
31
|
+
newEl.__rawCode = code
|
|
29
32
|
//Replace special chars
|
|
30
33
|
code = code.replace(/&/g, "&").replace(/>/g, ">").replace(/</g, "<").replace(/"/g, """)
|
|
31
34
|
|
|
@@ -82,6 +85,43 @@ function injectStyles() {
|
|
|
82
85
|
pre.with-copy-btn {
|
|
83
86
|
position: relative;
|
|
84
87
|
}
|
|
88
|
+
/* Soft-wrap long lines like VS Code's word wrap. white-space:pre-wrap is
|
|
89
|
+
a purely visual wrap: it does NOT insert real newlines into the text.
|
|
90
|
+
overflow-wrap:anywhere lets very long tokens / URLs break instead of
|
|
91
|
+
overflowing the slide. Copy fidelity is handled separately (see
|
|
92
|
+
addCopyButton / wrapCodeLines), so wrapping never leaks into copied text. */
|
|
93
|
+
.reveal pre.with-copy-btn,
|
|
94
|
+
.reveal pre.with-copy-btn code {
|
|
95
|
+
white-space: pre-wrap;
|
|
96
|
+
overflow-wrap: anywhere;
|
|
97
|
+
word-break: normal;
|
|
98
|
+
}
|
|
99
|
+
/* Each logical source line becomes its own block so wrapped continuation
|
|
100
|
+
rows can be hang-indented (text-indent pulls the first row back to the
|
|
101
|
+
margin; padding-left indents everything else). */
|
|
102
|
+
.reveal pre.with-copy-btn code .cl {
|
|
103
|
+
display: block;
|
|
104
|
+
position: relative;
|
|
105
|
+
padding-left: 2.5ch;
|
|
106
|
+
text-indent: -2.5ch;
|
|
107
|
+
}
|
|
108
|
+
/* Preserve the height of blank lines. Generated content is excluded from
|
|
109
|
+
selection/copy, so the zero-width space never reaches the clipboard. */
|
|
110
|
+
.reveal pre.with-copy-btn code .cl:empty::before {
|
|
111
|
+
content: "\\200b";
|
|
112
|
+
}
|
|
113
|
+
/* Continuation marker placed at each soft-wrap point (see wrapCodeLines).
|
|
114
|
+
user-select:none keeps it out of native select+copy. */
|
|
115
|
+
.reveal pre.with-copy-btn code .wrap-marker {
|
|
116
|
+
position: absolute;
|
|
117
|
+
left: 0;
|
|
118
|
+
text-indent: 0;
|
|
119
|
+
font-style: normal;
|
|
120
|
+
opacity: 0.4;
|
|
121
|
+
pointer-events: none;
|
|
122
|
+
user-select: none;
|
|
123
|
+
-webkit-user-select: none;
|
|
124
|
+
}
|
|
85
125
|
.copy-code-btn {
|
|
86
126
|
position: absolute;
|
|
87
127
|
top: 0.4em;
|
|
@@ -114,6 +154,80 @@ function injectStyles() {
|
|
|
114
154
|
document.head.appendChild(style);
|
|
115
155
|
}
|
|
116
156
|
|
|
157
|
+
// Wrap each logical line of a (already highlighted) <code> element in its own
|
|
158
|
+
// <span class="cl"> block. hljs emits a single run of HTML with "\n"
|
|
159
|
+
// separators and token <span>s that may straddle newlines, so we tokenize the
|
|
160
|
+
// markup and re-open any spans still active when a line ends.
|
|
161
|
+
function wrapCodeLines(codeEl) {
|
|
162
|
+
const html = codeEl.innerHTML
|
|
163
|
+
const tokenRe = /(<\/?[^>]+>)|([^<]+)/g
|
|
164
|
+
let m
|
|
165
|
+
let open = [] // stack of currently-open tag strings (hljs uses <span>)
|
|
166
|
+
let buf = ''
|
|
167
|
+
let lines = []
|
|
168
|
+
const closers = () => open.map(() => '</span>').join('')
|
|
169
|
+
const reopen = () => open.join('')
|
|
170
|
+
|
|
171
|
+
while ((m = tokenRe.exec(html)) !== null) {
|
|
172
|
+
if (m[1]) {
|
|
173
|
+
const tag = m[1]
|
|
174
|
+
if (tag[1] === '/') { open.pop(); buf += tag }
|
|
175
|
+
else if (tag.endsWith('/>')) { buf += tag } // self-closing (rare)
|
|
176
|
+
else { open.push(tag); buf += tag }
|
|
177
|
+
} else {
|
|
178
|
+
const parts = m[2].split('\n')
|
|
179
|
+
for (let i = 0; i < parts.length; i++) {
|
|
180
|
+
if (i > 0) {
|
|
181
|
+
buf += closers() // close spans before the line break
|
|
182
|
+
lines.push(buf)
|
|
183
|
+
buf = reopen() // re-open them on the next line
|
|
184
|
+
}
|
|
185
|
+
buf += parts[i]
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
buf += closers()
|
|
190
|
+
lines.push(buf)
|
|
191
|
+
|
|
192
|
+
codeEl.innerHTML = lines.map(l => `<span class="cl">${l}</span>`).join('')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Place a "↪" marker at the start of every wrapped continuation row. CSS has no
|
|
196
|
+
// selector for soft-wrap rows, so we measure them with a Range. The deck is
|
|
197
|
+
// rendered at a fixed layout width and merely CSS-scaled, so wrap positions are
|
|
198
|
+
// stable; we divide measured offsets by the current scale to get layout pixels.
|
|
199
|
+
function decorateWrappedLines(root, scale) {
|
|
200
|
+
if (!root) return
|
|
201
|
+
const s = scale || 1
|
|
202
|
+
for (const line of root.querySelectorAll('pre.with-copy-btn code .cl')) {
|
|
203
|
+
// Idempotent: clear any decoration from a previous pass before measuring.
|
|
204
|
+
for (const old of line.querySelectorAll('.wrap-marker')) old.remove()
|
|
205
|
+
|
|
206
|
+
const range = document.createRange()
|
|
207
|
+
range.selectNodeContents(line)
|
|
208
|
+
const rects = range.getClientRects()
|
|
209
|
+
if (!rects.length) continue
|
|
210
|
+
|
|
211
|
+
const base = line.getBoundingClientRect()
|
|
212
|
+
const tops = []
|
|
213
|
+
for (const r of rects) {
|
|
214
|
+
if (r.width === 0 && r.height === 0) continue
|
|
215
|
+
const t = Math.round(r.top - base.top)
|
|
216
|
+
if (!tops.length || Math.abs(tops[tops.length - 1] - t) > 2) tops.push(t)
|
|
217
|
+
}
|
|
218
|
+
if (tops.length <= 1) continue // line did not wrap
|
|
219
|
+
|
|
220
|
+
for (let i = 1; i < tops.length; i++) {
|
|
221
|
+
const marker = document.createElement('span')
|
|
222
|
+
marker.className = 'wrap-marker'
|
|
223
|
+
marker.textContent = '↪'
|
|
224
|
+
marker.setAttribute('aria-hidden', 'true')
|
|
225
|
+
marker.style.top = (tops[i] / s) + 'px'
|
|
226
|
+
line.appendChild(marker)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
117
231
|
function writeToClipboard(text) {
|
|
118
232
|
if (navigator.clipboard && navigator.clipboard.writeText)
|
|
119
233
|
return navigator.clipboard.writeText(text);
|
|
@@ -140,7 +254,13 @@ function addCopyButton(preEl) {
|
|
|
140
254
|
btn.textContent = 'copy';
|
|
141
255
|
btn.addEventListener('click', () => {
|
|
142
256
|
const code = preEl.querySelector('code');
|
|
143
|
-
|
|
257
|
+
// Prefer the stashed raw source: it is immune to the per-line <span>
|
|
258
|
+
// wrapping and wrap markers used for display. textContent is the
|
|
259
|
+
// fallback (block-per-line collapses to no newlines under textContent,
|
|
260
|
+
// so raw is what we really want here).
|
|
261
|
+
const text = preEl.__rawCode != null
|
|
262
|
+
? preEl.__rawCode
|
|
263
|
+
: (code ? code.textContent : preEl.textContent);
|
|
144
264
|
writeToClipboard(text).then(() => {
|
|
145
265
|
btn.textContent = '✓ copied';
|
|
146
266
|
btn.classList.add('copied');
|
|
@@ -162,6 +282,17 @@ export default () => {
|
|
|
162
282
|
init: (deck) => {
|
|
163
283
|
injectStyles();
|
|
164
284
|
|
|
285
|
+
// Re-measure wrap points after fonts settle and on every layout change.
|
|
286
|
+
// Only the visible slide can be measured (hidden slides have no layout),
|
|
287
|
+
// so slidechanged re-runs decoration as each slide is shown.
|
|
288
|
+
const decorate = (root) => {
|
|
289
|
+
const run = () => decorateWrappedLines(root, deck.getScale());
|
|
290
|
+
if (document.fonts && document.fonts.ready)
|
|
291
|
+
document.fonts.ready.then(() => requestAnimationFrame(run));
|
|
292
|
+
else
|
|
293
|
+
requestAnimationFrame(run);
|
|
294
|
+
};
|
|
295
|
+
|
|
165
296
|
deck.on('ready', async () => {
|
|
166
297
|
const highlightPlugin = deck.getPlugin("highlight")
|
|
167
298
|
|
|
@@ -178,8 +309,12 @@ export default () => {
|
|
|
178
309
|
// code text and overwrites the rendered diagram.
|
|
179
310
|
for (let el of deck.getRevealElement().querySelectorAll("pre code[class]")) {
|
|
180
311
|
if (!el.classList.contains("mermaid")) {
|
|
312
|
+
const pre = el.closest('pre')
|
|
313
|
+
// Capture raw source before highlighting/line-wrapping touches the DOM.
|
|
314
|
+
if (pre) pre.__rawCode = el.textContent
|
|
181
315
|
highlightPlugin.hljs.highlightElement(el);
|
|
182
|
-
|
|
316
|
+
wrapCodeLines(el);
|
|
317
|
+
addCopyButton(pre);
|
|
183
318
|
}
|
|
184
319
|
}
|
|
185
320
|
|
|
@@ -225,7 +360,9 @@ export default () => {
|
|
|
225
360
|
|
|
226
361
|
const newEl = showCode(el, language, code, showLink ? url : null, outdent)
|
|
227
362
|
if (language !== 'mermaid') {
|
|
228
|
-
|
|
363
|
+
const codeEl = newEl.querySelector('code') ?? newEl
|
|
364
|
+
highlightPlugin.hljs.highlightElement(codeEl)
|
|
365
|
+
wrapCodeLines(codeEl)
|
|
229
366
|
addCopyButton(newEl)
|
|
230
367
|
}
|
|
231
368
|
} catch (err) {
|
|
@@ -234,10 +371,15 @@ export default () => {
|
|
|
234
371
|
}
|
|
235
372
|
}
|
|
236
373
|
|
|
237
|
-
|
|
374
|
+
// First pass for the slide that is visible at startup.
|
|
375
|
+
decorate(deck.getRevealElement())
|
|
238
376
|
})
|
|
239
377
|
|
|
240
|
-
|
|
378
|
+
// Slides other than the current one have no layout while hidden, so we
|
|
379
|
+
// (re)place wrap markers each time a slide becomes visible, and after
|
|
380
|
+
// any resize that changes the scale.
|
|
381
|
+
deck.on('slidechanged', (e) => decorate(e.currentSlide))
|
|
382
|
+
deck.on('resize', () => decorate(deck.getRevealElement()))
|
|
241
383
|
}
|
|
242
384
|
}
|
|
243
|
-
}
|
|
385
|
+
}
|