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.
@@ -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,'&lt;');
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
- })();