sdocs-dev 1.1.1 → 1.1.2
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/LICENSE +21 -0
- package/bin/sdocs-dev.js +71 -6
- package/package.json +1 -1
- package/public/css/layout.css +4 -1
- package/public/css/mobile.css +1 -1
- package/public/css/rendered.css +12 -1
- package/public/css/tokens.css +5 -2
- package/public/default.md +123 -94
- 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 +20 -7
- package/public/sdocs-app.js +62 -12
- package/public/sdocs-controls.js +26 -39
- package/public/sdocs-export.js +2 -1
- package/public/sdocs-styles.js +85 -1
- package/public/sdocs-theme.js +7 -1
- package/public/sw.js +106 -0
- package/public/vendor/marked.min.js +6 -0
- package/server.js +65 -3
package/public/index.html
CHANGED
|
@@ -4,9 +4,6 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>SDocs</title>
|
|
7
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
10
7
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32' fill='none'%3E%3Crect x='2' y='2' width='28' height='28' rx='7' fill='%232563EB' fill-opacity='.15'/%3E%3Crect x='2' y='2' width='28' height='28' rx='7' stroke='%232563EB' stroke-width='2'/%3E%3Cpath d='M9 11h14M9 16h11M9 21h8' stroke='%232563EB' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E">
|
|
11
8
|
<link rel="stylesheet" href="/public/css/tokens.css">
|
|
12
9
|
<link rel="stylesheet" href="/public/css/layout.css">
|
|
@@ -37,12 +34,12 @@
|
|
|
37
34
|
<button class="btn active" id="btn-read" title="Read">
|
|
38
35
|
<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="M12 7v14"/><path d="M16 12h2"/><path d="M16 8h2"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/><path d="M6 12h2"/><path d="M6 8h2"/></svg>
|
|
39
36
|
</button>
|
|
40
|
-
<button class="btn" id="btn-write" title="Write">
|
|
41
|
-
<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="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
|
42
|
-
</button>
|
|
43
37
|
<button class="btn" id="btn-style" title="Style">
|
|
44
38
|
<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="m14.622 17.897-10.68-2.913"/><path d="M18.376 2.622a1 1 0 1 1 3.002 3.002L17.36 9.643a.5.5 0 0 0 0 .707l.944.944a2.41 2.41 0 0 1 0 3.408l-.944.944a.5.5 0 0 1-.707 0L8.354 7.348a.5.5 0 0 1 0-.707l.944-.944a2.41 2.41 0 0 1 3.408 0l.944.944a.5.5 0 0 0 .707 0z"/><path d="M9 8c-1.804 2.71-3.97 3.46-6.583 3.948a.507.507 0 0 0-.302.819l7.32 8.883a1 1 0 0 0 1.185.204C12.735 20.405 16 16.792 16 15"/></svg>
|
|
45
39
|
</button>
|
|
40
|
+
<button class="btn" id="btn-write" title="Write">
|
|
41
|
+
<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="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
|
42
|
+
</button>
|
|
46
43
|
<button class="btn" id="btn-raw" title="Raw">
|
|
47
44
|
<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="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
|
48
45
|
</button>
|
|
@@ -245,6 +242,10 @@
|
|
|
245
242
|
<label>Border</label>
|
|
246
243
|
<div class="color-pair"><input type="color" id="ctrl-bq-border-color" value="#2563eb"><button class="reset-btn" id="reset-bq-border-color" title="Reset to default">↺</button></div>
|
|
247
244
|
</div>
|
|
245
|
+
<div class="control-row">
|
|
246
|
+
<label>Background</label>
|
|
247
|
+
<div class="color-pair"><input type="color" id="ctrl-bq-bg" value="#f7f5f2"><button class="reset-btn" id="reset-bq-bg" title="Reset to default">↺</button></div>
|
|
248
|
+
</div>
|
|
248
249
|
<div class="control-row">
|
|
249
250
|
<label>Text</label>
|
|
250
251
|
<div class="color-pair"><input type="color" id="ctrl-bq-color" value="#6b6560"><button class="reset-btn" id="reset-bq-color" title="Reset to default">↺</button></div>
|
|
@@ -573,7 +574,7 @@
|
|
|
573
574
|
</span>
|
|
574
575
|
</div>
|
|
575
576
|
|
|
576
|
-
<script src="
|
|
577
|
+
<script src="/public/vendor/marked.min.js"></script>
|
|
577
578
|
<script src="/public/sdocs-yaml.js"></script>
|
|
578
579
|
<script src="/public/sdocs-styles.js"></script>
|
|
579
580
|
<script src="/public/sdocs-state.js"></script>
|
|
@@ -582,5 +583,17 @@
|
|
|
582
583
|
<script src="/public/sdocs-export.js"></script>
|
|
583
584
|
<script src="/public/sdocs-write.js"></script>
|
|
584
585
|
<script src="/public/sdocs-app.js"></script>
|
|
586
|
+
<script>
|
|
587
|
+
var APP_VERSION = '__APP_VERSION__';
|
|
588
|
+
if ('serviceWorker' in navigator) {
|
|
589
|
+
navigator.serviceWorker.register('/sw.js').then(function (reg) {
|
|
590
|
+
var msg = { type: 'check-update', version: APP_VERSION };
|
|
591
|
+
if (reg.active) reg.active.postMessage(msg);
|
|
592
|
+
navigator.serviceWorker.addEventListener('controllerchange', function () {
|
|
593
|
+
if (navigator.serviceWorker.controller) navigator.serviceWorker.controller.postMessage(msg);
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
</script>
|
|
585
598
|
</body>
|
|
586
599
|
</html>
|
package/public/sdocs-app.js
CHANGED
|
@@ -263,7 +263,10 @@ function updateHash() {
|
|
|
263
263
|
}
|
|
264
264
|
var params = new URLSearchParams();
|
|
265
265
|
if (!S._isDefaultState) {
|
|
266
|
-
var
|
|
266
|
+
var styles = SDocStyles.stripStyleDefaults(S.collectStyles());
|
|
267
|
+
var meta = Object.assign({}, S.currentMeta);
|
|
268
|
+
if (Object.keys(styles).length > 0) meta.styles = styles;
|
|
269
|
+
else delete meta.styles;
|
|
267
270
|
var full = SDocYaml.serializeFrontMatter(meta) + '\n' + S.currentBody;
|
|
268
271
|
var compressed = await compressText(full);
|
|
269
272
|
params.set('md', compressed);
|
|
@@ -415,6 +418,10 @@ document.getElementById('factory-reset-styles').addEventListener('click', functi
|
|
|
415
418
|
|
|
416
419
|
document.getElementById('toolbar-brand').addEventListener('click', function(e) {
|
|
417
420
|
e.preventDefault();
|
|
421
|
+
if (window.location.hash && window.location.hash.indexOf('md=') !== -1) {
|
|
422
|
+
window.open(window.location.origin + window.location.pathname, '_blank');
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
418
425
|
S.resetAllStyles();
|
|
419
426
|
loadText(DEFAULT_MD);
|
|
420
427
|
S._isDefaultState = true;
|
|
@@ -476,19 +483,26 @@ S.loadText = loadText;
|
|
|
476
483
|
// Sync theme tabs to initial theme
|
|
477
484
|
S.updateThemeTabs(S.activeTheme);
|
|
478
485
|
|
|
479
|
-
// ──
|
|
486
|
+
// ── Load document from URL hash ──────────────────────────────────
|
|
480
487
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
startNewDocument();
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
488
|
+
var _lastLoadedHash = null;
|
|
489
|
+
|
|
490
|
+
async function loadFromHash() {
|
|
487
491
|
var hash = window.location.hash.slice(1);
|
|
492
|
+
if (hash === _lastLoadedHash) return;
|
|
493
|
+
_lastLoadedHash = hash;
|
|
494
|
+
|
|
495
|
+
clearTimeout(S._hashTimer);
|
|
496
|
+
|
|
488
497
|
var params = hash ? new URLSearchParams(hash) : new URLSearchParams();
|
|
489
498
|
var mdParam = params.get('md');
|
|
490
499
|
var modeParam = params.get('mode');
|
|
491
500
|
var stylesParam = params.get('styles');
|
|
501
|
+
var themeParam = params.get('theme');
|
|
502
|
+
var secParam = params.get('sec');
|
|
503
|
+
|
|
504
|
+
S.resetAllStyles();
|
|
505
|
+
|
|
492
506
|
if (mdParam) {
|
|
493
507
|
try {
|
|
494
508
|
S._isDefaultState = false;
|
|
@@ -498,12 +512,29 @@ S.updateThemeTabs(S.activeTheme);
|
|
|
498
512
|
console.warn('sdocs-dev: could not decode hash', e);
|
|
499
513
|
}
|
|
500
514
|
}
|
|
515
|
+
|
|
516
|
+
if (themeParam === 'light' || themeParam === 'dark') {
|
|
517
|
+
var savedPref = localStorage.getItem('sdocs-theme');
|
|
518
|
+
S.switchThemeAndUpdate(themeParam);
|
|
519
|
+
// Restore localStorage — URL theme is view-only, not persistent
|
|
520
|
+
if (savedPref) localStorage.setItem('sdocs-theme', savedPref);
|
|
521
|
+
else localStorage.removeItem('sdocs-theme');
|
|
522
|
+
} else {
|
|
523
|
+
// Restore user's actual preferred theme (previous hash may have overridden it)
|
|
524
|
+
var preferred = S.getPreferredTheme();
|
|
525
|
+
if (preferred !== S.activeTheme) {
|
|
526
|
+
S.switchThemeAndUpdate(preferred);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
501
530
|
if (modeParam && ['read', 'style', 'write', 'raw', 'export'].includes(modeParam)) {
|
|
502
531
|
setMode(modeParam, true);
|
|
503
532
|
} else {
|
|
504
533
|
setMode('read', true);
|
|
505
534
|
}
|
|
535
|
+
|
|
506
536
|
if (!mdParam) {
|
|
537
|
+
S._isDefaultState = true;
|
|
507
538
|
loadText(DEFAULT_MD);
|
|
508
539
|
if (stylesParam) {
|
|
509
540
|
try {
|
|
@@ -515,13 +546,11 @@ S.updateThemeTabs(S.activeTheme);
|
|
|
515
546
|
}
|
|
516
547
|
}
|
|
517
548
|
|
|
518
|
-
var secParam = params.get('sec');
|
|
519
549
|
if (secParam) {
|
|
520
550
|
setTimeout(function() {
|
|
521
551
|
var target = document.getElementById(secParam);
|
|
522
552
|
if (!target) return;
|
|
523
553
|
|
|
524
|
-
// Expand the target's own section body (its content)
|
|
525
554
|
var ownSection = target.closest('.md-section');
|
|
526
555
|
if (ownSection) {
|
|
527
556
|
var ownBody = ownSection.querySelector(':scope > .md-section-body');
|
|
@@ -530,7 +559,6 @@ S.updateThemeTabs(S.activeTheme);
|
|
|
530
559
|
if (ownToggle) { ownToggle.classList.add('open'); }
|
|
531
560
|
}
|
|
532
561
|
|
|
533
|
-
// Expand all ancestor section bodies
|
|
534
562
|
var el = target.closest('.md-section-body');
|
|
535
563
|
while (el) {
|
|
536
564
|
el.classList.add('open');
|
|
@@ -542,7 +570,6 @@ S.updateThemeTabs(S.activeTheme);
|
|
|
542
570
|
el = el.parentElement ? el.parentElement.closest('.md-section-body') : null;
|
|
543
571
|
}
|
|
544
572
|
|
|
545
|
-
// Add scroll spacer so the target can reach the top
|
|
546
573
|
var spacerNeeded = contentArea.clientHeight - (contentArea.scrollHeight - target.offsetTop);
|
|
547
574
|
if (spacerNeeded > 0) {
|
|
548
575
|
var spacer = document.createElement('div');
|
|
@@ -554,8 +581,31 @@ S.updateThemeTabs(S.activeTheme);
|
|
|
554
581
|
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
555
582
|
}, 200);
|
|
556
583
|
}
|
|
584
|
+
|
|
585
|
+
if (!secParam) {
|
|
586
|
+
contentArea.scrollTop = 0;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ── Init ──────────────────────────────────
|
|
591
|
+
|
|
592
|
+
(async function () {
|
|
593
|
+
await _defaultReady;
|
|
594
|
+
if (window.location.pathname === '/new') {
|
|
595
|
+
startNewDocument();
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
await loadFromHash();
|
|
557
599
|
}());
|
|
558
600
|
|
|
601
|
+
window.addEventListener('hashchange', function () {
|
|
602
|
+
loadFromHash();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
window.addEventListener('popstate', function () {
|
|
606
|
+
loadFromHash();
|
|
607
|
+
});
|
|
608
|
+
|
|
559
609
|
// ── Toolbar scroll hints (fade + bounce-peek) ──────
|
|
560
610
|
|
|
561
611
|
function initScrollHint(el) {
|
package/public/sdocs-controls.js
CHANGED
|
@@ -51,7 +51,7 @@ var STANDALONE_COLOR_IDS = new Set(SDocStyles.STANDALONE_COLOR_IDS);
|
|
|
51
51
|
'ctrl-h1-weight','ctrl-h2-weight','ctrl-h3-weight','ctrl-h4-weight',
|
|
52
52
|
'ctrl-bg-color','ctrl-link-color','ctrl-link-decoration',
|
|
53
53
|
'ctrl-code-font','ctrl-code-bg','ctrl-code-color',
|
|
54
|
-
'ctrl-bq-border-color','ctrl-bq-color',
|
|
54
|
+
'ctrl-bq-border-color','ctrl-bq-bg','ctrl-bq-color',
|
|
55
55
|
].forEach(function(id) {
|
|
56
56
|
var handler = function() { if (STANDALONE_COLOR_IDS.has(id)) S.overriddenColors.add(id); applyCtrl(id); };
|
|
57
57
|
document.getElementById(id).addEventListener('input', handler);
|
|
@@ -97,7 +97,7 @@ document.getElementById('reset-h4-color').addEventListener('click', function()
|
|
|
97
97
|
document.getElementById('reset-p-color').addEventListener('click', function() { resetColorValue('ctrl-p-color'); S.syncAll('controls'); });
|
|
98
98
|
document.getElementById('reset-list-color').addEventListener('click', function() { resetColorValue('ctrl-list-color'); S.syncAll('controls'); });
|
|
99
99
|
|
|
100
|
-
['ctrl-bg-color','ctrl-link-color','ctrl-code-bg','ctrl-code-color','ctrl-bq-border-color','ctrl-bq-color'].forEach(function(ctrlId) {
|
|
100
|
+
['ctrl-bg-color','ctrl-link-color','ctrl-code-bg','ctrl-code-color','ctrl-bq-border-color','ctrl-bq-bg','ctrl-bq-color'].forEach(function(ctrlId) {
|
|
101
101
|
var btnId = 'reset-' + ctrlId.replace('ctrl-', '');
|
|
102
102
|
document.getElementById(btnId).addEventListener('click', function() {
|
|
103
103
|
var defaultVal = S.getStandaloneDefault(ctrlId);
|
|
@@ -165,54 +165,41 @@ function applyStylesFromMeta(s) {
|
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
// Legacy single-theme path: top-level colors go to light theme
|
|
168
|
+
// Legacy single-theme path: top-level colors go to light theme only,
|
|
169
|
+
// then loadThemeColors applies the correct theme's colors.
|
|
170
|
+
var lightOverridden = S.themeOverridden.light;
|
|
171
|
+
var lightColors = S.themeColors.light;
|
|
169
172
|
var newOverridden = result.overriddenColors;
|
|
170
173
|
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
lightOverridden.clear();
|
|
175
|
+
S.themeOverridden.dark.clear();
|
|
173
176
|
|
|
174
177
|
newOverridden.forEach(function(id) {
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
lightOverridden.add(id);
|
|
179
|
+
lightColors[id] = controls[id];
|
|
177
180
|
});
|
|
178
181
|
|
|
179
|
-
// Also
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (elBg) { elBg.value = s.background; applyCtrl('ctrl-bg-color'); }
|
|
184
|
-
}
|
|
185
|
-
if (s.link && s.link.color) {
|
|
186
|
-
S.overriddenColors.add('ctrl-link-color');
|
|
187
|
-
var el = document.getElementById('ctrl-link-color');
|
|
188
|
-
if (el) { el.value = s.link.color; applyCtrl('ctrl-link-color'); }
|
|
189
|
-
}
|
|
182
|
+
// Also store standalone colors from legacy format into light theme
|
|
183
|
+
var legacyStandalone = [];
|
|
184
|
+
if (s.background) legacyStandalone.push(['ctrl-bg-color', s.background]);
|
|
185
|
+
if (s.link && s.link.color) legacyStandalone.push(['ctrl-link-color', s.link.color]);
|
|
190
186
|
if (s.code) {
|
|
191
|
-
if (s.code.background)
|
|
192
|
-
|
|
193
|
-
var el2 = document.getElementById('ctrl-code-bg');
|
|
194
|
-
if (el2) { el2.value = s.code.background; applyCtrl('ctrl-code-bg'); }
|
|
195
|
-
}
|
|
196
|
-
if (s.code.color) {
|
|
197
|
-
S.overriddenColors.add('ctrl-code-color');
|
|
198
|
-
var el3 = document.getElementById('ctrl-code-color');
|
|
199
|
-
if (el3) { el3.value = s.code.color; applyCtrl('ctrl-code-color'); }
|
|
200
|
-
}
|
|
187
|
+
if (s.code.background) legacyStandalone.push(['ctrl-code-bg', s.code.background]);
|
|
188
|
+
if (s.code.color) legacyStandalone.push(['ctrl-code-color', s.code.color]);
|
|
201
189
|
}
|
|
202
190
|
if (s.blockquote) {
|
|
203
|
-
if (s.blockquote.borderColor)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (el4) { el4.value = s.blockquote.borderColor; applyCtrl('ctrl-bq-border-color'); }
|
|
207
|
-
}
|
|
208
|
-
if (s.blockquote.color) {
|
|
209
|
-
S.overriddenColors.add('ctrl-bq-color');
|
|
210
|
-
var el5 = document.getElementById('ctrl-bq-color');
|
|
211
|
-
if (el5) { el5.value = s.blockquote.color; applyCtrl('ctrl-bq-color'); }
|
|
212
|
-
}
|
|
191
|
+
if (s.blockquote.borderColor) legacyStandalone.push(['ctrl-bq-border-color', s.blockquote.borderColor]);
|
|
192
|
+
if (s.blockquote.background) legacyStandalone.push(['ctrl-bq-bg', s.blockquote.background]);
|
|
193
|
+
if (s.blockquote.color) legacyStandalone.push(['ctrl-bq-color', s.blockquote.color]);
|
|
213
194
|
}
|
|
195
|
+
legacyStandalone.forEach(function(pair) {
|
|
196
|
+
lightOverridden.add(pair[0]);
|
|
197
|
+
lightColors[pair[0]] = pair[1];
|
|
198
|
+
});
|
|
214
199
|
|
|
200
|
+
// Now apply the active theme's colors (light overrides or dark defaults)
|
|
215
201
|
S._syncing = wasSyncing;
|
|
202
|
+
S.loadThemeColors(S.activeTheme);
|
|
216
203
|
}
|
|
217
204
|
|
|
218
205
|
function collectStyles() {
|
|
@@ -257,7 +244,7 @@ function resetAllStyles() {
|
|
|
257
244
|
'ctrl-p-lh-num','ctrl-p-mb-num',
|
|
258
245
|
'ctrl-link-color','ctrl-link-decoration',
|
|
259
246
|
'ctrl-code-font','ctrl-code-bg','ctrl-code-color',
|
|
260
|
-
'ctrl-bq-border-color','ctrl-bq-bw-num','ctrl-bq-size-num','ctrl-bq-color',
|
|
247
|
+
'ctrl-bq-border-color','ctrl-bq-bg','ctrl-bq-bw-num','ctrl-bq-size-num','ctrl-bq-color',
|
|
261
248
|
'ctrl-list-spacing-num','ctrl-list-indent-num',
|
|
262
249
|
].forEach(function(id) { applyCtrl(id); });
|
|
263
250
|
}
|
package/public/sdocs-export.js
CHANGED
|
@@ -47,11 +47,12 @@ function buildExportCSS() {
|
|
|
47
47
|
var preBG = cv('--md-pre-bg') || codeBG;
|
|
48
48
|
|
|
49
49
|
var bqBorder = cv('--md-bq-border') || '3px solid #2563eb';
|
|
50
|
+
var bqBg = cv('--md-bq-bg') || '#f7f5f2';
|
|
50
51
|
var bqColor = cv('--md-bq-color') || '#6b6560';
|
|
51
52
|
var bqPad = cv('--md-bq-padding') || '0.5em 1em';
|
|
52
53
|
var bqMargin = cv('--md-bq-margin') || '1.2em 0';
|
|
53
54
|
|
|
54
|
-
return '\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: #3c3733; font-size: 0.88em; }\nblockquote { border-left: ' + bqBorder + '; color: ' + bqColor + '; padding: ' + bqPad + '; margin: ' + bqMargin + '; background:
|
|
55
|
+
return '\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: #3c3733; 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; }\nli { margin-bottom: 0.3em; }\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';
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
function buildExportHTML() {
|
package/public/sdocs-styles.js
CHANGED
|
@@ -55,6 +55,7 @@ const CTRL_CSS_MAP = {
|
|
|
55
55
|
'ctrl-code-color': { cssVar: '--md-code-color' },
|
|
56
56
|
'ctrl-bq-border-color': { cssVar: '--md-bq-border', compound: 'bq-border' },
|
|
57
57
|
'ctrl-bq-bw-num': { cssVar: '--md-bq-border', compound: 'bq-border' },
|
|
58
|
+
'ctrl-bq-bg': { cssVar: '--md-bq-bg' },
|
|
58
59
|
'ctrl-bq-size-num': { cssVar: '--md-bq-size', suffix: 'em' },
|
|
59
60
|
'ctrl-bq-color': { cssVar: '--md-bq-color' },
|
|
60
61
|
'ctrl-list-spacing-num': { cssVar: '--md-list-spacing', suffix: 'em' },
|
|
@@ -170,6 +171,7 @@ function collectStyles(values, overriddenColors) {
|
|
|
170
171
|
blockquote: {
|
|
171
172
|
borderColor: gv('ctrl-bq-border-color'),
|
|
172
173
|
borderWidth: gn('ctrl-bq-bw-num'),
|
|
174
|
+
background: gv('ctrl-bq-bg'),
|
|
173
175
|
fontSize: gn('ctrl-bq-size-num'),
|
|
174
176
|
color: gv('ctrl-bq-color'),
|
|
175
177
|
},
|
|
@@ -257,6 +259,7 @@ function stylesToControls(styles) {
|
|
|
257
259
|
const bq = styles.blockquote || {};
|
|
258
260
|
if (bq.borderColor) controls['ctrl-bq-border-color'] = bq.borderColor;
|
|
259
261
|
if (bq.borderWidth) controls['ctrl-bq-bw-num'] = bq.borderWidth;
|
|
262
|
+
if (bq.background) controls['ctrl-bq-bg'] = bq.background;
|
|
260
263
|
if (bq.fontSize) controls['ctrl-bq-size-num'] = bq.fontSize;
|
|
261
264
|
if (bq.color) controls['ctrl-bq-color'] = bq.color;
|
|
262
265
|
|
|
@@ -276,7 +279,7 @@ function stylesToControls(styles) {
|
|
|
276
279
|
// ═══════════════════════════════════════════════════════
|
|
277
280
|
|
|
278
281
|
var STANDALONE_COLOR_IDS = [
|
|
279
|
-
'ctrl-bg-color','ctrl-link-color','ctrl-code-bg','ctrl-code-color','ctrl-bq-border-color','ctrl-bq-color',
|
|
282
|
+
'ctrl-bg-color','ctrl-link-color','ctrl-code-bg','ctrl-code-color','ctrl-bq-border-color','ctrl-bq-bg','ctrl-bq-color',
|
|
280
283
|
];
|
|
281
284
|
|
|
282
285
|
var CASCADE_COLOR_IDS = Object.keys(COLOR_VAR_MAP);
|
|
@@ -341,6 +344,7 @@ function parseThemeColorBlock(block) {
|
|
|
341
344
|
}
|
|
342
345
|
if (block.blockquote) {
|
|
343
346
|
if (block.blockquote.borderColor) { colors['ctrl-bq-border-color'] = block.blockquote.borderColor; overridden.add('ctrl-bq-border-color'); }
|
|
347
|
+
if (block.blockquote.background) { colors['ctrl-bq-bg'] = block.blockquote.background; overridden.add('ctrl-bq-bg'); }
|
|
344
348
|
if (block.blockquote.color) { colors['ctrl-bq-color'] = block.blockquote.color; overridden.add('ctrl-bq-color'); }
|
|
345
349
|
}
|
|
346
350
|
|
|
@@ -397,6 +401,10 @@ function collectThemeColors(colorValues, overriddenSet) {
|
|
|
397
401
|
if (!block.blockquote) block.blockquote = {};
|
|
398
402
|
block.blockquote.borderColor = colorValues['ctrl-bq-border-color'];
|
|
399
403
|
}
|
|
404
|
+
if (overriddenSet.has('ctrl-bq-bg')) {
|
|
405
|
+
if (!block.blockquote) block.blockquote = {};
|
|
406
|
+
block.blockquote.background = colorValues['ctrl-bq-bg'];
|
|
407
|
+
}
|
|
400
408
|
if (overriddenSet.has('ctrl-bq-color')) {
|
|
401
409
|
if (!block.blockquote) block.blockquote = {};
|
|
402
410
|
block.blockquote.color = colorValues['ctrl-bq-color'];
|
|
@@ -438,6 +446,7 @@ function collectStylesDual(values, lightOverridden, darkOverridden, lightColors,
|
|
|
438
446
|
borderWidth: gn('ctrl-bq-bw-num'),
|
|
439
447
|
fontSize: gn('ctrl-bq-size-num'),
|
|
440
448
|
},
|
|
449
|
+
|
|
441
450
|
};
|
|
442
451
|
|
|
443
452
|
styles.list = {
|
|
@@ -525,6 +534,79 @@ stylesToControls = function(styles) {
|
|
|
525
534
|
};
|
|
526
535
|
};
|
|
527
536
|
|
|
537
|
+
// ═══════════════════════════════════════════════════════
|
|
538
|
+
// STRIP STYLE DEFAULTS (for shorter URLs)
|
|
539
|
+
// ═══════════════════════════════════════════════════════
|
|
540
|
+
|
|
541
|
+
// Default values matching HTML control defaults in index.html.
|
|
542
|
+
// When a style value equals its default, it can be omitted from serialization
|
|
543
|
+
// because stylesToControls / the browser already falls back to these defaults.
|
|
544
|
+
var STYLE_DEFAULTS = {
|
|
545
|
+
fontFamily: 'Inter',
|
|
546
|
+
baseFontSize: 16,
|
|
547
|
+
lineHeight: 1.75,
|
|
548
|
+
headers: {
|
|
549
|
+
fontFamily: 'inherit',
|
|
550
|
+
scale: 1.0,
|
|
551
|
+
marginBottom: 0.4,
|
|
552
|
+
},
|
|
553
|
+
h1: { fontSize: 2.1, fontWeight: 700 },
|
|
554
|
+
h2: { fontSize: 1.55, fontWeight: 600 },
|
|
555
|
+
h3: { fontSize: 1.2, fontWeight: 600 },
|
|
556
|
+
h4: { fontSize: 1.0, fontWeight: 600 },
|
|
557
|
+
p: { lineHeight: 1.75, marginBottom: 1.1 },
|
|
558
|
+
link: { decoration: 'underline' },
|
|
559
|
+
code: { font: 'JetBrains Mono' },
|
|
560
|
+
blockquote: { borderWidth: 3, fontSize: 1.0 },
|
|
561
|
+
list: { spacing: 0.3, indent: 1.6 },
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
function numEq(a, b) {
|
|
565
|
+
var na = typeof a === 'number' ? a : parseFloat(a);
|
|
566
|
+
var nb = typeof b === 'number' ? b : parseFloat(b);
|
|
567
|
+
if (isNaN(na) || isNaN(nb)) return false;
|
|
568
|
+
return Math.abs(na - nb) < 0.001;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* stripStyleDefaults(styles)
|
|
573
|
+
* Returns a new styles object with default-valued properties removed.
|
|
574
|
+
* light/dark color blocks are preserved as-is (they have no static defaults).
|
|
575
|
+
* Empty sub-objects are removed entirely.
|
|
576
|
+
*/
|
|
577
|
+
function stripStyleDefaults(styles) {
|
|
578
|
+
if (!styles || typeof styles !== 'object') return styles;
|
|
579
|
+
var result = {};
|
|
580
|
+
for (var key in styles) {
|
|
581
|
+
if (!styles.hasOwnProperty(key)) continue;
|
|
582
|
+
var val = styles[key];
|
|
583
|
+
var def = STYLE_DEFAULTS[key];
|
|
584
|
+
|
|
585
|
+
// light/dark theme color blocks — always keep
|
|
586
|
+
if (key === 'light' || key === 'dark') {
|
|
587
|
+
result[key] = val;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (typeof val === 'object' && val !== null) {
|
|
592
|
+
var sub = {};
|
|
593
|
+
var defObj = (typeof def === 'object' && def !== null) ? def : {};
|
|
594
|
+
for (var sk in val) {
|
|
595
|
+
if (!val.hasOwnProperty(sk)) continue;
|
|
596
|
+
var sv = val[sk];
|
|
597
|
+
var sd = defObj[sk];
|
|
598
|
+
if (sd !== undefined && (sv === sd || String(sv) === String(sd) || numEq(sv, sd))) continue;
|
|
599
|
+
sub[sk] = sv;
|
|
600
|
+
}
|
|
601
|
+
if (Object.keys(sub).length > 0) result[key] = sub;
|
|
602
|
+
} else {
|
|
603
|
+
if (def !== undefined && (val === def || String(val) === String(def) || numEq(val, def))) continue;
|
|
604
|
+
result[key] = val;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return result;
|
|
608
|
+
}
|
|
609
|
+
|
|
528
610
|
// ═══════════════════════════════════════════════════════
|
|
529
611
|
// EXPORTS
|
|
530
612
|
// ═══════════════════════════════════════════════════════
|
|
@@ -544,6 +626,8 @@ exports.collectStylesDual = collectStylesDual;
|
|
|
544
626
|
exports.collectThemeColors = collectThemeColors;
|
|
545
627
|
exports.parseThemeColorBlock = parseThemeColorBlock;
|
|
546
628
|
exports.stylesToControls = stylesToControls;
|
|
629
|
+
exports.STYLE_DEFAULTS = STYLE_DEFAULTS;
|
|
630
|
+
exports.stripStyleDefaults = stripStyleDefaults;
|
|
547
631
|
|
|
548
632
|
// UMD tail: in Node (tests) this writes to module.exports; in the browser
|
|
549
633
|
// it creates window.SDocStyles. We use this pattern instead of ES modules
|
package/public/sdocs-theme.js
CHANGED
|
@@ -55,16 +55,18 @@ const LIGHT_DEFAULTS = {
|
|
|
55
55
|
codeColor: '#6b21a8',
|
|
56
56
|
linkColor: '#2563eb',
|
|
57
57
|
bqBorderColor: '#2563eb',
|
|
58
|
+
bqBg: '#f7f5f2',
|
|
58
59
|
bqColor: '#6b6560',
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
const DARK_DEFAULTS = {
|
|
62
|
-
bgColor: '#
|
|
63
|
+
bgColor: '#2c2a26',
|
|
63
64
|
colorDefault: '#e7e5e2',
|
|
64
65
|
codeBg: '#1a1816',
|
|
65
66
|
codeColor: '#b8a99a',
|
|
66
67
|
linkColor: '#60a5fa',
|
|
67
68
|
bqBorderColor: '#60a5fa',
|
|
69
|
+
bqBg: '#252320',
|
|
68
70
|
bqColor: '#a8a29e',
|
|
69
71
|
};
|
|
70
72
|
|
|
@@ -142,6 +144,7 @@ function loadThemeColors(theme) {
|
|
|
142
144
|
'ctrl-code-bg': defaults.codeBg,
|
|
143
145
|
'ctrl-code-color': defaults.codeColor,
|
|
144
146
|
'ctrl-bq-border-color': defaults.bqBorderColor,
|
|
147
|
+
'ctrl-bq-bg': defaults.bqBg,
|
|
145
148
|
'ctrl-bq-color': defaults.bqColor,
|
|
146
149
|
};
|
|
147
150
|
for (var ctrlId in standaloneMap) {
|
|
@@ -192,6 +195,7 @@ function updateDefaultColors() {
|
|
|
192
195
|
'ctrl-code-bg': defaults.codeBg,
|
|
193
196
|
'ctrl-code-color': defaults.codeColor,
|
|
194
197
|
'ctrl-bq-border-color': defaults.bqBorderColor,
|
|
198
|
+
'ctrl-bq-bg': defaults.bqBg,
|
|
195
199
|
'ctrl-bq-color': defaults.bqColor,
|
|
196
200
|
};
|
|
197
201
|
for (const [ctrlId, val] of Object.entries(standaloneMap)) {
|
|
@@ -216,6 +220,7 @@ function getStandaloneDefault(ctrlId) {
|
|
|
216
220
|
'ctrl-code-bg': d.codeBg,
|
|
217
221
|
'ctrl-code-color': d.codeColor,
|
|
218
222
|
'ctrl-bq-border-color': d.bqBorderColor,
|
|
223
|
+
'ctrl-bq-bg': d.bqBg,
|
|
219
224
|
'ctrl-bq-color': d.bqColor,
|
|
220
225
|
};
|
|
221
226
|
return map[ctrlId];
|
|
@@ -247,6 +252,7 @@ S.getStandaloneDefault = getStandaloneDefault;
|
|
|
247
252
|
S.updateDefaultColors = updateDefaultColors;
|
|
248
253
|
S.toggleTheme = toggleTheme;
|
|
249
254
|
S.switchThemeAndUpdate = switchThemeAndUpdate;
|
|
255
|
+
S.getPreferredTheme = getPreferredTheme;
|
|
250
256
|
S.saveCurrentThemeColors = saveCurrentThemeColors;
|
|
251
257
|
S.loadThemeColors = loadThemeColors;
|
|
252
258
|
S.updateThemeTabs = updateThemeTabs;
|
package/public/sw.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Service worker — stale-while-revalidate + version-gated cache bust
|
|
2
|
+
var CACHE_NAME = 'sdocs-cache';
|
|
3
|
+
|
|
4
|
+
var APP_SHELL = [
|
|
5
|
+
'/',
|
|
6
|
+
'/public/css/tokens.css',
|
|
7
|
+
'/public/css/layout.css',
|
|
8
|
+
'/public/css/rendered.css',
|
|
9
|
+
'/public/css/panel.css',
|
|
10
|
+
'/public/css/write.css',
|
|
11
|
+
'/public/css/mobile.css',
|
|
12
|
+
'/public/sdocs-yaml.js',
|
|
13
|
+
'/public/sdocs-styles.js',
|
|
14
|
+
'/public/sdocs-state.js',
|
|
15
|
+
'/public/sdocs-theme.js',
|
|
16
|
+
'/public/sdocs-controls.js',
|
|
17
|
+
'/public/sdocs-export.js',
|
|
18
|
+
'/public/sdocs-write.js',
|
|
19
|
+
'/public/sdocs-app.js',
|
|
20
|
+
'/public/vendor/marked.min.js',
|
|
21
|
+
'/public/fonts/inter-400.woff2',
|
|
22
|
+
'/public/fonts/inter-500.woff2',
|
|
23
|
+
'/public/fonts/inter-600.woff2',
|
|
24
|
+
'/public/default.md',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Pre-cache app shell on install
|
|
28
|
+
self.addEventListener('install', function (e) {
|
|
29
|
+
e.waitUntil(
|
|
30
|
+
caches.open(CACHE_NAME).then(function (cache) {
|
|
31
|
+
return cache.addAll(APP_SHELL);
|
|
32
|
+
}).then(function () {
|
|
33
|
+
return self.skipWaiting();
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Claim clients on activate
|
|
39
|
+
self.addEventListener('activate', function (e) {
|
|
40
|
+
e.waitUntil(self.clients.claim());
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Stale-while-revalidate for same-origin, cache-first for fonts
|
|
44
|
+
self.addEventListener('fetch', function (e) {
|
|
45
|
+
var url = new URL(e.request.url);
|
|
46
|
+
|
|
47
|
+
if (e.request.method !== 'GET') return;
|
|
48
|
+
|
|
49
|
+
// Version-check always hits network
|
|
50
|
+
if (url.pathname === '/version-check') return;
|
|
51
|
+
|
|
52
|
+
// Google Fonts: cache-first (they're immutable)
|
|
53
|
+
if (url.hostname === 'fonts.googleapis.com' || url.hostname === 'fonts.gstatic.com') {
|
|
54
|
+
e.respondWith(
|
|
55
|
+
caches.match(e.request).then(function (cached) {
|
|
56
|
+
if (cached) return cached;
|
|
57
|
+
return fetch(e.request).then(function (response) {
|
|
58
|
+
var clone = response.clone();
|
|
59
|
+
caches.open(CACHE_NAME).then(function (cache) { cache.put(e.request, clone); });
|
|
60
|
+
return response;
|
|
61
|
+
});
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Same-origin: stale-while-revalidate
|
|
68
|
+
// Return cached immediately, fetch in background to update cache
|
|
69
|
+
if (url.origin === self.location.origin) {
|
|
70
|
+
e.respondWith(
|
|
71
|
+
caches.open(CACHE_NAME).then(function (cache) {
|
|
72
|
+
return cache.match(e.request).then(function (cached) {
|
|
73
|
+
var networkFetch = fetch(e.request).then(function (response) {
|
|
74
|
+
if (response.ok) {
|
|
75
|
+
cache.put(e.request, response.clone());
|
|
76
|
+
}
|
|
77
|
+
return response;
|
|
78
|
+
}).catch(function () {
|
|
79
|
+
return cached; // offline fallback
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return cached || networkFetch;
|
|
83
|
+
});
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Version check: if server version differs, purge and re-cache.
|
|
91
|
+
// No reload — stale-while-revalidate ensures next navigation gets fresh content.
|
|
92
|
+
self.addEventListener('message', function (e) {
|
|
93
|
+
if (e.data && e.data.type === 'check-update' && e.data.version) {
|
|
94
|
+
fetch('/version-check').then(function (res) {
|
|
95
|
+
return res.json();
|
|
96
|
+
}).then(function (data) {
|
|
97
|
+
if (data.version !== e.data.version) {
|
|
98
|
+
caches.delete(CACHE_NAME).then(function () {
|
|
99
|
+
return caches.open(CACHE_NAME).then(function (cache) {
|
|
100
|
+
return cache.addAll(APP_SHELL);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}).catch(function () { /* offline — ignore */ });
|
|
105
|
+
}
|
|
106
|
+
});
|