@farberg/reveal-template 1.1.18 → 1.1.20

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.
@@ -1,154 +1,397 @@
1
1
  /*
2
2
  Create a zip from existing files for download and display a tree of files
3
3
 
4
- <pre class="dirtree" data-zipname="k8s-Exercise.zip" data-strip-prefix="code/k8s-demo-app">
4
+ Attributes on <pre class="dirtree">:
5
+ data-zipname="archive.zip" — name of the downloaded zip (default: download.zip)
6
+ data-line-height="1.6" — line spacing (default: 1.85)
7
+ data-width="auto" — container width, e.g. "400px", "60%", "auto" (default: auto)
8
+
9
+ <pre class="dirtree" data-zipname="k8s-Exercise.zip" data-line-height="1.6" data-width="480px">
5
10
  code/k8s-demo-app/01-mariadb-deployment.yaml
6
11
  code/k8s-demo-app/02-k8s-mariadb-service.yaml
7
- code/k8s-demo-app/03-memcached-deployment-and-service.yaml
8
- code/k8s-demo-app/04-app-deployment-minikube.yaml
9
- code/k8s-demo-app/04-app-service-and-ingress.yaml
10
12
  code/k8s-demo-app/Dockerfile
11
13
  code/k8s-demo-app/package.json
12
- code/k8s-demo-app/server.js
13
14
  </pre>
14
-
15
-
16
-
17
15
  */
18
16
 
19
- const dir_tree = {
17
+ const dirTreeFactory = ({ zip, strToU8 }) => ({
20
18
  id: 'dir_tree',
21
19
  init: (deck) => {
22
20
 
23
- function getFileList(el) {
24
- const text = el.innerText;
25
- const files = text.split('\n').filter(e => e.trim().length > 0);
26
- return files
27
- }
21
+ function injectStyles() {
22
+ if (document.getElementById('dirtree-styles')) return;
23
+ const style = document.createElement('style');
24
+ style.id = 'dirtree-styles';
25
+ style.textContent = `
26
+ .dirtree-container {
27
+ font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace;
28
+ background: transparent;
29
+ border-radius: 10px;
30
+ padding: 38px 18px 14px 18px;
31
+ border: 1px solid #d0d7de;
32
+ display: inline-block;
33
+ min-width: 280px;
34
+ max-width: 90%;
35
+ text-align: left;
36
+ position: relative !important;
37
+ }
28
38
 
29
- function createTree(files) {
39
+ ul.dirtree {
40
+ list-style: none !important;
41
+ padding: 0 !important;
42
+ margin: 0 !important;
43
+ }
30
44
 
31
- function getPath(tree, path) {
32
- let nextNode = tree[path[0]]
45
+ ul.dirtree ul.dirtree {
46
+ padding-left: 0 !important;
47
+ }
48
+
49
+ li.dirtree {
50
+ color: #1f2328;
51
+ position: relative;
52
+ white-space: nowrap;
53
+ display: flex;
54
+ align-items: center;
55
+ }
56
+
57
+ li.dirtree::before,
58
+ li.dirtree::after {
59
+ display: none !important;
60
+ content: none !important;
61
+ border: none !important;
62
+ }
63
+
64
+ li.dirtree.root {
65
+ color: #0969da;
66
+ font-weight: 600;
67
+ margin-bottom: 2px;
68
+ }
69
+
70
+ .dirtree-dir-name {
71
+ color: #0969da;
72
+ }
73
+
74
+ a.dirtree-file-link {
75
+ color: #116329;
76
+ text-decoration: none;
77
+ transition: color 0.15s;
78
+ }
79
+
80
+ a.dirtree-file-link:hover {
81
+ color: #6639ba;
82
+ text-decoration: underline;
83
+ }
84
+
85
+ /* Copy button — hidden by default, shown on li hover */
86
+ .dirtree-copy-btn {
87
+ display: inline-flex;
88
+ align-items: center;
89
+ margin-left: 8px;
90
+ padding: 0px 6px;
91
+ background: transparent;
92
+ border: 1px solid #d0d7de;
93
+ border-radius: 4px;
94
+ color: #57606a;
95
+ font-size: 0.78em;
96
+ cursor: pointer;
97
+ opacity: 0 !important;
98
+ pointer-events: none !important;
99
+ transition: opacity 0.15s, background 0.15s, color 0.15s;
100
+ user-select: none;
101
+ line-height: 1.6;
102
+ font-family: inherit;
103
+ }
104
+
105
+ li.dirtree:hover > .dirtree-copy-btn {
106
+ opacity: 1 !important;
107
+ pointer-events: auto !important;
108
+ }
109
+
110
+ .dirtree-copy-btn:hover {
111
+ background: #f6f8fa;
112
+ border-color: #8c959f;
113
+ color: #1f2328;
114
+ }
115
+
116
+ .dirtree-copy-btn.copied {
117
+ color: #116329;
118
+ border-color: #116329;
119
+ background: #dafbe1;
120
+ }
121
+
122
+ /* Download button */
123
+ .dirtree-download-btn {
124
+ position: absolute !important;
125
+ top: 10px !important;
126
+ right: 14px !important;
127
+ padding: 2px 10px;
128
+ background: transparent;
129
+ border: 1px solid #d0d7de;
130
+ border-radius: 6px;
131
+ color: #cf222e !important;
132
+ font-size: 0.85em;
133
+ cursor: pointer;
134
+ text-decoration: none !important;
135
+ transition: background 0.15s, border-color 0.15s;
136
+ font-family: inherit;
137
+ }
138
+
139
+ .dirtree-download-btn:hover {
140
+ background: #fff5f5 !important;
141
+ border-color: #cf222e !important;
142
+ color: #cf222e !important;
143
+ }
33
144
 
34
- if (!nextNode) {
35
- nextNode = {}
36
- tree[path[0]] = nextNode
145
+ .dirtree-prefix {
146
+ color: #8c959f;
147
+ user-select: none;
148
+ white-space: pre;
149
+ flex-shrink: 0;
37
150
  }
38
151
 
39
- const nextPath = path.slice(1)
40
- if (nextPath.length > 0) {
41
- return getPath(nextNode, nextPath)
152
+ .dirtree-icon {
153
+ margin-right: 4px;
154
+ flex-shrink: 0;
42
155
  }
156
+ `;
157
+ document.head.appendChild(style);
158
+ }
43
159
 
44
- return nextNode
160
+ function getFileIcon(name, ext) {
161
+ if (name.toLowerCase() === 'dockerfile') return '🐳';
162
+ const icons = {
163
+ js: '📜', ts: '📜', jsx: '📜', tsx: '📜',
164
+ json: '📋', yaml: '📋', yml: '📋', toml: '📋', xml: '📋',
165
+ md: '📝', txt: '📝', rst: '📝',
166
+ html: '🌐', css: '🎨', scss: '🎨',
167
+ sh: '⚙️', bash: '⚙️', zsh: '⚙️',
168
+ py: '🐍', rb: '💎', go: '🔷', rs: '🦀', java: '☕',
169
+ png: '🖼️', jpg: '🖼️', jpeg: '🖼️', svg: '🖼️', gif: '🖼️',
170
+ zip: '📦', tar: '📦', gz: '📦',
171
+ sql: '🗄️',
172
+ };
173
+ return icons[ext] || '📄';
174
+ }
175
+
176
+ function writeToClipboard(text) {
177
+ if (navigator.clipboard && navigator.clipboard.writeText) {
178
+ return navigator.clipboard.writeText(text);
45
179
  }
180
+ // Fallback for HTTP / non-secure contexts
181
+ const ta = document.createElement('textarea');
182
+ ta.value = text;
183
+ ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none';
184
+ document.body.appendChild(ta);
185
+ ta.focus();
186
+ ta.select();
187
+ try {
188
+ document.execCommand('copy');
189
+ return Promise.resolve();
190
+ } catch (e) {
191
+ return Promise.reject(e);
192
+ } finally {
193
+ document.body.removeChild(ta);
194
+ }
195
+ }
46
196
 
47
- let tree = {}
48
- for (const file of files) {
49
- const path = file.split('/')
50
- const filename = path.pop()
197
+ function makeCopyButton(filePath) {
198
+ const btn = document.createElement('button');
199
+ btn.classList.add('dirtree-copy-btn');
200
+ btn.title = 'Copy file contents';
201
+ btn.textContent = 'copy';
202
+ btn.addEventListener('click', e => {
203
+ e.preventDefault();
204
+ btn.textContent = '…';
205
+ fetch(filePath, { credentials: 'include' })
206
+ .then(r => {
207
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
208
+ return r.text();
209
+ })
210
+ .then(text => writeToClipboard(text))
211
+ .then(() => {
212
+ btn.textContent = '✓ copied';
213
+ btn.classList.add('copied');
214
+ setTimeout(() => {
215
+ btn.textContent = 'copy';
216
+ btn.classList.remove('copied');
217
+ }, 1500);
218
+ })
219
+ .catch(() => {
220
+ btn.textContent = '✗ error';
221
+ setTimeout(() => { btn.textContent = 'copy'; }, 1500);
222
+ });
223
+ });
224
+ return btn;
225
+ }
226
+
227
+ function getFileList(el) {
228
+ return el.innerText.split('\n').filter(e => e.trim().length > 0);
229
+ }
51
230
 
52
- const treeNode = getPath(tree, path);
53
- treeNode[filename] = filename
231
+ function createTree(files) {
232
+ function getPath(tree, path) {
233
+ let node = tree[path[0]];
234
+ if (!node) { node = {}; tree[path[0]] = node; }
235
+ const rest = path.slice(1);
236
+ return rest.length > 0 ? getPath(node, rest) : node;
237
+ }
238
+ const tree = {};
239
+ for (const file of files) {
240
+ const parts = file.split('/');
241
+ const filename = parts.pop();
242
+ getPath(tree, parts)[filename] = filename;
54
243
  }
55
- return tree
244
+ return tree;
245
+ }
56
246
 
247
+ function downloadBlob(blob, filename) {
248
+ const url = URL.createObjectURL(blob);
249
+ const a = document.createElement('a');
250
+ a.href = url;
251
+ a.download = filename;
252
+ document.body.appendChild(a);
253
+ a.click();
254
+ document.body.removeChild(a);
255
+ URL.revokeObjectURL(url);
57
256
  }
58
257
 
59
258
  function download(files, zipName) {
60
- Promise.all(files.map(file => fetch(file, { "credentials": "include" })))
61
- .then(promises => {
62
- // Check for 401 responses
63
- const hasUnauthorized = promises.some(p => p.status === 401);
64
- if (hasUnauthorized) {
65
- console.log("Authentication required (dir-tree), reloading page");
259
+ Promise.all(files.map(f => fetch(f, { credentials: 'include' })))
260
+ .then(responses => {
261
+ if (responses.some(r => r.status === 401)) {
66
262
  window.location.reload();
67
263
  return;
68
264
  }
69
- return Promise.all(promises.map(p => p.text()))
70
- .then(values => {
71
- var zip = new JSZip();
72
- values.forEach((text, idx) => zip.file(files[idx], text))
73
- zip.generateAsync({ type: "blob" }).then(blob => {
74
- saveAs(blob, zipName);
75
- })
76
- })
77
- })
78
-
265
+ return Promise.all(responses.map(r => r.text())).then(values => {
266
+ const entries = {};
267
+ values.forEach((text, i) => { entries[files[i]] = strToU8(text); });
268
+ zip(entries, (err, data) => {
269
+ if (err) { console.error('zip failed', err); return; }
270
+ downloadBlob(new Blob([data], { type: 'application/zip' }), zipName);
271
+ });
272
+ });
273
+ });
79
274
  }
80
275
 
81
- function displayTree(el, treeNode, path, isFirst, files, zipName) {
82
-
83
- for (let name of Object.getOwnPropertyNames(treeNode)) {
84
- const o = treeNode[name]
85
- const isDir = typeof o === "object"
276
+ function displayTree(el, treeNode, path, isRoot, files, zipName, ancestorLast) {
277
+ const names = Object.getOwnPropertyNames(treeNode);
86
278
 
87
- if (isDir) {
88
- const li = document.createElement('li')
89
- li.classList.add("dirtree")
90
- el.appendChild(li)
279
+ names.forEach((name, index) => {
280
+ const o = treeNode[name];
281
+ const isDir = typeof o === 'object';
282
+ const isLast = index === names.length - 1;
91
283
 
92
- if (isFirst) {
93
- li.classList.add("root")
94
- li.innerHTML = name + " "
284
+ const li = document.createElement('li');
285
+ li.classList.add('dirtree');
95
286
 
96
- const a = document.createElement('a')
97
- li.appendChild(a)
98
- a.innerHTML = `(download ${zipName})`
99
- a.href = "#zip/" + zipName
100
- a.style = "color: #e10217";
101
- a.addEventListener('click', e => {
102
- e.preventDefault()
103
- download(files, zipName)
104
- })
287
+ if (isRoot) {
288
+ li.classList.add('root');
105
289
 
290
+ const icon = document.createElement('span');
291
+ icon.classList.add('dirtree-icon');
292
+ icon.textContent = '📁';
106
293
 
107
- } else {
108
- li.innerHTML = name
109
- }
294
+ const nameSpan = document.createElement('span');
295
+ nameSpan.classList.add('dirtree-dir-name');
296
+ nameSpan.textContent = name;
110
297
 
111
- const ul = document.createElement('ul')
112
- ul.classList.add("dirtree")
113
- li.appendChild(ul)
298
+ li.appendChild(icon);
299
+ li.appendChild(nameSpan);
300
+ el.appendChild(li);
114
301
 
115
- displayTree(ul, o, `${path}/${name}`, false, files, zipName)
302
+ const ul = document.createElement('ul');
303
+ ul.classList.add('dirtree');
304
+ li.appendChild(ul);
305
+ displayTree(ul, o, `${path}/${name}`, false, files, zipName, []);
116
306
 
117
307
  } else {
308
+ const contPrefix = ancestorLast.map(last => last ? ' ' : '│ ').join('');
309
+ const connector = isLast ? '└── ' : '├── ';
310
+
311
+ const prefixSpan = document.createElement('span');
312
+ prefixSpan.classList.add('dirtree-prefix');
313
+ prefixSpan.textContent = contPrefix + connector;
314
+ li.appendChild(prefixSpan);
315
+
316
+ if (isDir) {
317
+ const icon = document.createElement('span');
318
+ icon.classList.add('dirtree-icon');
319
+ icon.textContent = '📂';
118
320
 
119
- const li = document.createElement('li')
120
- li.classList.add("dirtree")
321
+ const nameSpan = document.createElement('span');
322
+ nameSpan.classList.add('dirtree-dir-name');
323
+ nameSpan.textContent = name;
121
324
 
122
- //Remove trailing slash
123
- const finalPath = `${path}/${o}`.replace(/^\//, "");
124
- li.innerHTML = `<a href="${finalPath}">${o}</a>`
325
+ li.appendChild(icon);
326
+ li.appendChild(nameSpan);
327
+ el.appendChild(li);
125
328
 
126
- el.appendChild(li)
329
+ const ul = document.createElement('ul');
330
+ ul.classList.add('dirtree');
331
+ li.appendChild(ul);
332
+ displayTree(ul, o, `${path}/${name}`, false, files, zipName, [...ancestorLast, isLast]);
333
+
334
+ } else {
335
+ const ext = name.includes('.') ? name.split('.').pop().toLowerCase() : '';
336
+
337
+ const icon = document.createElement('span');
338
+ icon.classList.add('dirtree-icon');
339
+ icon.textContent = getFileIcon(name, ext);
340
+
341
+ const finalPath = `${path}/${o}`.replace(/^\//, '');
342
+ const a = document.createElement('a');
343
+ a.classList.add('dirtree-file-link');
344
+ a.href = finalPath;
345
+ a.textContent = o;
346
+
347
+ li.appendChild(icon);
348
+ li.appendChild(a);
349
+ li.appendChild(makeCopyButton(finalPath));
350
+ el.appendChild(li);
351
+ }
127
352
  }
128
- }
353
+ });
129
354
  }
130
355
 
131
356
  deck.on('ready', () => {
132
- for (let el of deck.getRevealElement().querySelectorAll("pre.dirtree")) {
133
- const zipName = el.getAttribute("data-zipname") || "download.zip"
134
- const stripPrefix = el.getAttribute("data-strip-prefix")
357
+ injectStyles();
135
358
 
136
- const files = getFileList(el)
137
- const tree = createTree(files)
359
+ for (const el of deck.getRevealElement().querySelectorAll('pre.dirtree')) {
360
+ const zipName = el.getAttribute('data-zipname') || 'download.zip';
361
+ const lineHeight = el.getAttribute('data-line-height') || '1.85';
362
+ const width = el.getAttribute('data-width') || 'auto';
138
363
 
139
- const ul = document.createElement('ul')
140
- ul.classList.add("dirtree")
364
+ const files = getFileList(el);
365
+ const tree = createTree(files);
141
366
 
142
- displayTree(ul, tree, '', true, files, zipName)
367
+ const container = document.createElement('div');
368
+ container.classList.add('dirtree-container');
369
+ container.style.lineHeight = lineHeight;
370
+ if (width !== 'auto') container.style.width = width;
143
371
 
144
- el.parentElement.replaceChild(ul, el)
145
- }
372
+ // Forward font-size (and other inline styles) from the <pre>
373
+ if (el.getAttribute('style')) container.setAttribute('style', el.getAttribute('style'));
374
+ // Re-apply data-driven styles on top (inline style from <pre> must not override these)
375
+ container.style.lineHeight = lineHeight;
376
+ if (width !== 'auto') container.style.width = width;
146
377
 
147
- })
378
+ const btn = document.createElement('a');
379
+ btn.classList.add('dirtree-download-btn');
380
+ btn.textContent = '⬇ ' + zipName;
381
+ btn.href = '#zip/' + zipName;
382
+ btn.addEventListener('click', e => { e.preventDefault(); download(files, zipName); });
383
+ container.appendChild(btn);
148
384
 
385
+ const ul = document.createElement('ul');
386
+ ul.classList.add('dirtree');
387
+ container.appendChild(ul);
149
388
 
150
- }
151
- }
389
+ displayTree(ul, tree, '', true, files, zipName, []);
152
390
 
391
+ el.parentElement.replaceChild(container, el);
392
+ }
393
+ });
394
+ }
395
+ })
153
396
 
154
- export default dir_tree;
397
+ export default dirTreeFactory;
@@ -105,21 +105,43 @@ function injectStyles() {
105
105
  document.head.appendChild(style);
106
106
  }
107
107
 
108
+ function writeToClipboard(text) {
109
+ if (navigator.clipboard && navigator.clipboard.writeText)
110
+ return navigator.clipboard.writeText(text);
111
+ const ta = document.createElement('textarea');
112
+ ta.value = text;
113
+ ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none';
114
+ document.body.appendChild(ta);
115
+ ta.focus();
116
+ ta.select();
117
+ try {
118
+ document.execCommand('copy');
119
+ return Promise.resolve();
120
+ } catch (e) {
121
+ return Promise.reject(e);
122
+ } finally {
123
+ document.body.removeChild(ta);
124
+ }
125
+ }
126
+
108
127
  function addCopyButton(preEl) {
109
128
  preEl.classList.add('with-copy-btn');
110
129
  const btn = document.createElement('button');
111
130
  btn.className = 'copy-code-btn';
112
- btn.textContent = 'Copy';
131
+ btn.textContent = 'copy';
113
132
  btn.addEventListener('click', () => {
114
133
  const code = preEl.querySelector('code');
115
134
  const text = code ? code.innerText : preEl.innerText;
116
- navigator.clipboard.writeText(text).then(() => {
117
- btn.textContent = 'Copied!';
135
+ writeToClipboard(text).then(() => {
136
+ btn.textContent = '✓ copied';
118
137
  btn.classList.add('copied');
119
138
  setTimeout(() => {
120
- btn.textContent = 'Copy';
139
+ btn.textContent = 'copy';
121
140
  btn.classList.remove('copied');
122
141
  }, 1500);
142
+ }).catch(() => {
143
+ btn.textContent = '✗ error';
144
+ setTimeout(() => { btn.textContent = 'copy'; }, 1500);
123
145
  });
124
146
  });
125
147
  preEl.appendChild(btn);
@@ -148,6 +170,7 @@ export default () => {
148
170
  for (let el of deck.getRevealElement().querySelectorAll("pre code[class]")) {
149
171
  if (!el.classList.contains("mermaid")) {
150
172
  highlightPlugin.hljs.highlightElement(el);
173
+ addCopyButton(el.closest('pre'));
151
174
  }
152
175
  }
153
176
 
@@ -191,8 +214,10 @@ export default () => {
191
214
  code = outdent(code)
192
215
 
193
216
  const newEl = showCode(el, language, code, showLink ? url : null, outdent)
194
- if (language !== 'mermaid')
217
+ if (language !== 'mermaid') {
195
218
  highlightPlugin.hljs.highlightElement(newEl.querySelector('code') ?? newEl)
219
+ addCopyButton(newEl)
220
+ }
196
221
  } catch (err) {
197
222
  console.error(`show-code-snippets: failed to load ${url}:`, err)
198
223
  showError(el, `Failed to load ${url}: ${err.message}`)
@@ -19,18 +19,17 @@
19
19
  */
20
20
 
21
21
 
22
- function showLinkToSlidesAndQrCode(deck, url) {
22
+ function showLinkToSlidesAndQrCode(QrCreator, deck, url) {
23
23
  if (url === undefined)
24
24
  return
25
25
 
26
26
  let qrElements = deck.getSlidesElement().getElementsByClassName('qrcodeforslides')
27
27
  for (let qrel of qrElements) {
28
28
 
29
- new QRCode(qrel, {
29
+ QrCreator.render({
30
30
  text: url,
31
- width: 500,
32
- height: 500,
33
- });
31
+ size: 500,
32
+ }, qrel);
34
33
 
35
34
  console.log("QR Code for slides generated", qrel, url)
36
35
  }
@@ -43,7 +42,7 @@ function showLinkToSlidesAndQrCode(deck, url) {
43
42
 
44
43
  }
45
44
 
46
- export default () => {
45
+ export default (QrCreator) => {
47
46
  return {
48
47
  id: 'show_qr_code',
49
48
  init: (deck) => {
@@ -60,7 +59,7 @@ export default () => {
60
59
  return res.json();
61
60
  })
62
61
  .then(json => {
63
- if (json) showLinkToSlidesAndQrCode(deck, json.homepage)
62
+ if (json) showLinkToSlidesAndQrCode(QrCreator, deck, json.homepage)
64
63
  })
65
64
  .catch(err => console.log("Error fetching info json", err))
66
65
  else
@@ -17,6 +17,7 @@ let md_files = fs.readdirSync(".")
17
17
  .filter(file => path.extname(file).toLowerCase() === ".md")
18
18
  .filter(file => !path.basename(file, ".md").startsWith("__"))
19
19
  .filter(file => path.basename(file, ".md") !== "README")
20
+ .filter(file => path.basename(file, ".md") !== "CLAUDE")
20
21
 
21
22
  let generateTable = md_files.map(file => file.match(structuredNameRegexp)).reduce((a, b) => a && b, true)
22
23