@farberg/reveal-template 1.1.18 → 1.1.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farberg/reveal-template",
3
- "version": "1.1.18",
3
+ "version": "1.1.19",
4
4
  "homepage": "https://github.com/pfisterer/reveal-template",
5
5
  "description": "Reveal.js template for Dennis' lectures",
6
6
  "main": "index.js",
@@ -1,154 +1,383 @@
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
17
  const dir_tree = {
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
+ }
33
48
 
34
- if (!nextNode) {
35
- nextNode = {}
36
- tree[path[0]] = nextNode
49
+ li.dirtree {
50
+ color: #1f2328;
51
+ position: relative;
52
+ white-space: nowrap;
53
+ display: flex;
54
+ align-items: center;
37
55
  }
38
56
 
39
- const nextPath = path.slice(1)
40
- if (nextPath.length > 0) {
41
- return getPath(nextNode, nextPath)
57
+ li.dirtree::before,
58
+ li.dirtree::after {
59
+ display: none !important;
60
+ content: none !important;
61
+ border: none !important;
42
62
  }
43
63
 
44
- return nextNode
45
- }
64
+ li.dirtree.root {
65
+ color: #0969da;
66
+ font-weight: 600;
67
+ margin-bottom: 2px;
68
+ }
46
69
 
47
- let tree = {}
48
- for (const file of files) {
49
- const path = file.split('/')
50
- const filename = path.pop()
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
+ }
144
+
145
+ .dirtree-prefix {
146
+ color: #8c959f;
147
+ user-select: none;
148
+ white-space: pre;
149
+ flex-shrink: 0;
150
+ }
151
+
152
+ .dirtree-icon {
153
+ margin-right: 4px;
154
+ flex-shrink: 0;
155
+ }
156
+ `;
157
+ document.head.appendChild(style);
158
+ }
51
159
 
52
- const treeNode = getPath(tree, path);
53
- treeNode[filename] = filename
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);
54
179
  }
55
- return tree
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
+ }
56
196
 
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
+ }
230
+
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;
243
+ }
244
+ return tree;
57
245
  }
58
246
 
59
247
  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");
248
+ Promise.all(files.map(f => fetch(f, { credentials: 'include' })))
249
+ .then(responses => {
250
+ if (responses.some(r => r.status === 401)) {
66
251
  window.location.reload();
67
252
  return;
68
253
  }
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
-
254
+ return Promise.all(responses.map(r => r.text())).then(values => {
255
+ const zip = new JSZip();
256
+ values.forEach((text, i) => zip.file(files[i], text));
257
+ zip.generateAsync({ type: 'blob' }).then(blob => saveAs(blob, zipName));
258
+ });
259
+ });
79
260
  }
80
261
 
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"
262
+ function displayTree(el, treeNode, path, isRoot, files, zipName, ancestorLast) {
263
+ const names = Object.getOwnPropertyNames(treeNode);
86
264
 
87
- if (isDir) {
88
- const li = document.createElement('li')
89
- li.classList.add("dirtree")
90
- el.appendChild(li)
265
+ names.forEach((name, index) => {
266
+ const o = treeNode[name];
267
+ const isDir = typeof o === 'object';
268
+ const isLast = index === names.length - 1;
91
269
 
92
- if (isFirst) {
93
- li.classList.add("root")
94
- li.innerHTML = name + " "
270
+ const li = document.createElement('li');
271
+ li.classList.add('dirtree');
95
272
 
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
- })
273
+ if (isRoot) {
274
+ li.classList.add('root');
105
275
 
276
+ const icon = document.createElement('span');
277
+ icon.classList.add('dirtree-icon');
278
+ icon.textContent = '📁';
106
279
 
107
- } else {
108
- li.innerHTML = name
109
- }
280
+ const nameSpan = document.createElement('span');
281
+ nameSpan.classList.add('dirtree-dir-name');
282
+ nameSpan.textContent = name;
110
283
 
111
- const ul = document.createElement('ul')
112
- ul.classList.add("dirtree")
113
- li.appendChild(ul)
284
+ li.appendChild(icon);
285
+ li.appendChild(nameSpan);
286
+ el.appendChild(li);
114
287
 
115
- displayTree(ul, o, `${path}/${name}`, false, files, zipName)
288
+ const ul = document.createElement('ul');
289
+ ul.classList.add('dirtree');
290
+ li.appendChild(ul);
291
+ displayTree(ul, o, `${path}/${name}`, false, files, zipName, []);
116
292
 
117
293
  } else {
294
+ const contPrefix = ancestorLast.map(last => last ? ' ' : '│ ').join('');
295
+ const connector = isLast ? '└── ' : '├── ';
296
+
297
+ const prefixSpan = document.createElement('span');
298
+ prefixSpan.classList.add('dirtree-prefix');
299
+ prefixSpan.textContent = contPrefix + connector;
300
+ li.appendChild(prefixSpan);
301
+
302
+ if (isDir) {
303
+ const icon = document.createElement('span');
304
+ icon.classList.add('dirtree-icon');
305
+ icon.textContent = '📂';
118
306
 
119
- const li = document.createElement('li')
120
- li.classList.add("dirtree")
307
+ const nameSpan = document.createElement('span');
308
+ nameSpan.classList.add('dirtree-dir-name');
309
+ nameSpan.textContent = name;
121
310
 
122
- //Remove trailing slash
123
- const finalPath = `${path}/${o}`.replace(/^\//, "");
124
- li.innerHTML = `<a href="${finalPath}">${o}</a>`
311
+ li.appendChild(icon);
312
+ li.appendChild(nameSpan);
313
+ el.appendChild(li);
125
314
 
126
- el.appendChild(li)
315
+ const ul = document.createElement('ul');
316
+ ul.classList.add('dirtree');
317
+ li.appendChild(ul);
318
+ displayTree(ul, o, `${path}/${name}`, false, files, zipName, [...ancestorLast, isLast]);
319
+
320
+ } else {
321
+ const ext = name.includes('.') ? name.split('.').pop().toLowerCase() : '';
322
+
323
+ const icon = document.createElement('span');
324
+ icon.classList.add('dirtree-icon');
325
+ icon.textContent = getFileIcon(name, ext);
326
+
327
+ const finalPath = `${path}/${o}`.replace(/^\//, '');
328
+ const a = document.createElement('a');
329
+ a.classList.add('dirtree-file-link');
330
+ a.href = finalPath;
331
+ a.textContent = o;
332
+
333
+ li.appendChild(icon);
334
+ li.appendChild(a);
335
+ li.appendChild(makeCopyButton(finalPath));
336
+ el.appendChild(li);
337
+ }
127
338
  }
128
- }
339
+ });
129
340
  }
130
341
 
131
342
  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")
343
+ injectStyles();
135
344
 
136
- const files = getFileList(el)
137
- const tree = createTree(files)
345
+ for (const el of deck.getRevealElement().querySelectorAll('pre.dirtree')) {
346
+ const zipName = el.getAttribute('data-zipname') || 'download.zip';
347
+ const lineHeight = el.getAttribute('data-line-height') || '1.85';
348
+ const width = el.getAttribute('data-width') || 'auto';
138
349
 
139
- const ul = document.createElement('ul')
140
- ul.classList.add("dirtree")
350
+ const files = getFileList(el);
351
+ const tree = createTree(files);
141
352
 
142
- displayTree(ul, tree, '', true, files, zipName)
353
+ const container = document.createElement('div');
354
+ container.classList.add('dirtree-container');
355
+ container.style.lineHeight = lineHeight;
356
+ if (width !== 'auto') container.style.width = width;
143
357
 
144
- el.parentElement.replaceChild(ul, el)
145
- }
358
+ // Forward font-size (and other inline styles) from the <pre>
359
+ if (el.getAttribute('style')) container.setAttribute('style', el.getAttribute('style'));
360
+ // Re-apply data-driven styles on top (inline style from <pre> must not override these)
361
+ container.style.lineHeight = lineHeight;
362
+ if (width !== 'auto') container.style.width = width;
363
+
364
+ const btn = document.createElement('a');
365
+ btn.classList.add('dirtree-download-btn');
366
+ btn.textContent = '⬇ ' + zipName;
367
+ btn.href = '#zip/' + zipName;
368
+ btn.addEventListener('click', e => { e.preventDefault(); download(files, zipName); });
369
+ container.appendChild(btn);
146
370
 
147
- })
371
+ const ul = document.createElement('ul');
372
+ ul.classList.add('dirtree');
373
+ container.appendChild(ul);
148
374
 
375
+ displayTree(ul, tree, '', true, files, zipName, []);
149
376
 
377
+ el.parentElement.replaceChild(container, el);
378
+ }
379
+ });
150
380
  }
151
381
  }
152
382
 
153
-
154
- export default dir_tree;
383
+ export default dir_tree;
@@ -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}`)