sdocs-dev 1.3.0 → 1.4.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/bin/sdocs-dev.js +192 -25
- package/package.json +9 -10
- package/public/sdocs-styles.js +162 -162
- package/public/brotli-wasm-v1.js +0 -518
- package/public/brotli_wasm_bg.wasm +0 -0
- package/public/css/layout.css +0 -240
- package/public/css/mobile.css +0 -196
- package/public/css/panel.css +0 -393
- package/public/css/rendered.css +0 -478
- package/public/css/tokens.css +0 -116
- package/public/css/write.css +0 -128
- package/public/fonts/inter-400.woff2 +0 -0
- package/public/fonts/inter-500.woff2 +0 -0
- package/public/fonts/inter-600.woff2 +0 -0
- package/public/images/examples.png +0 -0
- package/public/index.html +0 -694
- package/public/sdoc.md +0 -542
- package/public/sdocs-app.js +0 -812
- package/public/sdocs-charts.js +0 -905
- package/public/sdocs-controls.js +0 -354
- package/public/sdocs-export.js +0 -1090
- package/public/sdocs-state.js +0 -73
- package/public/sdocs-theme.js +0 -262
- package/public/sdocs-write.js +0 -740
- package/public/sw.js +0 -110
- package/public/vendor/marked.min.js +0 -6
- package/public/vendor/purify.min.js +0 -3
- package/server.js +0 -176
package/public/sdocs-export.js
DELETED
|
@@ -1,1090 +0,0 @@
|
|
|
1
|
-
// sdocs-export.js — Export pipeline: CSS, HTML, PDF, Word, save-default
|
|
2
|
-
(function () {
|
|
3
|
-
'use strict';
|
|
4
|
-
|
|
5
|
-
var S = SDocs;
|
|
6
|
-
|
|
7
|
-
function getCssVar(name) {
|
|
8
|
-
return (S.renderedEl.style.getPropertyValue(name) ||
|
|
9
|
-
getComputedStyle(S.renderedEl).getPropertyValue(name)).trim();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function buildExportCSS() {
|
|
13
|
-
var bgColor = getCssVar('--md-bg') || '#ffffff';
|
|
14
|
-
var fontFamily = getCssVar('--md-font-family') || "'Inter', sans-serif";
|
|
15
|
-
var baseSize = getCssVar('--md-base-size') || '16px';
|
|
16
|
-
var lineHeight = getCssVar('--md-line-height') || '1.75';
|
|
17
|
-
var color = getCssVar('--md-color') || '#1c1917';
|
|
18
|
-
|
|
19
|
-
var hFontFamily = getCssVar('--md-h-font-family') || 'inherit';
|
|
20
|
-
var hScale = parseFloat(getCssVar('--md-h-scale') || '1');
|
|
21
|
-
var hMB = getCssVar('--md-h-margin-bottom')|| '0.4em';
|
|
22
|
-
var hColor = getCssVar('--md-h-color') || '#0f0d0c';
|
|
23
|
-
|
|
24
|
-
var h1Size = 'calc(' + (getCssVar('--md-h1-size') || '2.1em') + ' * ' + hScale + ')';
|
|
25
|
-
var h1Color = getCssVar('--md-h1-color') || hColor;
|
|
26
|
-
var h1Weight = getCssVar('--md-h1-weight') || '700';
|
|
27
|
-
var h2Size = 'calc(' + (getCssVar('--md-h2-size') || '1.55em') + ' * ' + hScale + ')';
|
|
28
|
-
var h2Color = getCssVar('--md-h2-color') || hColor;
|
|
29
|
-
var h2Weight = getCssVar('--md-h2-weight') || '600';
|
|
30
|
-
var h3Size = 'calc(' + (getCssVar('--md-h3-size') || '1.2em') + ' * ' + hScale + ')';
|
|
31
|
-
var h3Color = getCssVar('--md-h3-color') || hColor;
|
|
32
|
-
var h3Weight = getCssVar('--md-h3-weight') || '600';
|
|
33
|
-
var h4Size = 'calc(' + (getCssVar('--md-h4-size') || '1.0em') + ' * ' + hScale + ')';
|
|
34
|
-
var h4Color = getCssVar('--md-h4-color') || hColor;
|
|
35
|
-
var h4Weight = getCssVar('--md-h4-weight') || '600';
|
|
36
|
-
|
|
37
|
-
var pColor = getCssVar('--md-p-color') || '#3c3733';
|
|
38
|
-
var pLH = getCssVar('--md-p-line-height')|| lineHeight;
|
|
39
|
-
var pMargin = getCssVar('--md-p-margin') || '0 0 1.1em';
|
|
40
|
-
|
|
41
|
-
var linkColor = getCssVar('--md-link-color') || '#2563eb';
|
|
42
|
-
var linkDec = getCssVar('--md-link-decoration') || 'underline';
|
|
43
|
-
|
|
44
|
-
var codeBG = getCssVar('--md-code-bg') || '#f4f1ed';
|
|
45
|
-
var codeColor = getCssVar('--md-code-color') || '#6b21a8';
|
|
46
|
-
var codeFont = getCssVar('--md-code-font') || "'JetBrains Mono', monospace";
|
|
47
|
-
var preBG = getCssVar('--md-pre-bg') || codeBG;
|
|
48
|
-
|
|
49
|
-
var bqBorder = getCssVar('--md-bq-border') || '3px solid #2563eb';
|
|
50
|
-
var bqBg = getCssVar('--md-bq-bg') || '#f7f5f2';
|
|
51
|
-
var bqColor = getCssVar('--md-bq-color') || '#6b6560';
|
|
52
|
-
var bqPad = getCssVar('--md-bq-padding') || '0.5em 1em';
|
|
53
|
-
var bqMargin = getCssVar('--md-bq-margin') || '1.2em 0';
|
|
54
|
-
|
|
55
|
-
var listColor = getCssVar('--md-list-color') || pColor;
|
|
56
|
-
|
|
57
|
-
return '\n*, *::before, *::after { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }\nbody {\n font-family: ' + fontFamily + ';\n font-size: ' + baseSize + ';\n color: ' + color + ';\n line-height: ' + lineHeight + ';\n background-color: ' + bgColor + ';\n max-width: 720px;\n margin: 0 auto;\n padding: 40px 48px 60px;\n -webkit-font-smoothing: antialiased;\n}\nh1 { font-family: ' + hFontFamily + '; font-size: ' + h1Size + '; color: ' + h1Color + '; font-weight: ' + h1Weight + '; letter-spacing: -0.02em; line-height: 1.2; margin: 0 0 ' + hMB + '; }\nh2 { font-family: ' + hFontFamily + '; font-size: ' + h2Size + '; color: ' + h2Color + '; font-weight: ' + h2Weight + '; letter-spacing: -0.015em; line-height: 1.3; margin: 1.4em 0 ' + hMB + '; padding-bottom: 0.3em; border-bottom: 1px solid #ede8e2; }\nh3 { font-family: ' + hFontFamily + '; font-size: ' + h3Size + '; color: ' + h3Color + '; font-weight: ' + h3Weight + '; letter-spacing: -0.01em; line-height: 1.4; margin: 1.2em 0 ' + hMB + '; }\nh4 { font-family: ' + hFontFamily + '; font-size: ' + h4Size + '; color: ' + h4Color + '; font-weight: ' + h4Weight + '; line-height: 1.5; margin: 1em 0 ' + hMB + '; }\np { color: ' + pColor + '; line-height: ' + pLH + '; margin: ' + pMargin + '; }\na { color: ' + linkColor + '; text-decoration: ' + linkDec + '; text-underline-offset: 2px; }\ncode { background: ' + codeBG + '; color: ' + codeColor + '; padding: 0.15em 0.45em; border-radius: 4px; font-family: ' + codeFont + '; font-size: 0.85em; }\npre { background: ' + preBG + '; padding: 1.1em 1.25em; border-radius: 8px; overflow-x: auto; margin: 1.2em 0; border: 1px solid #e7e2db; }\npre code { background: none; padding: 0; color: ' + codeColor + '; font-size: 0.88em; }\nblockquote { border-left: ' + bqBorder + '; color: ' + bqColor + '; padding: ' + bqPad + '; margin: ' + bqMargin + '; background: ' + bqBg + '; border-radius: 0 6px 6px 0; }\nblockquote p { margin: 0; color: inherit; }\nul, ol { padding-left: 1.6em; margin: 0.5em 0 1.1em; color: ' + listColor + '; }\nli { margin-bottom: 0.3em; }\nli::marker { color: ' + listColor + '; }\nhr { border: none; border-top: 1px solid #ede8e2; margin: 2em 0; }\ntable { border-collapse: collapse; width: 100%; margin: 1.2em 0; font-size: 0.92em; }\nth, td { border: 1px solid #e2ddd6; padding: 7px 12px; text-align: left; }\nth { background: #f4f1ed; font-weight: 600; }\ntr:nth-child(even) td { background: #fafaf8; }\nimg { max-width: 100%; border-radius: 8px; }\n';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function inlineImages(clone) {
|
|
61
|
-
var imgs = clone.querySelectorAll('img');
|
|
62
|
-
for (var i = 0; i < imgs.length; i++) {
|
|
63
|
-
var img = imgs[i];
|
|
64
|
-
if (/^data:/.test(img.src)) continue;
|
|
65
|
-
// Find the corresponding loaded image in the live DOM by matching src
|
|
66
|
-
var liveImg = S.renderedEl.querySelector('img[src="' + img.getAttribute('src') + '"]');
|
|
67
|
-
if (!liveImg || !liveImg.naturalWidth) continue;
|
|
68
|
-
try {
|
|
69
|
-
var c = document.createElement('canvas');
|
|
70
|
-
c.width = liveImg.naturalWidth;
|
|
71
|
-
c.height = liveImg.naturalHeight;
|
|
72
|
-
c.getContext('2d').drawImage(liveImg, 0, 0);
|
|
73
|
-
img.src = c.toDataURL('image/png');
|
|
74
|
-
} catch (e) {
|
|
75
|
-
// CORS — leave original src in place
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function inlineCharts(clone) {
|
|
81
|
-
// Remove chart menu UI
|
|
82
|
-
clone.querySelectorAll('.chart-menu-btn, .chart-menu').forEach(function(el) { el.remove(); });
|
|
83
|
-
// Build a map from wrapper element → data URL using Chart.js instances
|
|
84
|
-
var chartImages = S.getChartImages ? S.getChartImages() : [];
|
|
85
|
-
var wrapperMap = new Map();
|
|
86
|
-
chartImages.forEach(function(entry) {
|
|
87
|
-
wrapperMap.set(entry.wrapper, entry.dataUrl);
|
|
88
|
-
});
|
|
89
|
-
// Match live wrappers to clone wrappers by index
|
|
90
|
-
var origWrappers = S.renderedEl.querySelectorAll('.sdoc-chart');
|
|
91
|
-
var cloneWrappers = clone.querySelectorAll('.sdoc-chart');
|
|
92
|
-
for (var i = 0; i < cloneWrappers.length; i++) {
|
|
93
|
-
var dataUrl = origWrappers[i] ? wrapperMap.get(origWrappers[i]) : null;
|
|
94
|
-
if (!dataUrl || dataUrl === 'data:,') {
|
|
95
|
-
// Fallback: try canvas.toDataURL directly
|
|
96
|
-
var origCanvas = origWrappers[i] && origWrappers[i].querySelector('canvas');
|
|
97
|
-
if (origCanvas && origCanvas.width > 0) {
|
|
98
|
-
try { dataUrl = origCanvas.toDataURL('image/png'); } catch (e) { /* skip */ }
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
var canvas = cloneWrappers[i].querySelector('canvas');
|
|
102
|
-
if (canvas && dataUrl && dataUrl !== 'data:,') {
|
|
103
|
-
var img = document.createElement('img');
|
|
104
|
-
img.src = dataUrl;
|
|
105
|
-
img.style.width = '100%';
|
|
106
|
-
canvas.parentNode.replaceChild(img, canvas);
|
|
107
|
-
} else if (canvas) {
|
|
108
|
-
// No valid image — remove the empty canvas entirely
|
|
109
|
-
canvas.remove();
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function buildExportHTML() {
|
|
115
|
-
var fontName = document.getElementById('ctrl-font-family').value.replace(/['"]/g,'').split(',')[0].trim();
|
|
116
|
-
var fontLink = S.GOOGLE_FONTS.includes(fontName)
|
|
117
|
-
? '<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=' + encodeURIComponent(fontName) + ':wght@400;500;600;700&display=swap">'
|
|
118
|
-
: '';
|
|
119
|
-
var title = (S.currentMeta.title || 'Document').replace(/</g,'<');
|
|
120
|
-
var clone = S.renderedEl.cloneNode(true);
|
|
121
|
-
inlineCharts(clone);
|
|
122
|
-
inlineImages(clone);
|
|
123
|
-
clone.querySelectorAll('.section-toggle').forEach(function(el) { el.remove(); });
|
|
124
|
-
clone.querySelectorAll('.md-section-body').forEach(function(el) {
|
|
125
|
-
while (el.firstChild) el.parentNode.insertBefore(el.firstChild, el);
|
|
126
|
-
el.remove();
|
|
127
|
-
});
|
|
128
|
-
clone.querySelectorAll('.md-section').forEach(function(el) {
|
|
129
|
-
while (el.firstChild) el.parentNode.insertBefore(el.firstChild, el);
|
|
130
|
-
el.remove();
|
|
131
|
-
});
|
|
132
|
-
return '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n<title>' + title + '</title>\n' + fontLink + '\n<style>' + buildExportCSS() + '\n.sdoc-chart { text-align: center; margin: 1.2em 0; }\n.sdoc-chart img { max-width: 100%; }</style>\n</head>\n<body>\n' +
|
|
133
|
-
clone.innerHTML
|
|
134
|
-
.replace(/<button class="copy-btn"[^]*?<\/button>/g, '')
|
|
135
|
-
.replace(/<button class="header-copy-btn"[^]*?<\/button>/g, '')
|
|
136
|
-
.replace(/<div class="pre-wrapper">([\s\S]*?)<\/div>/g, '$1')
|
|
137
|
-
.replace(/<a class="header-anchor"[^]*?<\/a>/g, '') +
|
|
138
|
-
'\n</body>\n</html>';
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function expandAllSections() {
|
|
142
|
-
var closed = [];
|
|
143
|
-
S.renderedEl.querySelectorAll('.md-section-body:not(.open)').forEach(function(b) {
|
|
144
|
-
b.classList.add('open');
|
|
145
|
-
closed.push(b);
|
|
146
|
-
});
|
|
147
|
-
return closed;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function restoreSections(closed) {
|
|
151
|
-
closed.forEach(function(b) { b.classList.remove('open'); });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ── pdf-lib PDF renderer ──────────────────────────────
|
|
155
|
-
|
|
156
|
-
var pdfLibLoaded = false;
|
|
157
|
-
|
|
158
|
-
function loadPdfLib() {
|
|
159
|
-
return new Promise(function(resolve, reject) {
|
|
160
|
-
if (pdfLibLoaded) { resolve(); return; }
|
|
161
|
-
var s1 = document.createElement('script');
|
|
162
|
-
s1.src = 'https://cdn.jsdelivr.net/npm/pdf-lib@1.17.1/dist/pdf-lib.min.js';
|
|
163
|
-
s1.onload = function() {
|
|
164
|
-
var s2 = document.createElement('script');
|
|
165
|
-
s2.src = 'https://cdn.jsdelivr.net/npm/@pdf-lib/fontkit@1.1.1/dist/fontkit.umd.min.js';
|
|
166
|
-
s2.onload = function() { pdfLibLoaded = true; resolve(); };
|
|
167
|
-
s2.onerror = function() { reject(new Error('Could not load fontkit')); };
|
|
168
|
-
document.head.appendChild(s2);
|
|
169
|
-
};
|
|
170
|
-
s1.onerror = function() { reject(new Error('Could not load pdf-lib')); };
|
|
171
|
-
document.head.appendChild(s1);
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ── Font loading ──
|
|
176
|
-
|
|
177
|
-
var fontBufCache = {};
|
|
178
|
-
|
|
179
|
-
function fontSlug(name) { return name.toLowerCase().replace(/\s+/g, '-'); }
|
|
180
|
-
|
|
181
|
-
function fetchFontBuf(slug, weight) {
|
|
182
|
-
var key = slug + '-' + weight;
|
|
183
|
-
if (fontBufCache[key]) return Promise.resolve(fontBufCache[key]);
|
|
184
|
-
var url = 'https://cdn.jsdelivr.net/fontsource/fonts/' + slug + '@latest/latin-' + weight + '-normal.ttf';
|
|
185
|
-
return fetch(url).then(function(r) {
|
|
186
|
-
if (!r.ok) throw new Error(r.status);
|
|
187
|
-
return r.arrayBuffer();
|
|
188
|
-
}).then(function(buf) {
|
|
189
|
-
fontBufCache[key] = buf;
|
|
190
|
-
return buf;
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ── Color parsing ──
|
|
195
|
-
|
|
196
|
-
function hexToRgb(hex) {
|
|
197
|
-
if (!hex || hex.charAt(0) !== '#') return PDFLib.rgb(0, 0, 0);
|
|
198
|
-
hex = hex.replace('#', '');
|
|
199
|
-
if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
|
|
200
|
-
return PDFLib.rgb(parseInt(hex.substring(0,2),16)/255, parseInt(hex.substring(2,4),16)/255, parseInt(hex.substring(4,6),16)/255);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function parseBqBorderColor(val) {
|
|
204
|
-
if (!val) return '#2563eb';
|
|
205
|
-
var m = val.match(/#[0-9a-fA-F]{3,8}/);
|
|
206
|
-
return m ? m[0] : '#2563eb';
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ── Style reading ──
|
|
210
|
-
|
|
211
|
-
function readPdfStyles() {
|
|
212
|
-
var g = getCssVar;
|
|
213
|
-
var hColor = g('--md-h-color') || '#0f0d0c';
|
|
214
|
-
var pColor = g('--md-p-color') || '#3c3733';
|
|
215
|
-
var codeBg = g('--md-code-bg') || '#f4f1ed';
|
|
216
|
-
var hScale = parseFloat(g('--md-h-scale') || '1');
|
|
217
|
-
var basePx = parseFloat(g('--md-base-size') || '16');
|
|
218
|
-
var basePt = basePx * 0.75;
|
|
219
|
-
return {
|
|
220
|
-
basePt: basePt,
|
|
221
|
-
lineH: parseFloat(g('--md-line-height') || '1.75'),
|
|
222
|
-
hScale: hScale,
|
|
223
|
-
h1Size: parseFloat(g('--md-h1-size') || '2.1') * basePt * hScale,
|
|
224
|
-
h2Size: parseFloat(g('--md-h2-size') || '1.55') * basePt * hScale,
|
|
225
|
-
h3Size: parseFloat(g('--md-h3-size') || '1.2') * basePt * hScale,
|
|
226
|
-
h4Size: parseFloat(g('--md-h4-size') || '1.0') * basePt * hScale,
|
|
227
|
-
color: g('--md-color') || '#1c1917',
|
|
228
|
-
hColor: hColor,
|
|
229
|
-
h1Color: g('--md-h1-color') || hColor, h2Color: g('--md-h2-color') || hColor,
|
|
230
|
-
h3Color: g('--md-h3-color') || hColor, h4Color: g('--md-h4-color') || hColor,
|
|
231
|
-
pColor: pColor,
|
|
232
|
-
linkColor: g('--md-link-color') || '#2563eb',
|
|
233
|
-
codeBg: codeBg, codeColor: g('--md-code-color') || '#6b21a8',
|
|
234
|
-
preBg: g('--md-pre-bg') || codeBg, preBorder: g('--md-pre-border') || '#e7e2db',
|
|
235
|
-
bqBg: g('--md-bq-bg') || '#f7f5f2', bqColor: g('--md-bq-color') || '#6b6560',
|
|
236
|
-
bqBorderColor: parseBqBorderColor(g('--md-bq-border')),
|
|
237
|
-
listColor: g('--md-list-color') || pColor,
|
|
238
|
-
tableBorder: g('--md-table-border') || '#e2ddd6',
|
|
239
|
-
tableHeaderBg: g('--md-table-header-bg') || '#f4f1ed',
|
|
240
|
-
tableEvenBg: g('--md-table-even-bg') || '#fafaf8',
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// ── Image helper ──
|
|
245
|
-
|
|
246
|
-
function imgToDataUrl(img) {
|
|
247
|
-
if (!img || !img.src) return null;
|
|
248
|
-
if (/^data:/.test(img.src)) return img.src;
|
|
249
|
-
if (!img.naturalWidth) return null;
|
|
250
|
-
try {
|
|
251
|
-
var c = document.createElement('canvas');
|
|
252
|
-
c.width = img.naturalWidth; c.height = img.naturalHeight;
|
|
253
|
-
c.getContext('2d').drawImage(img, 0, 0);
|
|
254
|
-
return c.toDataURL('image/png');
|
|
255
|
-
} catch (e) { return null; }
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ── CSS → PDF helpers ──
|
|
259
|
-
|
|
260
|
-
// px → pt (CSS uses 1px = 1/96in; PDF uses 1pt = 1/72in → pt = px * 0.75)
|
|
261
|
-
function pxToPt(px) { return parseFloat(px) * 0.75; }
|
|
262
|
-
|
|
263
|
-
// Parse "rgb(r, g, b)" or "rgba(r, g, b, a)" → hex
|
|
264
|
-
function rgbStrToHex(s) {
|
|
265
|
-
if (!s || s === 'transparent' || s === 'rgba(0, 0, 0, 0)') return null;
|
|
266
|
-
var m = s.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
267
|
-
if (!m) return s.charAt(0) === '#' ? s : null;
|
|
268
|
-
var r = parseInt(m[1]), g = parseInt(m[2]), b = parseInt(m[3]);
|
|
269
|
-
return '#' + [r, g, b].map(function(v) { return (v < 16 ? '0' : '') + v.toString(16); }).join('');
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Read computed CSS into a structured object
|
|
273
|
-
function readComputedBlock(el) {
|
|
274
|
-
var cs = getComputedStyle(el);
|
|
275
|
-
return {
|
|
276
|
-
color: rgbStrToHex(cs.color),
|
|
277
|
-
background: rgbStrToHex(cs.backgroundColor),
|
|
278
|
-
fontSize: pxToPt(parseFloat(cs.fontSize)),
|
|
279
|
-
fontWeight: parseInt(cs.fontWeight) || 400,
|
|
280
|
-
fontStyle: cs.fontStyle,
|
|
281
|
-
lineHeight: cs.lineHeight === 'normal' ? null : parseFloat(cs.lineHeight),
|
|
282
|
-
padL: pxToPt(parseFloat(cs.paddingLeft) || 0),
|
|
283
|
-
padR: pxToPt(parseFloat(cs.paddingRight) || 0),
|
|
284
|
-
padT: pxToPt(parseFloat(cs.paddingTop) || 0),
|
|
285
|
-
padB: pxToPt(parseFloat(cs.paddingBottom) || 0),
|
|
286
|
-
marL: pxToPt(parseFloat(cs.marginLeft) || 0),
|
|
287
|
-
marR: pxToPt(parseFloat(cs.marginRight) || 0),
|
|
288
|
-
marT: pxToPt(parseFloat(cs.marginTop) || 0),
|
|
289
|
-
marB: pxToPt(parseFloat(cs.marginBottom) || 0),
|
|
290
|
-
rTL: pxToPt(parseFloat(cs.borderTopLeftRadius) || 0),
|
|
291
|
-
rTR: pxToPt(parseFloat(cs.borderTopRightRadius) || 0),
|
|
292
|
-
rBR: pxToPt(parseFloat(cs.borderBottomRightRadius) || 0),
|
|
293
|
-
rBL: pxToPt(parseFloat(cs.borderBottomLeftRadius) || 0),
|
|
294
|
-
borderLeftColor: rgbStrToHex(cs.borderLeftColor),
|
|
295
|
-
borderLeftWidth: pxToPt(parseFloat(cs.borderLeftWidth) || 0),
|
|
296
|
-
borderColor: rgbStrToHex(cs.borderColor || cs.borderTopColor),
|
|
297
|
-
borderWidth: pxToPt(parseFloat(cs.borderTopWidth) || 0),
|
|
298
|
-
textDecoration: cs.textDecorationLine || cs.textDecoration,
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ── pdf-lib rendering engine ──
|
|
303
|
-
|
|
304
|
-
async function renderPdf(rendered, st, chartImages) {
|
|
305
|
-
var PDFDocument = PDFLib.PDFDocument;
|
|
306
|
-
var rgb = PDFLib.rgb;
|
|
307
|
-
var doc = await PDFDocument.create();
|
|
308
|
-
doc.registerFontkit(fontkit);
|
|
309
|
-
|
|
310
|
-
// Load fonts
|
|
311
|
-
var bodyFontRaw = getCssVar('--md-font-family') || "'Inter', sans-serif";
|
|
312
|
-
var headFontRaw = getCssVar('--md-h-font-family') || 'inherit';
|
|
313
|
-
var bodyName = bodyFontRaw.replace(/['"]/g, '').split(',')[0].trim();
|
|
314
|
-
var headName = headFontRaw === 'inherit' ? bodyName : headFontRaw.replace(/['"]/g, '').split(',')[0].trim();
|
|
315
|
-
var bodySlug = fontSlug(bodyName);
|
|
316
|
-
var headSlug = fontSlug(headName);
|
|
317
|
-
|
|
318
|
-
var font, bold, headFont, headBold, mono;
|
|
319
|
-
try {
|
|
320
|
-
var b400 = await fetchFontBuf(bodySlug, 400);
|
|
321
|
-
var b700 = await fetchFontBuf(bodySlug, 700);
|
|
322
|
-
// { subset: true } is critical: the default CustomFontEmbedder builds its
|
|
323
|
-
// ToUnicode CMap from the font's cmap table only, so any glyph produced
|
|
324
|
-
// by OpenType substitution (e.g. Inter's "case" feature swapping "(" for
|
|
325
|
-
// an uppercase-aware variant next to capitals) has no Unicode mapping
|
|
326
|
-
// and becomes unextractable. SubsetEmbedder CMaps every glyph that
|
|
327
|
-
// encodeText actually uses, so substitutions round-trip correctly.
|
|
328
|
-
font = await doc.embedFont(b400, { subset: true });
|
|
329
|
-
bold = await doc.embedFont(b700, { subset: true });
|
|
330
|
-
} catch (e) {
|
|
331
|
-
font = await doc.embedFont(PDFLib.StandardFonts.Helvetica);
|
|
332
|
-
bold = await doc.embedFont(PDFLib.StandardFonts.HelveticaBold);
|
|
333
|
-
}
|
|
334
|
-
if (headSlug !== bodySlug) {
|
|
335
|
-
try {
|
|
336
|
-
var h600 = await fetchFontBuf(headSlug, 600).catch(function() { return fetchFontBuf(headSlug, 700); });
|
|
337
|
-
var h700 = await fetchFontBuf(headSlug, 700).catch(function() { return h600; });
|
|
338
|
-
headFont = await doc.embedFont(h600, { subset: true });
|
|
339
|
-
headBold = await doc.embedFont(h700, { subset: true });
|
|
340
|
-
} catch (e) { headFont = font; headBold = bold; }
|
|
341
|
-
} else {
|
|
342
|
-
try {
|
|
343
|
-
var semi = await fetchFontBuf(bodySlug, 600);
|
|
344
|
-
headFont = await doc.embedFont(semi, { subset: true });
|
|
345
|
-
} catch (e) { headFont = bold; }
|
|
346
|
-
headBold = bold;
|
|
347
|
-
}
|
|
348
|
-
mono = await doc.embedFont(PDFLib.StandardFonts.Courier);
|
|
349
|
-
|
|
350
|
-
// Page layout
|
|
351
|
-
var W = 595.28; // A4 width
|
|
352
|
-
var H = 841.89; // A4 height
|
|
353
|
-
var ML = 48, MR = 48, MT = 48, MB = 48;
|
|
354
|
-
var CW = W - ML - MR; // content width
|
|
355
|
-
var fontSize = st.basePt;
|
|
356
|
-
var lineSpacing = fontSize * st.lineH;
|
|
357
|
-
|
|
358
|
-
var page = doc.addPage([W, H]);
|
|
359
|
-
var y = H - MT;
|
|
360
|
-
|
|
361
|
-
function ensureSpace(need) {
|
|
362
|
-
if (y - need < MB) {
|
|
363
|
-
page = doc.addPage([W, H]);
|
|
364
|
-
y = H - MT;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// ── Rounded rectangle (SVG path; pdf-lib's drawRectangle doesn't support borderRadius) ──
|
|
369
|
-
// opts.x/y = top-left corner in PDF coords (y = distance from page bottom)
|
|
370
|
-
// opts.radius = number, or {tl, tr, br, bl} for per-corner radii
|
|
371
|
-
function drawRoundedRect(opts) {
|
|
372
|
-
var x = opts.x, yTop = opts.y, w = opts.width, h = opts.height;
|
|
373
|
-
var r = opts.radius;
|
|
374
|
-
var rTL, rTR, rBR, rBL;
|
|
375
|
-
if (typeof r === 'object') {
|
|
376
|
-
rTL = r.tl || 0; rTR = r.tr || 0; rBR = r.br || 0; rBL = r.bl || 0;
|
|
377
|
-
} else {
|
|
378
|
-
rTL = rTR = rBR = rBL = r || 0;
|
|
379
|
-
}
|
|
380
|
-
var maxR = Math.min(w, h) / 2;
|
|
381
|
-
rTL = Math.min(rTL, maxR); rTR = Math.min(rTR, maxR);
|
|
382
|
-
rBR = Math.min(rBR, maxR); rBL = Math.min(rBL, maxR);
|
|
383
|
-
|
|
384
|
-
// Build SVG path in local coords: origin at top-left, y grows DOWN, extends to (w, h)
|
|
385
|
-
var path = 'M' + rTL + ' 0';
|
|
386
|
-
path += ' L' + (w - rTR) + ' 0';
|
|
387
|
-
if (rTR) path += ' Q' + w + ' 0 ' + w + ' ' + rTR;
|
|
388
|
-
path += ' L' + w + ' ' + (h - rBR);
|
|
389
|
-
if (rBR) path += ' Q' + w + ' ' + h + ' ' + (w - rBR) + ' ' + h;
|
|
390
|
-
path += ' L' + rBL + ' ' + h;
|
|
391
|
-
if (rBL) path += ' Q0 ' + h + ' 0 ' + (h - rBL);
|
|
392
|
-
path += ' L0 ' + rTL;
|
|
393
|
-
if (rTL) path += ' Q0 0 ' + rTL + ' 0';
|
|
394
|
-
path += ' Z';
|
|
395
|
-
|
|
396
|
-
// Pass origin via x/y options — pdf-lib flips SVG y-down into PDF y-up
|
|
397
|
-
var drawOpts = { x: x, y: yTop };
|
|
398
|
-
if (opts.color) drawOpts.color = hexToRgb(opts.color);
|
|
399
|
-
if (opts.borderColor) drawOpts.borderColor = hexToRgb(opts.borderColor);
|
|
400
|
-
if (opts.borderWidth) drawOpts.borderWidth = opts.borderWidth;
|
|
401
|
-
page.drawSvgPath(path, drawOpts);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// ── Text wrapping engine ──
|
|
405
|
-
|
|
406
|
-
function measureRun(run) {
|
|
407
|
-
var f = run.bold ? bold : (run.italic ? font : font);
|
|
408
|
-
var s = run.size || fontSize;
|
|
409
|
-
if (run.code) { f = mono; s = fontSize * 0.85; }
|
|
410
|
-
return { width: f.widthOfTextAtSize(run.text, s), font: f, size: s };
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Extract inline runs from DOM element
|
|
414
|
-
function extractRuns(el) {
|
|
415
|
-
var runs = [];
|
|
416
|
-
el.childNodes.forEach(function(n) {
|
|
417
|
-
if (n.nodeType === 3) {
|
|
418
|
-
if (n.textContent) runs.push({ text: n.textContent, color: st.pColor });
|
|
419
|
-
} else if (n.nodeType === 1) {
|
|
420
|
-
var tag = n.tagName.toLowerCase();
|
|
421
|
-
if (tag === 'strong' || tag === 'b') {
|
|
422
|
-
runs.push({ text: n.textContent, bold: true, color: st.pColor });
|
|
423
|
-
} else if (tag === 'em' || tag === 'i') {
|
|
424
|
-
runs.push({ text: n.textContent, italic: true, color: st.pColor });
|
|
425
|
-
} else if (tag === 'code') {
|
|
426
|
-
runs.push({ text: n.textContent, code: true, color: st.codeColor });
|
|
427
|
-
} else if (tag === 'a') {
|
|
428
|
-
runs.push({ text: n.textContent, color: st.linkColor, link: n.href, underline: true });
|
|
429
|
-
} else if (tag === 'br') {
|
|
430
|
-
runs.push({ text: '\n', color: st.pColor });
|
|
431
|
-
} else if (tag !== 'img') {
|
|
432
|
-
runs = runs.concat(extractRuns(n));
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
return runs;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Split runs into wrapped lines that fit within maxW
|
|
440
|
-
function wrapRuns(runs, maxW) {
|
|
441
|
-
var lines = [[]];
|
|
442
|
-
var lineW = 0;
|
|
443
|
-
runs.forEach(function(run) {
|
|
444
|
-
if (run.text === '\n') { lines.push([]); lineW = 0; return; }
|
|
445
|
-
|
|
446
|
-
// Inline code: treat the whole run as one unit (don't word-split)
|
|
447
|
-
// so spaces inside "npm i -g sdocs-dev" stay in one pill.
|
|
448
|
-
if (run.code) {
|
|
449
|
-
var f = mono;
|
|
450
|
-
var s = fontSize * 0.85;
|
|
451
|
-
// Account for pill padding in wrap calculation
|
|
452
|
-
var ww = f.widthOfTextAtSize(run.text, s) + 5; // CODE_PAD_X * 2 = 5
|
|
453
|
-
if (lineW + ww > maxW && lineW > 0) {
|
|
454
|
-
lines.push([]); lineW = 0;
|
|
455
|
-
}
|
|
456
|
-
lines[lines.length - 1].push({
|
|
457
|
-
text: run.text, font: f, size: s,
|
|
458
|
-
color: run.color, code: true,
|
|
459
|
-
});
|
|
460
|
-
lineW += ww;
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
var words = run.text.split(/(\s+)/);
|
|
465
|
-
words.forEach(function(word) {
|
|
466
|
-
if (!word) return;
|
|
467
|
-
var f = run.bold ? bold : (run.italic ? font : font);
|
|
468
|
-
var s = run.size || fontSize;
|
|
469
|
-
var ww = f.widthOfTextAtSize(word, s);
|
|
470
|
-
if (lineW + ww > maxW && lineW > 0 && word.trim()) {
|
|
471
|
-
lines.push([]);
|
|
472
|
-
lineW = 0;
|
|
473
|
-
// Skip leading whitespace on wrapped lines
|
|
474
|
-
if (!word.trim()) return;
|
|
475
|
-
}
|
|
476
|
-
lines[lines.length - 1].push({
|
|
477
|
-
text: word, font: f, size: s,
|
|
478
|
-
color: run.color, bold: run.bold, italic: run.italic,
|
|
479
|
-
link: run.link, underline: run.underline,
|
|
480
|
-
});
|
|
481
|
-
lineW += ww;
|
|
482
|
-
});
|
|
483
|
-
});
|
|
484
|
-
return lines;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Wrap a single code-block line to fit within maxW chars (word-break, keep leading indent)
|
|
488
|
-
function wrapCodeLine(line, maxW, codeSize) {
|
|
489
|
-
if (mono.widthOfTextAtSize(line, codeSize) <= maxW) return [line];
|
|
490
|
-
// Extract leading whitespace for continuation indent
|
|
491
|
-
var indentMatch = line.match(/^\s*/);
|
|
492
|
-
var indent = indentMatch ? indentMatch[0] : '';
|
|
493
|
-
var contIndent = indent + ' ';
|
|
494
|
-
var words = line.split(/( +)/); // split but keep spaces
|
|
495
|
-
var out = [''];
|
|
496
|
-
var curW = 0;
|
|
497
|
-
words.forEach(function(w) {
|
|
498
|
-
if (!w) return;
|
|
499
|
-
var ww = mono.widthOfTextAtSize(w, codeSize);
|
|
500
|
-
if (curW + ww > maxW && out[out.length - 1].trim()) {
|
|
501
|
-
out.push(contIndent);
|
|
502
|
-
curW = mono.widthOfTextAtSize(contIndent, codeSize);
|
|
503
|
-
}
|
|
504
|
-
out[out.length - 1] += w;
|
|
505
|
-
curW += ww;
|
|
506
|
-
});
|
|
507
|
-
return out;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Add a clickable link annotation to the current page
|
|
511
|
-
function addLinkAnnotation(url, x1, y1, x2, y2) {
|
|
512
|
-
var PDFName = PDFLib.PDFName;
|
|
513
|
-
var PDFString = PDFLib.PDFString;
|
|
514
|
-
var ctx = doc.context;
|
|
515
|
-
var linkDict = ctx.obj({
|
|
516
|
-
Type: 'Annot',
|
|
517
|
-
Subtype: 'Link',
|
|
518
|
-
Rect: [x1, y1, x2, y2],
|
|
519
|
-
Border: [0, 0, 0],
|
|
520
|
-
C: [0, 0, 1],
|
|
521
|
-
A: { Type: 'Action', S: 'URI', URI: PDFString.of(url) },
|
|
522
|
-
});
|
|
523
|
-
var linkRef = ctx.register(linkDict);
|
|
524
|
-
var annots = page.node.get(PDFName.of('Annots'));
|
|
525
|
-
if (!annots) {
|
|
526
|
-
page.node.set(PDFName.of('Annots'), ctx.obj([linkRef]));
|
|
527
|
-
} else {
|
|
528
|
-
annots.push(linkRef);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Merge adjacent runs with identical formatting so whitespace gets drawn
|
|
533
|
-
// as part of a single text string (preserves proper inter-word spacing).
|
|
534
|
-
function mergeRuns(lineRuns) {
|
|
535
|
-
var out = [];
|
|
536
|
-
lineRuns.forEach(function(r) {
|
|
537
|
-
if (!r.text) return;
|
|
538
|
-
var last = out[out.length - 1];
|
|
539
|
-
var key = (r.font === last ? 'x' : '') + '|' + r.size + '|' + (r.color || '') +
|
|
540
|
-
'|' + (r.code ? '1' : '0') + '|' + (r.underline ? '1' : '0') +
|
|
541
|
-
'|' + (r.bold ? '1' : '0') + '|' + (r.italic ? '1' : '0') +
|
|
542
|
-
'|' + (r.link || '');
|
|
543
|
-
if (last && last._font === r.font && last._key === key) {
|
|
544
|
-
last.text += r.text;
|
|
545
|
-
} else {
|
|
546
|
-
out.push({
|
|
547
|
-
_font: r.font, _key: key,
|
|
548
|
-
text: r.text, font: r.font, size: r.size, color: r.color,
|
|
549
|
-
code: r.code, underline: r.underline, link: r.link,
|
|
550
|
-
bold: r.bold, italic: r.italic,
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
return out;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// Inline code pill spacing
|
|
558
|
-
var CODE_PAD_X = 2.5;
|
|
559
|
-
var CODE_PAD_Y = 1;
|
|
560
|
-
|
|
561
|
-
// Draw a single line of runs. `ly` is the baseline Y (PDF coords).
|
|
562
|
-
function drawLine(lineRuns, x, ly) {
|
|
563
|
-
var merged = mergeRuns(lineRuns);
|
|
564
|
-
var cx = x;
|
|
565
|
-
merged.forEach(function(r) {
|
|
566
|
-
if (!r.text) return;
|
|
567
|
-
var w = r.font.widthOfTextAtSize(r.text, r.size);
|
|
568
|
-
// Inline code pill — pill width = w + 2*padX, text centered with padX inset
|
|
569
|
-
if (r.code && r.text.trim()) {
|
|
570
|
-
var ascent = r.size * 0.78;
|
|
571
|
-
var descent = r.size * 0.22;
|
|
572
|
-
var pillH = ascent + descent + CODE_PAD_Y * 2;
|
|
573
|
-
drawRoundedRect({
|
|
574
|
-
x: cx,
|
|
575
|
-
y: ly + ascent + CODE_PAD_Y,
|
|
576
|
-
width: w + CODE_PAD_X * 2,
|
|
577
|
-
height: pillH,
|
|
578
|
-
radius: 3,
|
|
579
|
-
color: st.codeBg,
|
|
580
|
-
});
|
|
581
|
-
page.drawText(r.text, { x: cx + CODE_PAD_X, y: ly, size: r.size, font: r.font, color: hexToRgb(r.color || st.codeColor) });
|
|
582
|
-
cx += w + CODE_PAD_X * 2;
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
page.drawText(r.text, { x: cx, y: ly, size: r.size, font: r.font, color: hexToRgb(r.color || st.pColor) });
|
|
586
|
-
if (r.underline) {
|
|
587
|
-
page.drawLine({
|
|
588
|
-
start: { x: cx, y: ly - 1.2 },
|
|
589
|
-
end: { x: cx + w, y: ly - 1.2 },
|
|
590
|
-
thickness: 0.6,
|
|
591
|
-
color: hexToRgb(r.color),
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
if (r.link) {
|
|
595
|
-
try { addLinkAnnotation(r.link, cx, ly - 2, cx + w, ly + r.size * 0.85); }
|
|
596
|
-
catch (e) { /* ignore annotation errors */ }
|
|
597
|
-
}
|
|
598
|
-
cx += w;
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Draw wrapped paragraph runs
|
|
603
|
-
function drawParagraph(runs, indent) {
|
|
604
|
-
indent = indent || 0;
|
|
605
|
-
var lines = wrapRuns(runs, CW - indent);
|
|
606
|
-
var lh = fontSize * st.lineH;
|
|
607
|
-
ensureSpace(lh);
|
|
608
|
-
lines.forEach(function(lineRuns) {
|
|
609
|
-
ensureSpace(lh);
|
|
610
|
-
drawLine(lineRuns, ML + indent, y);
|
|
611
|
-
y -= lh;
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// ── Async walker ──
|
|
616
|
-
|
|
617
|
-
var chartIdx = 0;
|
|
618
|
-
|
|
619
|
-
async function walk(parent) {
|
|
620
|
-
var children = parent.children;
|
|
621
|
-
for (var i = 0; i < children.length; i++) {
|
|
622
|
-
var el = children[i];
|
|
623
|
-
var tag = el.tagName.toLowerCase();
|
|
624
|
-
|
|
625
|
-
if (el.classList.contains('section-toggle') || el.classList.contains('copy-btn') ||
|
|
626
|
-
el.classList.contains('header-copy-btn') || el.classList.contains('header-anchor')) continue;
|
|
627
|
-
if (el.classList.contains('md-section') || el.classList.contains('md-section-body')) { await walk(el); continue; }
|
|
628
|
-
if (el.classList.contains('pre-wrapper')) {
|
|
629
|
-
var ip = el.querySelector('pre');
|
|
630
|
-
if (ip) drawCodeBlock(ip);
|
|
631
|
-
continue;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
if (/^h[1-4]$/.test(tag)) drawHeading(el, tag);
|
|
635
|
-
else if (tag === 'p') await drawParagraphEl(el);
|
|
636
|
-
else if (tag === 'pre') drawCodeBlock(el);
|
|
637
|
-
else if (tag === 'blockquote') drawBlockquote(el);
|
|
638
|
-
else if (tag === 'ul') drawList(el, false);
|
|
639
|
-
else if (tag === 'ol') drawList(el, true);
|
|
640
|
-
else if (el.classList.contains('sdoc-chart')) await drawChart();
|
|
641
|
-
else if (tag === 'hr') drawHR(el);
|
|
642
|
-
else if (tag === 'img') await drawImage(el);
|
|
643
|
-
else if (tag === 'table') drawTable(el);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// ── Element renderers (CSS-driven) ──
|
|
648
|
-
|
|
649
|
-
function drawHeading(el, tag) {
|
|
650
|
-
var level = parseInt(tag[1]);
|
|
651
|
-
var s = readComputedBlock(el);
|
|
652
|
-
var sz = s.fontSize;
|
|
653
|
-
var f = s.fontWeight >= 700 ? headBold : headFont;
|
|
654
|
-
var color = s.color || st.hColor;
|
|
655
|
-
y -= s.marT * 0.8;
|
|
656
|
-
var runs = [{ text: el.textContent.trim() }];
|
|
657
|
-
var lines = wrapHeadingRuns(runs, CW, f, sz);
|
|
658
|
-
var lh = sz * 1.2;
|
|
659
|
-
lines.forEach(function(line) {
|
|
660
|
-
ensureSpace(lh);
|
|
661
|
-
var cx = ML;
|
|
662
|
-
line.forEach(function(word) {
|
|
663
|
-
page.drawText(word.text, { x: cx, y: y - sz * 0.82, size: sz, font: f, color: hexToRgb(color) });
|
|
664
|
-
cx += f.widthOfTextAtSize(word.text, sz);
|
|
665
|
-
});
|
|
666
|
-
y -= lh;
|
|
667
|
-
});
|
|
668
|
-
// Heading bottom margin — use full CSS value so paragraphs below aren't cramped
|
|
669
|
-
y -= Math.max(s.marB, sz * 0.35);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Special wrap for heading (single font/size, just for line breaking)
|
|
673
|
-
function wrapHeadingRuns(runs, maxW, f, sz) {
|
|
674
|
-
var lines = [[]];
|
|
675
|
-
var lineW = 0;
|
|
676
|
-
runs.forEach(function(run) {
|
|
677
|
-
var words = run.text.split(/(\s+)/);
|
|
678
|
-
words.forEach(function(word) {
|
|
679
|
-
if (!word) return;
|
|
680
|
-
var ww = f.widthOfTextAtSize(word, sz);
|
|
681
|
-
if (lineW + ww > maxW && lineW > 0 && word.trim()) {
|
|
682
|
-
lines.push([]); lineW = 0;
|
|
683
|
-
}
|
|
684
|
-
lines[lines.length - 1].push({ text: word });
|
|
685
|
-
lineW += ww;
|
|
686
|
-
});
|
|
687
|
-
});
|
|
688
|
-
return lines;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
async function drawParagraphEl(el) {
|
|
692
|
-
var s = readComputedBlock(el);
|
|
693
|
-
y -= s.marT * 0.2; // tighter vertical rhythm in PDF
|
|
694
|
-
// Detect images inside the paragraph
|
|
695
|
-
var imgChildren = el.querySelectorAll(':scope > img');
|
|
696
|
-
if (imgChildren.length) {
|
|
697
|
-
// Process each child in order
|
|
698
|
-
for (var i = 0; i < el.childNodes.length; i++) {
|
|
699
|
-
var child = el.childNodes[i];
|
|
700
|
-
if (child.nodeType === 1 && child.tagName === 'IMG') {
|
|
701
|
-
await drawImage(child);
|
|
702
|
-
} else if (child.nodeType === 3 && child.textContent.trim()) {
|
|
703
|
-
drawParagraphRuns([{ text: child.textContent, color: s.color || st.pColor }]);
|
|
704
|
-
} else if (child.nodeType === 1) {
|
|
705
|
-
drawParagraphRuns(extractRuns(child));
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
} else {
|
|
709
|
-
drawParagraphRuns(extractRuns(el), s);
|
|
710
|
-
}
|
|
711
|
-
y -= s.marB * 0.4;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function drawParagraphRuns(runs, s) {
|
|
715
|
-
if (!runs.length) return;
|
|
716
|
-
var lines = wrapRuns(runs, CW);
|
|
717
|
-
var lh = s && s.lineHeight ? s.lineHeight * 0.75 : fontSize * st.lineH;
|
|
718
|
-
lines.forEach(function(lineRuns) {
|
|
719
|
-
ensureSpace(lh);
|
|
720
|
-
drawLine(lineRuns, ML, y - fontSize * 0.85);
|
|
721
|
-
y -= lh;
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
function drawCodeBlock(el) {
|
|
726
|
-
var s = readComputedBlock(el);
|
|
727
|
-
var code = el.querySelector('code');
|
|
728
|
-
var rawText = (code || el).textContent;
|
|
729
|
-
var rawLines = rawText.split('\n');
|
|
730
|
-
if (rawLines[rawLines.length - 1] === '') rawLines.pop();
|
|
731
|
-
|
|
732
|
-
var codeSize = s.fontSize * 0.85;
|
|
733
|
-
if (code) {
|
|
734
|
-
codeSize = pxToPt(parseFloat(getComputedStyle(code).fontSize));
|
|
735
|
-
}
|
|
736
|
-
var lineH = codeSize * 1.5;
|
|
737
|
-
var padT = s.padT, padB = s.padB, padL = s.padL, padR = s.padR;
|
|
738
|
-
var innerW = CW - padL - padR;
|
|
739
|
-
var radius = { tl: s.rTL, tr: s.rTR, br: s.rBR, bl: s.rBL };
|
|
740
|
-
|
|
741
|
-
// Wrap each raw line to fit
|
|
742
|
-
var wrappedLines = [];
|
|
743
|
-
rawLines.forEach(function(line) {
|
|
744
|
-
wrappedLines = wrappedLines.concat(wrapCodeLine(line, innerW, codeSize));
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
var blockH = wrappedLines.length * lineH + padT + padB;
|
|
748
|
-
|
|
749
|
-
y -= s.marT * 0.5;
|
|
750
|
-
ensureSpace(blockH + 8);
|
|
751
|
-
|
|
752
|
-
drawRoundedRect({
|
|
753
|
-
x: ML, y: y, width: CW, height: blockH,
|
|
754
|
-
radius: radius,
|
|
755
|
-
color: s.background || st.preBg,
|
|
756
|
-
borderColor: s.borderColor || st.preBorder,
|
|
757
|
-
borderWidth: s.borderWidth > 0 ? s.borderWidth : 0.5,
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
var cy = y - padT - codeSize * 0.82;
|
|
761
|
-
var codeColor = code ? rgbStrToHex(getComputedStyle(code).color) : st.codeColor;
|
|
762
|
-
wrappedLines.forEach(function(line) {
|
|
763
|
-
if (line) page.drawText(line, { x: ML + padL, y: cy, size: codeSize, font: mono, color: hexToRgb(codeColor) });
|
|
764
|
-
cy -= lineH;
|
|
765
|
-
});
|
|
766
|
-
// More breathing room after code blocks
|
|
767
|
-
y -= blockH + Math.max(s.marB, fontSize * 0.8);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function drawBlockquote(el) {
|
|
771
|
-
var s = readComputedBlock(el);
|
|
772
|
-
var runs = [];
|
|
773
|
-
el.querySelectorAll('p').forEach(function(p) { runs = runs.concat(extractRuns(p)); });
|
|
774
|
-
if (!runs.length) runs = [{ text: el.textContent.trim(), color: s.color || st.bqColor }];
|
|
775
|
-
runs.forEach(function(r) { r.color = s.color || st.bqColor; });
|
|
776
|
-
|
|
777
|
-
var lh = s.lineHeight ? s.lineHeight * 0.75 : fontSize * st.lineH;
|
|
778
|
-
var padL = s.padL, padR = s.padR, padT = s.padT, padB = s.padB;
|
|
779
|
-
var borderW = s.borderLeftWidth;
|
|
780
|
-
var textX = ML + padL + borderW;
|
|
781
|
-
var lines = wrapRuns(runs, CW - padL - padR - borderW);
|
|
782
|
-
var blockH = lines.length * lh + padT + padB;
|
|
783
|
-
var radius = { tl: s.rTL, tr: s.rTR, br: s.rBR, bl: s.rBL };
|
|
784
|
-
|
|
785
|
-
y -= s.marT * 0.4;
|
|
786
|
-
ensureSpace(blockH + 4);
|
|
787
|
-
|
|
788
|
-
// Background — full width, with CSS radii (typically left corners = 0, right corners = 6)
|
|
789
|
-
drawRoundedRect({
|
|
790
|
-
x: ML, y: y, width: CW, height: blockH,
|
|
791
|
-
radius: radius,
|
|
792
|
-
color: s.background || st.bqBg,
|
|
793
|
-
});
|
|
794
|
-
// Left border — flat rectangle (no rounding)
|
|
795
|
-
if (borderW > 0) {
|
|
796
|
-
var borderColor = s.borderLeftColor || st.bqBorderColor;
|
|
797
|
-
drawRoundedRect({
|
|
798
|
-
x: ML, y: y, width: borderW, height: blockH,
|
|
799
|
-
radius: 0,
|
|
800
|
-
color: borderColor,
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// Vertically center: distribute half-leading above first line
|
|
805
|
-
var halfLeading = Math.max((lh - fontSize) / 2, 0);
|
|
806
|
-
var bqY = y - padT - halfLeading - fontSize * 0.82;
|
|
807
|
-
lines.forEach(function(lineRuns) {
|
|
808
|
-
drawLine(lineRuns, textX + 4, bqY);
|
|
809
|
-
bqY -= lh;
|
|
810
|
-
});
|
|
811
|
-
y -= blockH + s.marB * 0.4;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
function drawList(el, ordered) {
|
|
815
|
-
var s = readComputedBlock(el);
|
|
816
|
-
var items = el.querySelectorAll(':scope > li');
|
|
817
|
-
y -= s.marT * 0.2;
|
|
818
|
-
items.forEach(function(li, idx) {
|
|
819
|
-
var itemS = readComputedBlock(li);
|
|
820
|
-
var lh = itemS.lineHeight ? itemS.lineHeight * 0.75 : fontSize * st.lineH;
|
|
821
|
-
var bullet = ordered ? (idx + 1) + '.' : '\u2022';
|
|
822
|
-
var indent = 14;
|
|
823
|
-
var runs = extractRuns(li);
|
|
824
|
-
var lines = wrapRuns(runs, CW - indent - 8);
|
|
825
|
-
lines.forEach(function(lineRuns, lineIdx) {
|
|
826
|
-
ensureSpace(lh);
|
|
827
|
-
if (lineIdx === 0) {
|
|
828
|
-
page.drawText(bullet, { x: ML + 4, y: y - fontSize * 0.85, size: fontSize, font: font, color: hexToRgb(st.listColor) });
|
|
829
|
-
}
|
|
830
|
-
drawLine(lineRuns, ML + indent + 4, y - fontSize * 0.85);
|
|
831
|
-
y -= lh;
|
|
832
|
-
});
|
|
833
|
-
});
|
|
834
|
-
y -= s.marB * 0.3;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
async function drawChart() {
|
|
838
|
-
var chartImg = chartImages[chartIdx];
|
|
839
|
-
chartIdx++;
|
|
840
|
-
if (!chartImg || !chartImg.dataUrl || chartImg.dataUrl === 'data:,') return;
|
|
841
|
-
try {
|
|
842
|
-
var imgBytes = Uint8Array.from(atob(chartImg.dataUrl.split(',')[1]), function(c) { return c.charCodeAt(0); });
|
|
843
|
-
var img = await doc.embedPng(imgBytes);
|
|
844
|
-
var scale = Math.min(CW / img.width, 1);
|
|
845
|
-
var drawW = img.width * scale;
|
|
846
|
-
var drawH = img.height * scale;
|
|
847
|
-
ensureSpace(drawH + 16);
|
|
848
|
-
var cx = ML + (CW - drawW) / 2;
|
|
849
|
-
page.drawImage(img, { x: cx, y: y - drawH, width: drawW, height: drawH });
|
|
850
|
-
y -= drawH + 12;
|
|
851
|
-
} catch (e) { console.warn('Chart embed failed:', e); }
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function drawHR(el) {
|
|
855
|
-
var s = el ? readComputedBlock(el) : { marT: 20, marB: 20 };
|
|
856
|
-
y -= s.marT * 0.5;
|
|
857
|
-
ensureSpace(10);
|
|
858
|
-
page.drawLine({ start: { x: ML, y: y }, end: { x: ML + CW, y: y }, thickness: 0.5, color: hexToRgb('#e2ddd6') });
|
|
859
|
-
y -= s.marB * 0.5;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
async function drawImage(imgEl) {
|
|
863
|
-
var dataUrl = imgToDataUrl(imgEl);
|
|
864
|
-
if (!dataUrl) return;
|
|
865
|
-
try {
|
|
866
|
-
var imgBytes = Uint8Array.from(atob(dataUrl.split(',')[1]), function(c) { return c.charCodeAt(0); });
|
|
867
|
-
var img = dataUrl.indexOf('image/png') > -1 ? await doc.embedPng(imgBytes) : await doc.embedJpg(imgBytes);
|
|
868
|
-
var scale = Math.min(CW / img.width, 1);
|
|
869
|
-
var drawW = img.width * scale;
|
|
870
|
-
var drawH = img.height * scale;
|
|
871
|
-
// Read border-radius from computed style
|
|
872
|
-
var imgRadius = 0;
|
|
873
|
-
if (imgEl) {
|
|
874
|
-
var cs = getComputedStyle(imgEl);
|
|
875
|
-
imgRadius = pxToPt(parseFloat(cs.borderTopLeftRadius) || 0);
|
|
876
|
-
}
|
|
877
|
-
ensureSpace(drawH + 8);
|
|
878
|
-
var cx = ML + (CW - drawW) / 2;
|
|
879
|
-
page.drawImage(img, { x: cx, y: y - drawH, width: drawW, height: drawH });
|
|
880
|
-
y -= drawH + 8;
|
|
881
|
-
} catch (e) { console.warn('Image embed failed:', e); }
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function drawTable(el) {
|
|
885
|
-
var rows = [];
|
|
886
|
-
el.querySelectorAll('tr').forEach(function(tr) {
|
|
887
|
-
var cells = [];
|
|
888
|
-
tr.querySelectorAll('th, td').forEach(function(td) {
|
|
889
|
-
cells.push({ text: td.textContent, isHeader: td.tagName === 'TH' });
|
|
890
|
-
});
|
|
891
|
-
if (cells.length) rows.push(cells);
|
|
892
|
-
});
|
|
893
|
-
if (!rows.length) return;
|
|
894
|
-
var cols = rows[0].length;
|
|
895
|
-
var colW = CW / cols;
|
|
896
|
-
var rowH = fontSize * 1.8;
|
|
897
|
-
rows.forEach(function(cells, ri) {
|
|
898
|
-
ensureSpace(rowH);
|
|
899
|
-
var fillColor = ri === 0 ? st.tableHeaderBg : (ri % 2 === 0 ? st.tableEvenBg : null);
|
|
900
|
-
if (fillColor) {
|
|
901
|
-
page.drawRectangle({ x: ML, y: y - rowH, width: CW, height: rowH, color: hexToRgb(fillColor) });
|
|
902
|
-
}
|
|
903
|
-
// Top border
|
|
904
|
-
page.drawLine({ start: { x: ML, y: y }, end: { x: ML + CW, y: y }, thickness: 0.3, color: hexToRgb(st.tableBorder) });
|
|
905
|
-
cells.forEach(function(cell, ci) {
|
|
906
|
-
var cx = ML + ci * colW + 8;
|
|
907
|
-
var f = cell.isHeader ? bold : font;
|
|
908
|
-
var txt = cell.text.substring(0, Math.floor(colW / (fontSize * 0.5)));
|
|
909
|
-
page.drawText(txt, { x: cx, y: y - fontSize * 1.1, size: fontSize * 0.88, font: f, color: hexToRgb(st.pColor) });
|
|
910
|
-
if (ci > 0) {
|
|
911
|
-
page.drawLine({ start: { x: ML + ci * colW, y: y }, end: { x: ML + ci * colW, y: y - rowH }, thickness: 0.3, color: hexToRgb(st.tableBorder) });
|
|
912
|
-
}
|
|
913
|
-
});
|
|
914
|
-
y -= rowH;
|
|
915
|
-
});
|
|
916
|
-
page.drawLine({ start: { x: ML, y: y }, end: { x: ML + CW, y: y }, thickness: 0.3, color: hexToRgb(st.tableBorder) });
|
|
917
|
-
page.drawLine({ start: { x: ML, y: y + rows.length * rowH }, end: { x: ML, y: y }, thickness: 0.3, color: hexToRgb(st.tableBorder) });
|
|
918
|
-
page.drawLine({ start: { x: ML + CW, y: y + rows.length * rowH }, end: { x: ML + CW, y: y }, thickness: 0.3, color: hexToRgb(st.tableBorder) });
|
|
919
|
-
y -= 8;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
// ── Render ──
|
|
923
|
-
|
|
924
|
-
await walk(rendered);
|
|
925
|
-
|
|
926
|
-
// Handle async image/chart embeds — wait for all pending promises
|
|
927
|
-
// pdf-lib embedPng/embedJpg return promises; we called them in sync context
|
|
928
|
-
// Re-render approach: we need to make walk async for images/charts
|
|
929
|
-
// For now, images embedded synchronously via the Promise.resolve pattern above
|
|
930
|
-
// will work because pdf-lib's embed methods are sync for already-loaded data
|
|
931
|
-
|
|
932
|
-
return doc.save();
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// ── Export orchestration ──
|
|
936
|
-
|
|
937
|
-
async function exportPDF() {
|
|
938
|
-
S.setStatus('Generating PDF\u2026');
|
|
939
|
-
try {
|
|
940
|
-
await loadPdfLib();
|
|
941
|
-
|
|
942
|
-
var closed = expandAllSections();
|
|
943
|
-
await new Promise(function(r) { requestAnimationFrame(function() { setTimeout(r, 150); }); });
|
|
944
|
-
|
|
945
|
-
var st = readPdfStyles();
|
|
946
|
-
var chartImages = S.getChartImages ? S.getChartImages() : [];
|
|
947
|
-
var pdfBytes = await renderPdf(S.renderedEl, st, chartImages);
|
|
948
|
-
restoreSections(closed);
|
|
949
|
-
|
|
950
|
-
var blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
|
951
|
-
var a = document.createElement('a');
|
|
952
|
-
a.href = URL.createObjectURL(blob);
|
|
953
|
-
a.download = (S.currentMeta.title || 'document').replace(/[^a-z0-9_-]/gi, '_') + '.pdf';
|
|
954
|
-
a.click();
|
|
955
|
-
URL.revokeObjectURL(a.href);
|
|
956
|
-
S.setStatus('PDF downloaded');
|
|
957
|
-
} catch (e) {
|
|
958
|
-
S.setStatus('PDF export failed: ' + e.message);
|
|
959
|
-
console.error(e);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
S.buildExportHTML = buildExportHTML;
|
|
964
|
-
S.expandAllSections = expandAllSections;
|
|
965
|
-
S.restoreSections = restoreSections;
|
|
966
|
-
|
|
967
|
-
var htmlToDocxLoaded = false;
|
|
968
|
-
|
|
969
|
-
function loadHtmlToDocx() {
|
|
970
|
-
return new Promise(function(resolve, reject) {
|
|
971
|
-
if (htmlToDocxLoaded) { resolve(); return; }
|
|
972
|
-
window.global = window; // polyfill for browser
|
|
973
|
-
var s = document.createElement('script');
|
|
974
|
-
s.src = 'https://cdn.jsdelivr.net/npm/@turbodocx/html-to-docx@1/dist/html-to-docx.browser.js';
|
|
975
|
-
s.onload = function() { htmlToDocxLoaded = true; resolve(); };
|
|
976
|
-
s.onerror = function() { reject(new Error('Could not load html-to-docx')); };
|
|
977
|
-
document.head.appendChild(s);
|
|
978
|
-
});
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
async function exportWord() {
|
|
982
|
-
S.setStatus('Generating Word document\u2026');
|
|
983
|
-
try {
|
|
984
|
-
await loadHtmlToDocx();
|
|
985
|
-
var closed = expandAllSections();
|
|
986
|
-
await new Promise(function(r) { requestAnimationFrame(function() { setTimeout(r, 150); }); });
|
|
987
|
-
var html = buildExportHTML();
|
|
988
|
-
restoreSections(closed);
|
|
989
|
-
var blob = await window.HTMLToDOCX(html, null, {
|
|
990
|
-
orientation: 'portrait',
|
|
991
|
-
margins: { top: 720, right: 900, bottom: 720, left: 900 },
|
|
992
|
-
});
|
|
993
|
-
var a = document.createElement('a');
|
|
994
|
-
a.href = URL.createObjectURL(blob);
|
|
995
|
-
a.download = (S.currentMeta.title || 'document').replace(/[^a-z0-9_-]/gi,'_') + '.docx';
|
|
996
|
-
a.click();
|
|
997
|
-
URL.revokeObjectURL(a.href);
|
|
998
|
-
S.setStatus('Exported Word .docx');
|
|
999
|
-
} catch (e) {
|
|
1000
|
-
S.setStatus('Word export failed: ' + e.message);
|
|
1001
|
-
console.error(e);
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
function download(filename, content) {
|
|
1006
|
-
var a = document.createElement('a');
|
|
1007
|
-
a.href = URL.createObjectURL(new Blob([content], { type: 'text/plain' }));
|
|
1008
|
-
a.download = filename;
|
|
1009
|
-
a.click();
|
|
1010
|
-
URL.revokeObjectURL(a.href);
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
// ── Export panel handlers ──────────────────────────────
|
|
1014
|
-
|
|
1015
|
-
document.getElementById('exp-pdf').addEventListener('click', exportPDF);
|
|
1016
|
-
document.getElementById('exp-word').addEventListener('click', exportWord);
|
|
1017
|
-
|
|
1018
|
-
document.getElementById('exp-raw').addEventListener('click', function() {
|
|
1019
|
-
download('document.md', S.currentBody);
|
|
1020
|
-
S.setStatus('Exported raw .md');
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
document.getElementById('exp-styled').addEventListener('click', function() {
|
|
1024
|
-
var meta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
|
|
1025
|
-
var fm = SDocYaml.serializeFrontMatter(meta);
|
|
1026
|
-
download('document.md', fm + '\n' + S.currentBody);
|
|
1027
|
-
S.setStatus('Exported styled .md with YAML front matter');
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
// ── Save as default styles ──────────────────────────────
|
|
1031
|
-
|
|
1032
|
-
function formatStyleValue(k, v) {
|
|
1033
|
-
if (typeof v !== 'object' || v === null) {
|
|
1034
|
-
return k + ': ' + JSON.stringify(v);
|
|
1035
|
-
}
|
|
1036
|
-
// light/dark theme blocks: serialize as nested block (not inline)
|
|
1037
|
-
if (k === 'light' || k === 'dark') {
|
|
1038
|
-
var blockLines = [k + ':'];
|
|
1039
|
-
Object.keys(v).forEach(function(sk) {
|
|
1040
|
-
var sv = v[sk];
|
|
1041
|
-
if (typeof sv === 'object' && sv !== null) {
|
|
1042
|
-
var inner = Object.keys(sv).map(function(a) { return a + ': ' + JSON.stringify(sv[a]); }).join(', ');
|
|
1043
|
-
blockLines.push(' ' + sk + ': { ' + inner + ' }');
|
|
1044
|
-
} else {
|
|
1045
|
-
blockLines.push(' ' + sk + ': ' + JSON.stringify(sv));
|
|
1046
|
-
}
|
|
1047
|
-
});
|
|
1048
|
-
return blockLines.join('\n');
|
|
1049
|
-
}
|
|
1050
|
-
// Default: inline object
|
|
1051
|
-
var inner = Object.keys(v).map(function(a) { return a + ': ' + JSON.stringify(v[a]); }).join(', ');
|
|
1052
|
-
return k + ': { ' + inner + ' }';
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
function buildStylesYaml() {
|
|
1056
|
-
var styles = S.collectStyles();
|
|
1057
|
-
var lines = [];
|
|
1058
|
-
Object.keys(styles).forEach(function(k) {
|
|
1059
|
-
lines.push(formatStyleValue(k, styles[k]));
|
|
1060
|
-
});
|
|
1061
|
-
return lines.join('\n');
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
var saveDefaultCmd = '';
|
|
1065
|
-
|
|
1066
|
-
function refreshSaveDefaultPreview() {
|
|
1067
|
-
var yaml = buildStylesYaml();
|
|
1068
|
-
saveDefaultCmd = "mkdir -p ~/.sdocs && cat > ~/.sdocs/styles.yaml << 'SDOCS'\n" + yaml + '\nSDOCS';
|
|
1069
|
-
document.getElementById('save-default-display').textContent = saveDefaultCmd;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
document.querySelector('[data-target="body-save-default"]').addEventListener('click', function() {
|
|
1073
|
-
// Delay so the panel-body open class is toggled first
|
|
1074
|
-
setTimeout(refreshSaveDefaultPreview, 0);
|
|
1075
|
-
});
|
|
1076
|
-
|
|
1077
|
-
document.getElementById('btn-copy-default').addEventListener('click', async function() {
|
|
1078
|
-
refreshSaveDefaultPreview();
|
|
1079
|
-
try {
|
|
1080
|
-
await navigator.clipboard.writeText(saveDefaultCmd);
|
|
1081
|
-
var msg = document.getElementById('save-default-msg');
|
|
1082
|
-
msg.textContent = 'Copied! Paste in your terminal to save defaults.';
|
|
1083
|
-
msg.style.display = 'block';
|
|
1084
|
-
setTimeout(function() { msg.style.display = 'none'; }, 4000);
|
|
1085
|
-
} catch (e) {
|
|
1086
|
-
S.setStatus('Could not copy to clipboard');
|
|
1087
|
-
}
|
|
1088
|
-
});
|
|
1089
|
-
|
|
1090
|
-
})();
|