sdocs-dev 1.1.2 → 1.3.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/README.md +149 -0
- package/bin/sdocs-dev.js +303 -43
- package/package.json +9 -3
- package/public/brotli-wasm-v1.js +518 -0
- package/public/brotli_wasm_bg.wasm +0 -0
- package/public/css/layout.css +3 -4
- package/public/css/mobile.css +7 -0
- package/public/css/panel.css +1 -1
- package/public/css/rendered.css +184 -1
- package/public/images/examples.png +0 -0
- package/public/index.html +137 -42
- package/public/sdoc.md +542 -0
- package/public/sdocs-app.js +223 -66
- package/public/sdocs-charts.js +905 -0
- package/public/sdocs-controls.js +135 -53
- package/public/sdocs-export.js +939 -61
- package/public/sdocs-slugify.js +12 -0
- package/public/sdocs-state.js +14 -0
- package/public/sdocs-styles.js +180 -254
- package/public/sdocs-theme.js +21 -19
- package/public/sdocs-write.js +4 -1
- package/public/sw.js +6 -2
- package/public/vendor/purify.min.js +3 -0
- package/server.js +52 -3
- package/public/default.md +0 -255
package/public/sdocs-app.js
CHANGED
|
@@ -9,12 +9,12 @@ var S = SDocs;
|
|
|
9
9
|
var LINK_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>';
|
|
10
10
|
var COPY_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
11
11
|
var CHECK_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
|
|
12
|
+
var CHEVRON_SVG = '<span class="section-toggle"><svg width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M3 2l4 3-4 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></span>';
|
|
13
|
+
var COPY_FEEDBACK_MS = 1500;
|
|
12
14
|
|
|
13
15
|
// ── Slugify + section helpers ──────────────────────
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
return text.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
17
|
-
}
|
|
17
|
+
var slugify = SDocSlugify.slugify;
|
|
18
18
|
|
|
19
19
|
function buildSectionUrl(slug) {
|
|
20
20
|
var base = window.location.origin + window.location.pathname;
|
|
@@ -50,15 +50,11 @@ function getSectionMarkdown(headingIndex) {
|
|
|
50
50
|
return lines.slice(target.line, endLine).join('\n').trimEnd();
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// ── Render ──────────────────────────────────
|
|
54
|
-
|
|
55
|
-
function render() {
|
|
56
|
-
var oldSpacer = S.renderedEl.querySelector('.sec-scroll-spacer');
|
|
57
|
-
if (oldSpacer) oldSpacer.remove();
|
|
58
|
-
S.renderedEl.innerHTML = marked.parse(S.currentBody);
|
|
53
|
+
// ── Render sub-functions ──────────────────────────────────
|
|
59
54
|
|
|
55
|
+
function attachHeadingAnchors(container) {
|
|
60
56
|
var slugCounts = {};
|
|
61
|
-
var allHeadings = [].slice.call(
|
|
57
|
+
var allHeadings = [].slice.call(container.querySelectorAll('h1, h2, h3, h4'));
|
|
62
58
|
allHeadings.forEach(function(h, idx) {
|
|
63
59
|
var slug = slugify(h.textContent);
|
|
64
60
|
if (!slug) slug = 'section';
|
|
@@ -78,7 +74,7 @@ function render() {
|
|
|
78
74
|
e.preventDefault();
|
|
79
75
|
navigator.clipboard.writeText(buildSectionUrl(slug)).then(function() {
|
|
80
76
|
anchor.innerHTML = CHECK_SVG;
|
|
81
|
-
setTimeout(function() { anchor.innerHTML = LINK_SVG; },
|
|
77
|
+
setTimeout(function() { anchor.innerHTML = LINK_SVG; }, COPY_FEEDBACK_MS);
|
|
82
78
|
});
|
|
83
79
|
});
|
|
84
80
|
h.appendChild(anchor);
|
|
@@ -94,57 +90,75 @@ function render() {
|
|
|
94
90
|
var md = getSectionMarkdown(hIdx);
|
|
95
91
|
navigator.clipboard.writeText(md).then(function() {
|
|
96
92
|
copyBtn.innerHTML = CHECK_SVG;
|
|
97
|
-
setTimeout(function() { copyBtn.innerHTML = COPY_SVG; },
|
|
93
|
+
setTimeout(function() { copyBtn.innerHTML = COPY_SVG; }, COPY_FEEDBACK_MS);
|
|
98
94
|
});
|
|
99
95
|
});
|
|
100
96
|
h.appendChild(copyBtn);
|
|
101
97
|
});
|
|
98
|
+
}
|
|
102
99
|
|
|
103
|
-
|
|
100
|
+
function attachCodeCopyButtons(container) {
|
|
101
|
+
container.querySelectorAll('pre').forEach(function(pre) {
|
|
104
102
|
var wrapper = document.createElement('div');
|
|
105
103
|
wrapper.className = 'pre-wrapper';
|
|
106
104
|
pre.parentNode.insertBefore(wrapper, pre);
|
|
107
105
|
wrapper.appendChild(pre);
|
|
108
106
|
var btn = document.createElement('button');
|
|
109
107
|
btn.className = 'copy-btn';
|
|
110
|
-
btn.innerHTML =
|
|
108
|
+
btn.innerHTML = COPY_SVG;
|
|
111
109
|
btn.title = 'Copy code';
|
|
112
110
|
btn.addEventListener('click', function() {
|
|
113
111
|
var code = pre.querySelector('code');
|
|
114
112
|
navigator.clipboard.writeText(code ? code.textContent : pre.textContent).then(function() {
|
|
115
|
-
btn.innerHTML =
|
|
116
|
-
setTimeout(function() {
|
|
117
|
-
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
118
|
-
}, 1500);
|
|
113
|
+
btn.innerHTML = CHECK_SVG;
|
|
114
|
+
setTimeout(function() { btn.innerHTML = COPY_SVG; }, COPY_FEEDBACK_MS);
|
|
119
115
|
});
|
|
120
116
|
});
|
|
121
117
|
wrapper.appendChild(btn);
|
|
122
118
|
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
var SECTION_LEVELS = { H2: 2, H3: 3, H4: 4 };
|
|
122
|
+
|
|
123
|
+
function buildCollapsibleSections(container) {
|
|
124
|
+
// H1 expand/collapse toggle (controls all sections below)
|
|
125
|
+
container.querySelectorAll('h1').forEach(function(h1) {
|
|
126
|
+
h1.insertAdjacentHTML('afterbegin', CHEVRON_SVG);
|
|
127
|
+
h1.style.cursor = 'pointer';
|
|
128
|
+
h1.addEventListener('click', function(e) {
|
|
129
|
+
if (e.target.closest('.header-anchor') || e.target.closest('.header-copy-btn')) return;
|
|
130
|
+
var toggle = h1.querySelector('.section-toggle');
|
|
131
|
+
var isOpen = toggle.classList.toggle('open');
|
|
132
|
+
container.querySelectorAll('.md-section-body').forEach(function(b) { b.classList.toggle('open', isOpen); });
|
|
133
|
+
container.querySelectorAll('.md-section > h2 > .section-toggle, .md-section > h3 > .section-toggle, .md-section > h4 > .section-toggle').forEach(function(t) { t.classList.toggle('open', isOpen); });
|
|
134
|
+
});
|
|
135
|
+
});
|
|
123
136
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
var children = [].slice.call(
|
|
128
|
-
var
|
|
137
|
+
// Nest H2/H3/H4 into collapsible section wrappers.
|
|
138
|
+
// Each stack frame tracks its heading level so siblings are siblings
|
|
139
|
+
// even when intermediate levels are skipped (e.g. h1 → h4 directly).
|
|
140
|
+
var children = [].slice.call(container.children);
|
|
141
|
+
var stack = [{ body: container, level: 0 }];
|
|
129
142
|
children.forEach(function(child) {
|
|
130
143
|
var level = SECTION_LEVELS[child.tagName];
|
|
131
144
|
if (level) {
|
|
132
|
-
while (
|
|
145
|
+
while (stack[stack.length - 1].level >= level) stack.pop();
|
|
133
146
|
var sectionDiv = document.createElement('div');
|
|
134
147
|
sectionDiv.className = 'md-section';
|
|
135
148
|
var sectionBody = document.createElement('div');
|
|
136
149
|
sectionBody.className = 'md-section-body';
|
|
137
|
-
child.insertAdjacentHTML('afterbegin',
|
|
138
|
-
|
|
150
|
+
child.insertAdjacentHTML('afterbegin', CHEVRON_SVG);
|
|
151
|
+
stack[stack.length - 1].body.appendChild(sectionDiv);
|
|
139
152
|
sectionDiv.appendChild(child);
|
|
140
153
|
sectionDiv.appendChild(sectionBody);
|
|
141
|
-
|
|
154
|
+
stack.push({ body: sectionBody, level: level });
|
|
142
155
|
} else {
|
|
143
|
-
|
|
156
|
+
stack[stack.length - 1].body.appendChild(child);
|
|
144
157
|
}
|
|
145
158
|
});
|
|
146
159
|
|
|
147
|
-
|
|
160
|
+
// Attach click handlers for section heading toggles
|
|
161
|
+
container.querySelectorAll('.md-section > h2, .md-section > h3, .md-section > h4').forEach(function(heading) {
|
|
148
162
|
heading.addEventListener('click', function(e) {
|
|
149
163
|
if (e.target.closest('.header-anchor') || e.target.closest('.header-copy-btn')) return;
|
|
150
164
|
var yBefore = heading.getBoundingClientRect().top;
|
|
@@ -163,6 +177,96 @@ function render() {
|
|
|
163
177
|
});
|
|
164
178
|
}
|
|
165
179
|
|
|
180
|
+
// ── Render (orchestrator) ──────────────────────────────────
|
|
181
|
+
|
|
182
|
+
function render() {
|
|
183
|
+
S.destroyCharts();
|
|
184
|
+
var oldSpacer = S.renderedEl.querySelector('.sec-scroll-spacer');
|
|
185
|
+
if (oldSpacer) oldSpacer.remove();
|
|
186
|
+
S.renderedEl.innerHTML = DOMPurify.sanitize(marked.parse(S.currentBody));
|
|
187
|
+
|
|
188
|
+
attachHeadingAnchors(S.renderedEl);
|
|
189
|
+
attachCodeCopyButtons(S.renderedEl);
|
|
190
|
+
buildCollapsibleSections(S.renderedEl);
|
|
191
|
+
S.processCharts(S.renderedEl);
|
|
192
|
+
renderFileInfoCard();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── File-info card ─────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
function renderFileInfoCard() {
|
|
198
|
+
var card = document.getElementById('file-info-card');
|
|
199
|
+
if (!card) return;
|
|
200
|
+
var meta = S.currentMeta || {};
|
|
201
|
+
var local = S.localMeta || {};
|
|
202
|
+
var rowsEl = card.querySelector('.fic-rows');
|
|
203
|
+
|
|
204
|
+
var rows = [];
|
|
205
|
+
if (meta.file) rows.push({ key: 'file', label: 'File', value: meta.file, local: false });
|
|
206
|
+
if (local.path) rows.push({ key: 'path', label: 'Path', value: local.path, local: true });
|
|
207
|
+
if (local.fullPath) rows.push({ key: 'fullPath', label: 'Full', value: local.fullPath, local: true });
|
|
208
|
+
|
|
209
|
+
if (rows.length === 0) {
|
|
210
|
+
card.hidden = true;
|
|
211
|
+
rowsEl.innerHTML = '';
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
card.hidden = false;
|
|
216
|
+
var note = card.querySelector('.fic-privacy-note');
|
|
217
|
+
if (note) note.hidden = !rows.some(function(r) { return r.local; });
|
|
218
|
+
rowsEl.innerHTML = rows.map(function(r) {
|
|
219
|
+
var pill = r.local ? '<span class="fic-local-pill" title="Only visible on this device — not included in shared sdocs">local only</span>' : '';
|
|
220
|
+
return '<div class="fic-row" data-key="' + r.key + '">'
|
|
221
|
+
+ '<span class="fic-label">' + r.label + '</span>'
|
|
222
|
+
+ '<span class="fic-value">' + escapeHtml(r.value) + '</span>'
|
|
223
|
+
+ pill
|
|
224
|
+
+ '<button class="fic-copy" title="Copy ' + r.label.toLowerCase() + '">' + COPY_SVG + '</button>'
|
|
225
|
+
+ '</div>';
|
|
226
|
+
}).join('');
|
|
227
|
+
|
|
228
|
+
rowsEl.querySelectorAll('.fic-copy').forEach(function(btn) {
|
|
229
|
+
btn.addEventListener('click', function() {
|
|
230
|
+
var row = btn.closest('.fic-row');
|
|
231
|
+
var val = row.querySelector('.fic-value').textContent;
|
|
232
|
+
copyWithIconFeedback(val, btn);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function escapeHtml(s) {
|
|
238
|
+
return String(s).replace(/[&<>"']/g, function(c) {
|
|
239
|
+
return ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' })[c];
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Swap the button's <svg> to a check, restore after COPY_FEEDBACK_MS.
|
|
244
|
+
// Works for both icon-only and icon+label buttons.
|
|
245
|
+
function copyWithIconFeedback(text, btn) {
|
|
246
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
247
|
+
if (!btn) return;
|
|
248
|
+
var svg = btn.querySelector('svg');
|
|
249
|
+
if (svg) {
|
|
250
|
+
svg.outerHTML = CHECK_SVG;
|
|
251
|
+
setTimeout(function() {
|
|
252
|
+
var current = btn.querySelector('svg');
|
|
253
|
+
if (current) current.outerHTML = COPY_SVG;
|
|
254
|
+
}, COPY_FEEDBACK_MS);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
260
|
+
var copyAll = document.getElementById('fic-copy-all');
|
|
261
|
+
if (copyAll) {
|
|
262
|
+
copyAll.addEventListener('click', function() {
|
|
263
|
+
var meta = Object.assign({}, S.currentMeta || {}, { styles: S.collectStyles() });
|
|
264
|
+
var full = SDocYaml.serializeFrontMatter(meta) + '\n' + (S.currentBody || '');
|
|
265
|
+
copyWithIconFeedback(full, copyAll);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
166
270
|
// ── Status ──────────────────────────────────
|
|
167
271
|
|
|
168
272
|
function setStatus(msg) {
|
|
@@ -175,6 +279,7 @@ function loadText(text, filename) {
|
|
|
175
279
|
var parsed = SDocYaml.parseFrontMatter(text);
|
|
176
280
|
S.currentMeta = parsed.meta;
|
|
177
281
|
S.currentBody = parsed.body;
|
|
282
|
+
S.chartStyles = (parsed.meta.styles && parsed.meta.styles.chart) || null;
|
|
178
283
|
render();
|
|
179
284
|
if (parsed.meta.styles) S.applyStylesFromMeta(parsed.meta.styles);
|
|
180
285
|
// Re-apply theme defaults for standalone colors (front matter may have
|
|
@@ -193,11 +298,30 @@ function loadText(text, filename) {
|
|
|
193
298
|
});
|
|
194
299
|
S.currentMeta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
|
|
195
300
|
S.rawEl.value = SDocYaml.serializeFrontMatter(S.currentMeta) + '\n' + S.currentBody;
|
|
196
|
-
setStatus(filename ? 'Loaded: ' + filename : '
|
|
301
|
+
setStatus(filename ? 'Loaded: ' + filename : '');
|
|
197
302
|
syncAll('load');
|
|
198
303
|
}
|
|
199
304
|
|
|
200
|
-
// ── Compression helpers (
|
|
305
|
+
// ── Compression helpers (brotli + base64url) ──
|
|
306
|
+
|
|
307
|
+
function concatChunks(chunks) {
|
|
308
|
+
var total = chunks.reduce(function(n, c) { return n + c.length; }, 0);
|
|
309
|
+
var buf = new Uint8Array(total);
|
|
310
|
+
var offset = 0;
|
|
311
|
+
for (var i = 0; i < chunks.length; i++) { buf.set(chunks[i], offset); offset += chunks[i].length; }
|
|
312
|
+
return buf;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function readAllChunks(readable) {
|
|
316
|
+
var chunks = [];
|
|
317
|
+
var reader = readable.getReader();
|
|
318
|
+
while (true) {
|
|
319
|
+
var result = await reader.read();
|
|
320
|
+
if (result.done) break;
|
|
321
|
+
chunks.push(result.value);
|
|
322
|
+
}
|
|
323
|
+
return concatChunks(chunks);
|
|
324
|
+
}
|
|
201
325
|
|
|
202
326
|
function toBase64Url(bytes) {
|
|
203
327
|
var bin = Array.from(new Uint8Array(bytes), function(b) { return String.fromCharCode(b); }).join('');
|
|
@@ -212,44 +336,46 @@ function fromBase64Url(str) {
|
|
|
212
336
|
return bytes;
|
|
213
337
|
}
|
|
214
338
|
|
|
215
|
-
async function
|
|
339
|
+
async function compressDeflate(text) {
|
|
216
340
|
var encoded = new TextEncoder().encode(text);
|
|
217
341
|
var cs = new CompressionStream('deflate-raw');
|
|
218
342
|
var writer = cs.writable.getWriter();
|
|
219
343
|
writer.write(encoded);
|
|
220
344
|
writer.close();
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
345
|
+
return toBase64Url(await readAllChunks(cs.readable));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function compressText(text) {
|
|
349
|
+
if (typeof BrotliWasm === 'undefined') return compressDeflate(text);
|
|
350
|
+
try {
|
|
351
|
+
await BrotliWasm.ready;
|
|
352
|
+
var encoded = new TextEncoder().encode(text);
|
|
353
|
+
var compressed = BrotliWasm.compress(encoded, { quality: 11 });
|
|
354
|
+
return toBase64Url(compressed);
|
|
355
|
+
} catch (_) {
|
|
356
|
+
return compressDeflate(text);
|
|
227
357
|
}
|
|
228
|
-
var total = chunks.reduce(function(n, c) { return n + c.length; }, 0);
|
|
229
|
-
var buf = new Uint8Array(total);
|
|
230
|
-
var offset = 0;
|
|
231
|
-
for (var i = 0; i < chunks.length; i++) { buf.set(chunks[i], offset); offset += chunks[i].length; }
|
|
232
|
-
return toBase64Url(buf);
|
|
233
358
|
}
|
|
234
359
|
|
|
235
|
-
async function
|
|
236
|
-
var bytes = fromBase64Url(b64url);
|
|
360
|
+
async function decompressDeflate(bytes) {
|
|
237
361
|
var ds = new DecompressionStream('deflate-raw');
|
|
238
362
|
var writer = ds.writable.getWriter();
|
|
239
363
|
writer.write(bytes);
|
|
240
364
|
writer.close();
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
365
|
+
return new TextDecoder().decode(await readAllChunks(ds.readable));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function decompressText(b64url) {
|
|
369
|
+
var bytes = fromBase64Url(b64url);
|
|
370
|
+
// Try brotli first, fall back to deflate for old URLs or missing WASM
|
|
371
|
+
if (typeof BrotliWasm !== 'undefined') {
|
|
372
|
+
try {
|
|
373
|
+
await BrotliWasm.ready;
|
|
374
|
+
var decompressed = BrotliWasm.decompress(bytes);
|
|
375
|
+
return new TextDecoder().decode(decompressed);
|
|
376
|
+
} catch (_) {}
|
|
247
377
|
}
|
|
248
|
-
|
|
249
|
-
var buf = new Uint8Array(total);
|
|
250
|
-
var offset = 0;
|
|
251
|
-
for (var i = 0; i < chunks.length; i++) { buf.set(chunks[i], offset); offset += chunks[i].length; }
|
|
252
|
-
return new TextDecoder().decode(buf);
|
|
378
|
+
return decompressDeflate(bytes);
|
|
253
379
|
}
|
|
254
380
|
|
|
255
381
|
// ── Auto-save to URL hash ──────────────────────────
|
|
@@ -287,6 +413,7 @@ function syncAll(source) {
|
|
|
287
413
|
try {
|
|
288
414
|
if (source === 'controls') {
|
|
289
415
|
S._isDefaultState = false;
|
|
416
|
+
S.invalidateLocalMeta();
|
|
290
417
|
S.currentMeta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
|
|
291
418
|
S.rawEl.value = SDocYaml.serializeFrontMatter(S.currentMeta) + '\n' + S.currentBody;
|
|
292
419
|
updateHash();
|
|
@@ -333,6 +460,9 @@ contentArea.addEventListener('drop', function(e) {
|
|
|
333
460
|
});
|
|
334
461
|
|
|
335
462
|
S.rawEl.addEventListener('input', function() {
|
|
463
|
+
// Content has diverged from the on-disk file — drop local paths.
|
|
464
|
+
S.invalidateLocalMeta();
|
|
465
|
+
|
|
336
466
|
clearTimeout(S._rawSyncTimer);
|
|
337
467
|
S._rawSyncTimer = setTimeout(function() { syncAll('raw'); }, 300);
|
|
338
468
|
});
|
|
@@ -370,6 +500,10 @@ function setMode(mode, skipHash) {
|
|
|
370
500
|
S.enterWriteMode();
|
|
371
501
|
}
|
|
372
502
|
|
|
503
|
+
if (mode === 'read') {
|
|
504
|
+
document.getElementById('content-area').focus();
|
|
505
|
+
}
|
|
506
|
+
|
|
373
507
|
if (!skipHash) updateHash();
|
|
374
508
|
}
|
|
375
509
|
|
|
@@ -387,14 +521,6 @@ document.getElementById('btn-new').addEventListener('click', function() {
|
|
|
387
521
|
startNewDocument();
|
|
388
522
|
});
|
|
389
523
|
|
|
390
|
-
document.getElementById('btn-expand-all').addEventListener('click', function() {
|
|
391
|
-
S.renderedEl.querySelectorAll('.md-section-body').forEach(function(b) { b.classList.add('open'); });
|
|
392
|
-
S.renderedEl.querySelectorAll('.section-toggle').forEach(function(t) { t.classList.add('open'); });
|
|
393
|
-
});
|
|
394
|
-
document.getElementById('btn-collapse-all').addEventListener('click', function() {
|
|
395
|
-
S.renderedEl.querySelectorAll('.md-section-body').forEach(function(b) { b.classList.remove('open'); });
|
|
396
|
-
S.renderedEl.querySelectorAll('.section-toggle').forEach(function(t) { t.classList.remove('open'); });
|
|
397
|
-
});
|
|
398
524
|
|
|
399
525
|
document.getElementById('right-header').addEventListener('click', function() {
|
|
400
526
|
if (window.innerWidth <= 768) {
|
|
@@ -450,7 +576,7 @@ document.querySelectorAll('.sub-header').forEach(function(h) {
|
|
|
450
576
|
// ── Default content ──────────────────────────────────
|
|
451
577
|
|
|
452
578
|
var DEFAULT_MD = '';
|
|
453
|
-
var _defaultReady = fetch('/public/
|
|
579
|
+
var _defaultReady = fetch('/public/sdoc.md').then(function(r) { return r.text(); }).then(function(t) { DEFAULT_MD = t; });
|
|
454
580
|
|
|
455
581
|
// ── Register on SDocs for cross-module access ──────────
|
|
456
582
|
|
|
@@ -479,6 +605,18 @@ S.setStatus = setStatus;
|
|
|
479
605
|
S.setMode = setMode;
|
|
480
606
|
S.render = render;
|
|
481
607
|
S.loadText = loadText;
|
|
608
|
+
S.renderFileInfoCard = renderFileInfoCard;
|
|
609
|
+
|
|
610
|
+
// Clear runtime-only local metadata (paths) once the user has edited the
|
|
611
|
+
// document, since the content no longer corresponds to the file on disk.
|
|
612
|
+
// Suppressed while a document is loading — style changes during load come from
|
|
613
|
+
// applying saved styles, not from user edits.
|
|
614
|
+
S.invalidateLocalMeta = function() {
|
|
615
|
+
if (S._loadingDocument) return;
|
|
616
|
+
if (!S.localMeta || Object.keys(S.localMeta).length === 0) return;
|
|
617
|
+
S.localMeta = {};
|
|
618
|
+
renderFileInfoCard();
|
|
619
|
+
};
|
|
482
620
|
|
|
483
621
|
// Sync theme tabs to initial theme
|
|
484
622
|
S.updateThemeTabs(S.activeTheme);
|
|
@@ -500,14 +638,33 @@ async function loadFromHash() {
|
|
|
500
638
|
var stylesParam = params.get('styles');
|
|
501
639
|
var themeParam = params.get('theme');
|
|
502
640
|
var secParam = params.get('sec');
|
|
641
|
+
var localParam = params.get('local');
|
|
642
|
+
|
|
643
|
+
// Read &local=<base64url-json> into memory, then strip it from the URL bar
|
|
644
|
+
// so anything the user copies/shares no longer contains it.
|
|
645
|
+
if (localParam) {
|
|
646
|
+
try {
|
|
647
|
+
var b64 = localParam.replace(/-/g, '+').replace(/_/g, '/');
|
|
648
|
+
while (b64.length % 4) b64 += '=';
|
|
649
|
+
S.localMeta = JSON.parse(atob(b64)) || {};
|
|
650
|
+
} catch (e) {
|
|
651
|
+
S.localMeta = {};
|
|
652
|
+
}
|
|
653
|
+
params.delete('local');
|
|
654
|
+
var newHash = params.toString();
|
|
655
|
+
var newUrl = window.location.pathname + (newHash ? '#' + newHash : '');
|
|
656
|
+
window.history.replaceState(null, '', newUrl);
|
|
657
|
+
_lastLoadedHash = newHash; // prevent re-trigger
|
|
658
|
+
}
|
|
503
659
|
|
|
660
|
+
S._loadingDocument = true;
|
|
504
661
|
S.resetAllStyles();
|
|
505
662
|
|
|
506
663
|
if (mdParam) {
|
|
507
664
|
try {
|
|
508
665
|
S._isDefaultState = false;
|
|
509
666
|
var text = await decompressText(mdParam);
|
|
510
|
-
loadText(text
|
|
667
|
+
loadText(text);
|
|
511
668
|
} catch (e) {
|
|
512
669
|
console.warn('sdocs-dev: could not decode hash', e);
|
|
513
670
|
}
|