sdocs-dev 1.2.0 → 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 +282 -38
- package/package.json +6 -3
- package/public/{brotli-wasm.js → brotli-wasm-v1.js} +5 -6
- 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/index.html +137 -43
- package/public/sdoc.md +542 -0
- package/public/sdocs-app.js +222 -63
- 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 +5 -3
- package/public/vendor/purify.min.js +3 -0
- package/server.js +51 -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,12 +298,31 @@ 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
305
|
// ── Compression helpers (brotli + base64url) ──
|
|
201
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
|
+
}
|
|
325
|
+
|
|
202
326
|
function toBase64Url(bytes) {
|
|
203
327
|
var bin = Array.from(new Uint8Array(bytes), function(b) { return String.fromCharCode(b); }).join('');
|
|
204
328
|
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
@@ -212,11 +336,25 @@ function fromBase64Url(str) {
|
|
|
212
336
|
return bytes;
|
|
213
337
|
}
|
|
214
338
|
|
|
215
|
-
async function
|
|
216
|
-
await BrotliWasm.ready;
|
|
339
|
+
async function compressDeflate(text) {
|
|
217
340
|
var encoded = new TextEncoder().encode(text);
|
|
218
|
-
var
|
|
219
|
-
|
|
341
|
+
var cs = new CompressionStream('deflate-raw');
|
|
342
|
+
var writer = cs.writable.getWriter();
|
|
343
|
+
writer.write(encoded);
|
|
344
|
+
writer.close();
|
|
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);
|
|
357
|
+
}
|
|
220
358
|
}
|
|
221
359
|
|
|
222
360
|
async function decompressDeflate(bytes) {
|
|
@@ -224,30 +362,20 @@ async function decompressDeflate(bytes) {
|
|
|
224
362
|
var writer = ds.writable.getWriter();
|
|
225
363
|
writer.write(bytes);
|
|
226
364
|
writer.close();
|
|
227
|
-
|
|
228
|
-
var reader = ds.readable.getReader();
|
|
229
|
-
while (true) {
|
|
230
|
-
var result = await reader.read();
|
|
231
|
-
if (result.done) break;
|
|
232
|
-
chunks.push(result.value);
|
|
233
|
-
}
|
|
234
|
-
var total = chunks.reduce(function(n, c) { return n + c.length; }, 0);
|
|
235
|
-
var buf = new Uint8Array(total);
|
|
236
|
-
var offset = 0;
|
|
237
|
-
for (var i = 0; i < chunks.length; i++) { buf.set(chunks[i], offset); offset += chunks[i].length; }
|
|
238
|
-
return new TextDecoder().decode(buf);
|
|
365
|
+
return new TextDecoder().decode(await readAllChunks(ds.readable));
|
|
239
366
|
}
|
|
240
367
|
|
|
241
368
|
async function decompressText(b64url) {
|
|
242
|
-
await BrotliWasm.ready;
|
|
243
369
|
var bytes = fromBase64Url(b64url);
|
|
244
|
-
// Try brotli first, fall back to deflate for old URLs
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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 (_) {}
|
|
250
377
|
}
|
|
378
|
+
return decompressDeflate(bytes);
|
|
251
379
|
}
|
|
252
380
|
|
|
253
381
|
// ── Auto-save to URL hash ──────────────────────────
|
|
@@ -285,6 +413,7 @@ function syncAll(source) {
|
|
|
285
413
|
try {
|
|
286
414
|
if (source === 'controls') {
|
|
287
415
|
S._isDefaultState = false;
|
|
416
|
+
S.invalidateLocalMeta();
|
|
288
417
|
S.currentMeta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
|
|
289
418
|
S.rawEl.value = SDocYaml.serializeFrontMatter(S.currentMeta) + '\n' + S.currentBody;
|
|
290
419
|
updateHash();
|
|
@@ -331,6 +460,9 @@ contentArea.addEventListener('drop', function(e) {
|
|
|
331
460
|
});
|
|
332
461
|
|
|
333
462
|
S.rawEl.addEventListener('input', function() {
|
|
463
|
+
// Content has diverged from the on-disk file — drop local paths.
|
|
464
|
+
S.invalidateLocalMeta();
|
|
465
|
+
|
|
334
466
|
clearTimeout(S._rawSyncTimer);
|
|
335
467
|
S._rawSyncTimer = setTimeout(function() { syncAll('raw'); }, 300);
|
|
336
468
|
});
|
|
@@ -368,6 +500,10 @@ function setMode(mode, skipHash) {
|
|
|
368
500
|
S.enterWriteMode();
|
|
369
501
|
}
|
|
370
502
|
|
|
503
|
+
if (mode === 'read') {
|
|
504
|
+
document.getElementById('content-area').focus();
|
|
505
|
+
}
|
|
506
|
+
|
|
371
507
|
if (!skipHash) updateHash();
|
|
372
508
|
}
|
|
373
509
|
|
|
@@ -385,14 +521,6 @@ document.getElementById('btn-new').addEventListener('click', function() {
|
|
|
385
521
|
startNewDocument();
|
|
386
522
|
});
|
|
387
523
|
|
|
388
|
-
document.getElementById('btn-expand-all').addEventListener('click', function() {
|
|
389
|
-
S.renderedEl.querySelectorAll('.md-section-body').forEach(function(b) { b.classList.add('open'); });
|
|
390
|
-
S.renderedEl.querySelectorAll('.section-toggle').forEach(function(t) { t.classList.add('open'); });
|
|
391
|
-
});
|
|
392
|
-
document.getElementById('btn-collapse-all').addEventListener('click', function() {
|
|
393
|
-
S.renderedEl.querySelectorAll('.md-section-body').forEach(function(b) { b.classList.remove('open'); });
|
|
394
|
-
S.renderedEl.querySelectorAll('.section-toggle').forEach(function(t) { t.classList.remove('open'); });
|
|
395
|
-
});
|
|
396
524
|
|
|
397
525
|
document.getElementById('right-header').addEventListener('click', function() {
|
|
398
526
|
if (window.innerWidth <= 768) {
|
|
@@ -448,7 +576,7 @@ document.querySelectorAll('.sub-header').forEach(function(h) {
|
|
|
448
576
|
// ── Default content ──────────────────────────────────
|
|
449
577
|
|
|
450
578
|
var DEFAULT_MD = '';
|
|
451
|
-
var _defaultReady = fetch('/public/
|
|
579
|
+
var _defaultReady = fetch('/public/sdoc.md').then(function(r) { return r.text(); }).then(function(t) { DEFAULT_MD = t; });
|
|
452
580
|
|
|
453
581
|
// ── Register on SDocs for cross-module access ──────────
|
|
454
582
|
|
|
@@ -477,6 +605,18 @@ S.setStatus = setStatus;
|
|
|
477
605
|
S.setMode = setMode;
|
|
478
606
|
S.render = render;
|
|
479
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
|
+
};
|
|
480
620
|
|
|
481
621
|
// Sync theme tabs to initial theme
|
|
482
622
|
S.updateThemeTabs(S.activeTheme);
|
|
@@ -498,14 +638,33 @@ async function loadFromHash() {
|
|
|
498
638
|
var stylesParam = params.get('styles');
|
|
499
639
|
var themeParam = params.get('theme');
|
|
500
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
|
+
}
|
|
501
659
|
|
|
660
|
+
S._loadingDocument = true;
|
|
502
661
|
S.resetAllStyles();
|
|
503
662
|
|
|
504
663
|
if (mdParam) {
|
|
505
664
|
try {
|
|
506
665
|
S._isDefaultState = false;
|
|
507
666
|
var text = await decompressText(mdParam);
|
|
508
|
-
loadText(text
|
|
667
|
+
loadText(text);
|
|
509
668
|
} catch (e) {
|
|
510
669
|
console.warn('sdocs-dev: could not decode hash', e);
|
|
511
670
|
}
|