lowkey-doc 1.0.1 → 1.1.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/package.json +1 -1
- package/src/client/app.mjs +111 -4
- package/src/client/css.mjs +53 -17
- package/src/client/diagrams.mjs +121 -26
- package/src/collect.mjs +8 -11
- package/src/config.mjs +2 -1
- package/src/markdown.mjs +25 -10
- package/src/page.mjs +1 -0
- package/src/sections.mjs +12 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lowkey-doc",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Render a folder of Markdown into one self-contained documentation dashboard, with flowcharts and ER diagrams that stay readable when they get dense.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
package/src/client/app.mjs
CHANGED
|
@@ -34,6 +34,7 @@ export const APP_JS = String.raw`
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
var CARET = '<svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M6 4l4 4-4 4"/></svg>';
|
|
37
|
+
var CHECK = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6.3"/><path d="M5.3 8.2l1.9 1.9 3.6-3.9"/></svg>';
|
|
37
38
|
|
|
38
39
|
function buildNav(){
|
|
39
40
|
var group = null;
|
|
@@ -47,6 +48,7 @@ export const APP_JS = String.raw`
|
|
|
47
48
|
item.title = d.file;
|
|
48
49
|
item.appendChild(el('span', 'num', d.badge));
|
|
49
50
|
item.appendChild(el('span', 'item-title', d.title));
|
|
51
|
+
if (RV_ON) item.insertAdjacentHTML('beforeend', '<span class="rv-mark" title="Reviewed">' + CHECK + '</span>');
|
|
50
52
|
if (d.sections.length) item.insertAdjacentHTML('beforeend', CARET);
|
|
51
53
|
item.addEventListener('click', function(e){
|
|
52
54
|
if (e.target.closest('.caret') && current === d.id){ item.classList.toggle('open'); return; }
|
|
@@ -62,6 +64,8 @@ export const APP_JS = String.raw`
|
|
|
62
64
|
a.title = (s.num ? s.num + ' ' : '') + s.text;
|
|
63
65
|
if (s.num) a.appendChild(el('span', 'sub-n', s.num));
|
|
64
66
|
a.appendChild(el('span', 'sub-t', s.text));
|
|
67
|
+
if (RV_ON) a.insertAdjacentHTML('beforeend', '<span class="rv-mark" title="Reviewed">' + CHECK + '</span>');
|
|
68
|
+
a.dataset.sec = s.id;
|
|
65
69
|
subs.appendChild(a);
|
|
66
70
|
});
|
|
67
71
|
nav.appendChild(subs);
|
|
@@ -106,6 +110,7 @@ export const APP_JS = String.raw`
|
|
|
106
110
|
restoreFolds(main);
|
|
107
111
|
if (window.__diagrams) window.__diagrams.drawAll(main);
|
|
108
112
|
renderComments();
|
|
113
|
+
paintReviews();
|
|
109
114
|
watch();
|
|
110
115
|
}
|
|
111
116
|
if (anchor && reveal(anchor, false)) return;
|
|
@@ -130,9 +135,15 @@ export const APP_JS = String.raw`
|
|
|
130
135
|
var head = el('header', 'head');
|
|
131
136
|
head.appendChild(el('h1', null, d.title));
|
|
132
137
|
if (d.subtitle){
|
|
133
|
-
var
|
|
134
|
-
|
|
135
|
-
head.appendChild(
|
|
138
|
+
var lede = el('div', 'lede');
|
|
139
|
+
lede.innerHTML = d.subtitle;
|
|
140
|
+
head.appendChild(lede);
|
|
141
|
+
}
|
|
142
|
+
if (RV_ON){
|
|
143
|
+
var rvBar = el('div', 'rv-bar');
|
|
144
|
+
rvBar.appendChild(el('span', 'rv-count'));
|
|
145
|
+
rvBar.insertAdjacentHTML('beforeend', '<button class="rv rv-doc" type="button" aria-pressed="false" aria-label="Reviewed">' + CHECK + '</button>');
|
|
146
|
+
head.appendChild(rvBar);
|
|
136
147
|
}
|
|
137
148
|
wrap.appendChild(head);
|
|
138
149
|
|
|
@@ -201,6 +212,98 @@ export const APP_JS = String.raw`
|
|
|
201
212
|
return true;
|
|
202
213
|
}
|
|
203
214
|
|
|
215
|
+
/* ---------------- reviews ----------------
|
|
216
|
+
A reader marks what they have read and agree with. The mark is kept on the
|
|
217
|
+
smallest sections only: a section with sections under it is reviewed when
|
|
218
|
+
every one of them is, and the document when all of its sections are. So
|
|
219
|
+
marking a document or a chapter marks everything in it, reviewing the last
|
|
220
|
+
open section of a chapter completes the chapter, and a section written
|
|
221
|
+
later opens its chapter and its document again until it is read too.
|
|
222
|
+
Like comments, the marks live in this browser and nowhere else. */
|
|
223
|
+
|
|
224
|
+
var RV_ON = CFG.reviews !== false;
|
|
225
|
+
var reviewed = {};
|
|
226
|
+
try {
|
|
227
|
+
var rvSaved = JSON.parse(recall('reviewed') || '{}');
|
|
228
|
+
if (rvSaved && rvSaved.docs && typeof rvSaved.docs === 'object') reviewed = rvSaved.docs;
|
|
229
|
+
} catch (e) { reviewed = {}; }
|
|
230
|
+
|
|
231
|
+
function findSec(list, id){
|
|
232
|
+
for (var i = 0; i < list.length; i++){
|
|
233
|
+
if (list[i].id === id) return list[i];
|
|
234
|
+
var hit = findSec(list[i].kids, id);
|
|
235
|
+
if (hit) return hit;
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// what a review of the document (id null) or of one section stands for: the
|
|
241
|
+
// smallest sections under it, or the document itself if it has no sections
|
|
242
|
+
function rvKeys(d, id){
|
|
243
|
+
var outline = d.outline || [];
|
|
244
|
+
var node = id ? findSec(outline, id) : { kids: outline };
|
|
245
|
+
if (!node) return [];
|
|
246
|
+
if (!node.kids.length) return [id || ''];
|
|
247
|
+
var out = [];
|
|
248
|
+
(function walk(list){
|
|
249
|
+
list.forEach(function(k){ if (k.kids.length) walk(k.kids); else out.push(k.id); });
|
|
250
|
+
})(node.kids);
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function rvCount(d, id){
|
|
255
|
+
var mine = reviewed[d.id] || {}, keys = rvKeys(d, id);
|
|
256
|
+
return { done: keys.filter(function(k){ return mine[k]; }).length, of: keys.length };
|
|
257
|
+
}
|
|
258
|
+
function isReviewed(d, id){ var c = rvCount(d, id); return c.of > 0 && c.done === c.of; }
|
|
259
|
+
|
|
260
|
+
function setReviewed(d, id, on){
|
|
261
|
+
var mine = reviewed[d.id] || {}, now = Date.now();
|
|
262
|
+
rvKeys(d, id).forEach(function(k){ if (on) mine[k] = mine[k] || now; else delete mine[k]; });
|
|
263
|
+
if (Object.keys(mine).length) reviewed[d.id] = mine; else delete reviewed[d.id];
|
|
264
|
+
keep('reviewed', JSON.stringify({ version: 1, docs: reviewed }));
|
|
265
|
+
paintReviews();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function paintBtn(btn, on, what){
|
|
269
|
+
btn.classList.toggle('is-on', on);
|
|
270
|
+
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
271
|
+
btn.title = on ? 'Reviewed. Click to open ' + what + ' again' : 'Mark ' + what + ' as reviewed';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function paintReviews(){
|
|
275
|
+
if (!RV_ON) return;
|
|
276
|
+
nav.querySelectorAll('.item').forEach(function(item){
|
|
277
|
+
var d = byId[item.dataset.doc];
|
|
278
|
+
if (!d) return;
|
|
279
|
+
item.classList.toggle('is-reviewed', isReviewed(d, null));
|
|
280
|
+
var subs = nav.querySelector('[data-subs="' + d.id + '"]');
|
|
281
|
+
if (subs) subs.querySelectorAll('.sub').forEach(function(a){
|
|
282
|
+
a.classList.toggle('is-reviewed', isReviewed(d, a.dataset.sec));
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
var d = byId[current];
|
|
286
|
+
if (!d) return;
|
|
287
|
+
main.querySelectorAll('.sec').forEach(function(sec){
|
|
288
|
+
var btn = sec.querySelector(':scope > .sec-head > .rv');
|
|
289
|
+
if (btn) paintBtn(btn, isReviewed(d, sec.dataset.sec), 'this section');
|
|
290
|
+
});
|
|
291
|
+
var whole = main.querySelector('.rv-doc'), count = main.querySelector('.rv-count');
|
|
292
|
+
if (whole) paintBtn(whole, isReviewed(d, null), 'the whole document');
|
|
293
|
+
if (count){
|
|
294
|
+
var c = rvCount(d, null);
|
|
295
|
+
count.textContent = c.done && c.done < c.of ? c.done + ' of ' + c.of + ' sections' : '';
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function toggleReview(btn){
|
|
300
|
+
var d = byId[current];
|
|
301
|
+
if (!d) return;
|
|
302
|
+
var sec = btn.classList.contains('rv-doc') ? null : btn.closest('.sec');
|
|
303
|
+
var id = sec ? sec.dataset.sec : null;
|
|
304
|
+
setReviewed(d, id, !isReviewed(d, id));
|
|
305
|
+
}
|
|
306
|
+
|
|
204
307
|
/* ---------------- lightbox ---------------- */
|
|
205
308
|
|
|
206
309
|
var box = document.getElementById('lightbox');
|
|
@@ -293,6 +396,8 @@ export const APP_JS = String.raw`
|
|
|
293
396
|
if (reveal(href.slice(1), true)) history.replaceState(null, '', '#/' + current + '/' + href.slice(1));
|
|
294
397
|
return;
|
|
295
398
|
}
|
|
399
|
+
var rv = e.target.closest('.rv');
|
|
400
|
+
if (rv){ toggleReview(rv); return; }
|
|
296
401
|
// anywhere on a heading folds its section - unless the click was really the
|
|
297
402
|
// end of a drag that selected some of it
|
|
298
403
|
var head = e.target.closest('.sec-head');
|
|
@@ -901,6 +1006,7 @@ export const APP_JS = String.raw`
|
|
|
901
1006
|
n.classList.toggle('open', !!open[n.dataset.doc] || n.dataset.doc === current);
|
|
902
1007
|
});
|
|
903
1008
|
if (search && search.value) search.dispatchEvent(new Event('input'));
|
|
1009
|
+
paintReviews();
|
|
904
1010
|
nav.scrollTop = top;
|
|
905
1011
|
}
|
|
906
1012
|
|
|
@@ -920,6 +1026,7 @@ export const APP_JS = String.raw`
|
|
|
920
1026
|
restoreFolds(main);
|
|
921
1027
|
if (window.__diagrams) window.__diagrams.drawAll(main);
|
|
922
1028
|
renderComments();
|
|
1029
|
+
paintReviews();
|
|
923
1030
|
watch();
|
|
924
1031
|
keepPlace(at);
|
|
925
1032
|
|
|
@@ -938,7 +1045,7 @@ export const APP_JS = String.raw`
|
|
|
938
1045
|
var out = [];
|
|
939
1046
|
var head = holder.querySelector('.head');
|
|
940
1047
|
if (was.title !== d.title) out.push(head.querySelector('h1'));
|
|
941
|
-
if (was.subtitle !== d.subtitle && head.querySelector('
|
|
1048
|
+
if (was.subtitle !== d.subtitle && head.querySelector('.lede')) out.push(head.querySelector('.lede'));
|
|
942
1049
|
|
|
943
1050
|
var body = holder.querySelector('.doc');
|
|
944
1051
|
var t = document.createElement('template');
|
package/src/client/css.mjs
CHANGED
|
@@ -9,6 +9,8 @@ html{-webkit-text-size-adjust:100%}
|
|
|
9
9
|
--line-2:rgba(255,255,255,.16); --surface:#13161b; --surface-2:#1a1e24; --hover:rgba(255,255,255,.05);
|
|
10
10
|
/* the one accent: an electric blue, used for what is lit, current or alive */
|
|
11
11
|
--accent:#4da3ff; --accent-soft:rgba(77,163,255,.12); --accent-ring:rgba(77,163,255,.36);
|
|
12
|
+
/* reviewed: a green as light and as cool as the blue, so the two sit together */
|
|
13
|
+
--ok:#45cf98; --ok-soft:rgba(69,207,152,.12); --ok-ring:rgba(69,207,152,.36);
|
|
12
14
|
/* a filled button is white with dark text */
|
|
13
15
|
--on-ink:#0b0d10; --ink-hover:#dde2e8;
|
|
14
16
|
--serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
|
|
@@ -153,9 +155,14 @@ a{color:inherit;text-decoration:none}
|
|
|
153
155
|
.sub-n{flex:none;font-size:11px;color:var(--faint);font-variant-numeric:tabular-nums}
|
|
154
156
|
.sub.current .sub-n{color:var(--muted)}
|
|
155
157
|
.sub-t{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
158
|
+
/* a reviewed document or section carries a green check in the sidebar */
|
|
159
|
+
.rv-mark{display:none;flex:none;width:13px;height:13px;color:var(--ok)}
|
|
160
|
+
.rv-mark svg{display:block;width:100%;height:100%}
|
|
161
|
+
.is-reviewed>.rv-mark{display:block}
|
|
162
|
+
.sub .rv-mark{align-self:center;width:12px;height:12px}
|
|
156
163
|
|
|
157
164
|
.collapsed .brand-name,.collapsed .search,.collapsed .nav-group,
|
|
158
|
-
.collapsed .item-title,.collapsed .caret,.collapsed .foot-lbl{display:none}
|
|
165
|
+
.collapsed .item-title,.collapsed .caret,.collapsed .foot-lbl,.collapsed .rv-mark{display:none}
|
|
159
166
|
.collapsed .subs,.collapsed .item.open+.subs{display:none}
|
|
160
167
|
.collapsed .side-top{padding:16px 0 12px}
|
|
161
168
|
.collapsed .brand-row{justify-content:center}
|
|
@@ -249,7 +256,9 @@ export const CSS_MAIN = `
|
|
|
249
256
|
@keyframes head-lamp{to{opacity:1}}
|
|
250
257
|
@media (prefers-reduced-motion:reduce){.head::before,.head::after{animation:none}}
|
|
251
258
|
.head h1{margin:0;max-width:22ch;font-size:36px;line-height:1.12;font-weight:650;letter-spacing:-.034em;color:var(--ink)}
|
|
252
|
-
.head
|
|
259
|
+
.head .lede{margin:14px 0 0;max-width:68ch;font-size:15.5px;line-height:1.62;color:var(--muted)}
|
|
260
|
+
.lede>*{margin:0}
|
|
261
|
+
.lede>*+*{margin-top:10px}
|
|
253
262
|
.doc{padding:4px var(--gutter) 30px;width:100%;max-width:var(--column);margin-inline:auto}
|
|
254
263
|
|
|
255
264
|
/* ---- prev / next ----
|
|
@@ -312,6 +321,32 @@ h4.sec-head,h5.sec-head,h6.sec-head{margin:0 0 8px;font-size:14px;font-weight:va
|
|
|
312
321
|
.sec-head:hover .h-anchor{opacity:1}
|
|
313
322
|
.h-anchor:hover{color:var(--ink)}
|
|
314
323
|
|
|
324
|
+
/* ---- reviews ----
|
|
325
|
+
Every section, and the document in its header card, has a Reviewed button:
|
|
326
|
+
a grey check until the reader marks it, then green. On a heading the word
|
|
327
|
+
shows only on hover, so a page of unread headings is not a page of labels. */
|
|
328
|
+
.rv{flex:none;align-self:center;margin-left:auto;display:inline-flex;align-items:center;gap:6px;
|
|
329
|
+
padding:3px 4px 3px 9px;border:1px solid transparent;border-radius:999px;background:none;
|
|
330
|
+
color:var(--faint);font:inherit;font-size:11.5px;font-weight:var(--semi);letter-spacing:0;line-height:1;
|
|
331
|
+
cursor:pointer;transition:color .15s,background .15s,border-color .15s}
|
|
332
|
+
.rv svg{width:16px;height:16px;flex:none}
|
|
333
|
+
.rv svg circle{transition:fill .15s,stroke .15s}
|
|
334
|
+
.rv::before{content:"Reviewed";opacity:0;transition:opacity .15s}
|
|
335
|
+
.sec-head:hover>.rv::before,.rv:focus-visible::before,.rv.is-on::before{opacity:1}
|
|
336
|
+
.rv:hover{color:var(--ink);background:var(--hover)}
|
|
337
|
+
.rv:focus-visible{outline:2px solid var(--ok-ring);outline-offset:1px}
|
|
338
|
+
.rv.is-on{color:var(--ok)}
|
|
339
|
+
.rv.is-on svg circle{fill:var(--ok);stroke:var(--ok)}
|
|
340
|
+
.rv.is-on svg path{stroke:var(--on-ink)}
|
|
341
|
+
.rv.is-on:hover{color:var(--ok);background:var(--ok-soft)}
|
|
342
|
+
/* the document's own, in the corner of the header card: always labelled */
|
|
343
|
+
.rv-bar{position:absolute;top:18px;right:18px;display:flex;align-items:center;gap:10px}
|
|
344
|
+
.rv-count{font-size:11.5px;color:var(--muted);font-variant-numeric:tabular-nums}
|
|
345
|
+
.rv-count:empty{display:none}
|
|
346
|
+
.rv-doc{margin:0;border-color:var(--line-2);color:var(--muted)}
|
|
347
|
+
.rv-doc::before{opacity:1}
|
|
348
|
+
.rv-doc.is-on{border-color:var(--ok-ring)}
|
|
349
|
+
|
|
315
350
|
/* ---- prose ---- */
|
|
316
351
|
.doc p{margin:0 0 18px}
|
|
317
352
|
.doc>p:first-child{margin-top:6px}
|
|
@@ -323,9 +358,9 @@ h4.sec-head,h5.sec-head,h6.sec-head{margin:0 0 8px;font-size:14px;font-weight:va
|
|
|
323
358
|
.doc ul>li::marker{color:var(--accent)}
|
|
324
359
|
.doc strong{font-weight:var(--semi);color:inherit}
|
|
325
360
|
.doc em{font-style:italic}
|
|
326
|
-
.doc a:not(.h-anchor){color:var(--ink);border-bottom:1px solid var(--line-2);padding-bottom:1px;
|
|
361
|
+
.doc a:not(.h-anchor),.lede a{color:var(--ink);border-bottom:1px solid var(--line-2);padding-bottom:1px;
|
|
327
362
|
transition:border-color .12s,color .12s}
|
|
328
|
-
.doc a:not(.h-anchor):hover{border-bottom-color:var(--accent);color:var(--accent)}
|
|
363
|
+
.doc a:not(.h-anchor):hover,.lede a:hover{border-bottom-color:var(--accent);color:var(--accent)}
|
|
329
364
|
.doc hr{margin:32px 0;border:0;border-top:1px solid var(--line)}
|
|
330
365
|
.doc blockquote{margin:0 0 18px;padding:2px 0 2px 17px;border-left:2px solid var(--accent-ring);color:var(--muted)}
|
|
331
366
|
/* Inline code is a plate set into the sentence: a faint white fill that
|
|
@@ -334,12 +369,12 @@ h4.sec-head,h5.sec-head,h6.sec-head{margin:0 0 8px;font-size:14px;font-weight:va
|
|
|
334
369
|
alone would vanish), and an ink a shade cooler than the prose, so a name
|
|
335
370
|
reads as a name without shouting. Padding is in em, so the plate tracks
|
|
336
371
|
the text it sits in, headings included. */
|
|
337
|
-
.doc code{font-family:var(--mono);font-size:.84em;letter-spacing:-.01em;
|
|
372
|
+
.doc code,.lede code{font-family:var(--mono);font-size:.84em;letter-spacing:-.01em;
|
|
338
373
|
background:var(--code-bg);box-shadow:inset 0 0 0 1px var(--code-ring);
|
|
339
374
|
border-radius:5px;padding:.1em .38em .12em;color:var(--code-ink);white-space:nowrap;
|
|
340
375
|
-webkit-box-decoration-break:clone;box-decoration-break:clone}
|
|
341
376
|
.api code{background:var(--code-bg-on-white)}
|
|
342
|
-
.doc a:not(.h-anchor) code{color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent-ring)}
|
|
377
|
+
.doc a:not(.h-anchor) code,.lede a code{color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent-ring)}
|
|
343
378
|
.doc h1 code,.doc h2 code,.doc h3 code,.doc h4 code{font-size:.86em;font-weight:var(--semi)}
|
|
344
379
|
/* a {placeholder} inside code: the braces fade, the name stays */
|
|
345
380
|
.c-var{color:var(--code-var);font-style:italic}
|
|
@@ -491,6 +526,7 @@ mark{background:rgba(77,163,255,.22);color:var(--ink);border-radius:3px;padding:
|
|
|
491
526
|
.docnav-btn.next{grid-column:1;text-align:left}
|
|
492
527
|
.head{width:calc(100% - 20px);margin-top:14px;padding-block:34px 36px}
|
|
493
528
|
.head h1{font-size:29px}
|
|
529
|
+
.rv-bar{position:static;margin-top:18px}
|
|
494
530
|
}
|
|
495
531
|
/* Not enough room beside the column to hang the arrow and the number in:
|
|
496
532
|
bring them back into the heading row rather than let them fall off-screen. */
|
|
@@ -501,7 +537,7 @@ mark{background:rgba(77,163,255,.22);color:var(--ink);border-radius:3px;padding:
|
|
|
501
537
|
@media print{
|
|
502
538
|
.side{display:none}.layout{grid-template-columns:1fr}
|
|
503
539
|
.head{border:0;border-radius:0;padding-block:20px;background:none}.head::before,.head::after{display:none}
|
|
504
|
-
.sec.shut>.sec-body{display:block}.sec-tog,.h-anchor{display:none}
|
|
540
|
+
.sec.shut>.sec-body{display:block}.sec-tog,.h-anchor,.rv,.rv-bar{display:none}
|
|
505
541
|
}
|
|
506
542
|
`;
|
|
507
543
|
|
|
@@ -753,19 +789,19 @@ export const CSS_CMT = `
|
|
|
753
789
|
A highlight and its card are two views of one comment, so hovering either
|
|
754
790
|
lights both (.hot); clicking either pins the pair (.on). */
|
|
755
791
|
.body-wrap{position:relative;display:flex;flex-direction:column;min-height:100vh}
|
|
756
|
-
.cmt-hl{background:rgba(
|
|
792
|
+
.cmt-hl{background:rgba(77,163,255,.16);box-shadow:inset 0 -1px 0 rgba(77,163,255,.6);border-radius:3px;padding:0 1px;
|
|
757
793
|
color:inherit;cursor:pointer;transition:background .12s,box-shadow .12s}
|
|
758
|
-
.cmt-hl.hot{background:rgba(
|
|
759
|
-
.cmt-hl.on{background:rgba(
|
|
794
|
+
.cmt-hl.hot{background:rgba(77,163,255,.26);box-shadow:inset 0 -1px 0 rgba(77,163,255,.8)}
|
|
795
|
+
.cmt-hl.on{background:rgba(77,163,255,.36);box-shadow:inset 0 -1px 0 var(--accent)}
|
|
760
796
|
.cmt-layer{position:absolute;inset:0;pointer-events:none;z-index:2}
|
|
761
797
|
.cmt-card{position:absolute;left:0;top:0;width:var(--cmt-w,264px);pointer-events:auto;padding:10px 12px;
|
|
762
798
|
background:var(--surface);border:1px solid var(--line);border-radius:14px;cursor:pointer;
|
|
763
799
|
font-size:12.5px;line-height:1.55;transition:border-color .15s ease,box-shadow .15s ease}
|
|
764
|
-
.cmt-card.hot{border-color:rgba(
|
|
765
|
-
.cmt-card.on{border-color:rgba(
|
|
800
|
+
.cmt-card.hot{border-color:rgba(77,163,255,.45);box-shadow:0 4px 16px rgba(0,0,0,.4)}
|
|
801
|
+
.cmt-card.on{border-color:rgba(77,163,255,.7);box-shadow:0 6px 20px rgba(0,0,0,.5)}
|
|
766
802
|
.cmt-card[hidden]{display:none!important}
|
|
767
803
|
.cmt-face{display:none}
|
|
768
|
-
.cmt-quote{margin:0 0 7px;padding-left:9px;border-left:2px solid rgba(
|
|
804
|
+
.cmt-quote{margin:0 0 7px;padding-left:9px;border-left:2px solid rgba(77,163,255,.6);color:var(--muted);
|
|
769
805
|
font-size:11.5px;line-height:1.45;overflow:hidden;
|
|
770
806
|
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
|
|
771
807
|
.cmt-text{white-space:pre-wrap;overflow-wrap:anywhere}
|
|
@@ -774,14 +810,14 @@ export const CSS_CMT = `
|
|
|
774
810
|
.cmt-act{padding:0;border:0;background:none;color:var(--muted);font:inherit;font-size:11px;
|
|
775
811
|
cursor:pointer;border-bottom:1px solid transparent}
|
|
776
812
|
.cmt-act:hover{color:var(--ink);border-bottom-color:var(--ink)}
|
|
777
|
-
.cmt-lost{margin:0 0 5px;font-size:10px;letter-spacing:.06em;text-transform:uppercase;color
|
|
813
|
+
.cmt-lost{margin:0 0 5px;font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--accent)}
|
|
778
814
|
|
|
779
815
|
/* tight gutters: the card shrinks to a numbered dot, opened as a popover */
|
|
780
816
|
.cmt-layer.dots .cmt-card{width:22px;height:22px;padding:0;border-radius:50%;
|
|
781
|
-
background:#
|
|
782
|
-
.cmt-layer.dots .cmt-card.hot,.cmt-layer.dots .cmt-card.on{background:#
|
|
817
|
+
background:#10284d;border-color:#2f6db3;display:grid;place-items:center;overflow:hidden}
|
|
818
|
+
.cmt-layer.dots .cmt-card.hot,.cmt-layer.dots .cmt-card.on{background:#16386b;border-color:var(--accent)}
|
|
783
819
|
.cmt-layer.dots .cmt-full{display:none}
|
|
784
|
-
.cmt-layer.dots .cmt-face{display:block;font-size:11px;font-weight:var(--bold);color:#
|
|
820
|
+
.cmt-layer.dots .cmt-face{display:block;font-size:11px;font-weight:var(--bold);color:#a9d2ff;
|
|
785
821
|
font-variant-numeric:tabular-nums}
|
|
786
822
|
|
|
787
823
|
/* ---- comments: floating bits ---- */
|
package/src/client/diagrams.mjs
CHANGED
|
@@ -800,36 +800,101 @@ export const DIAGRAMS_JS = String.raw`
|
|
|
800
800
|
return out;
|
|
801
801
|
}
|
|
802
802
|
|
|
803
|
-
|
|
803
|
+
// the corners of the shapes that are drawn as polygons, null for the rest
|
|
804
|
+
function polyPoints(x, y, w, h, shape){
|
|
804
805
|
var s;
|
|
805
806
|
switch (shape){
|
|
806
807
|
case 'diamond': case 'question': case 'rhombus':
|
|
807
|
-
return
|
|
808
|
-
case 'stadium': case 'pill':
|
|
809
|
-
return mk('rect', { x:x, y:y, width:w, height:h, rx:h/2, ry:h/2 });
|
|
810
|
-
case 'circle': case 'doublecircle': case 'state':
|
|
811
|
-
return mk('ellipse', { cx:x+w/2, cy:y+h/2, rx:w/2, ry:h/2 });
|
|
812
|
-
case 'cylinder': case 'database':
|
|
813
|
-
var e = Math.min(11, h/5);
|
|
814
|
-
return mk('path', { d:'M'+x+','+(y+e)+' A'+(w/2)+','+e+' 0 0 1 '+(x+w)+','+(y+e)+
|
|
815
|
-
' L'+(x+w)+','+(y+h-e)+' A'+(w/2)+','+e+' 0 0 1 '+x+','+(y+h-e)+' Z'+
|
|
816
|
-
' M'+x+','+(y+e)+' A'+(w/2)+','+e+' 0 0 0 '+(x+w)+','+(y+e) });
|
|
808
|
+
return [[x+w/2,y], [x+w,y+h/2], [x+w/2,y+h], [x,y+h/2]];
|
|
817
809
|
case 'hexagon':
|
|
818
810
|
s = Math.min(18, w/5);
|
|
819
|
-
return
|
|
820
|
-
(x+w-s)+','+(y+h)+' '+(x+s)+','+(y+h)+' '+x+','+(y+h/2) });
|
|
811
|
+
return [[x+s,y], [x+w-s,y], [x+w,y+h/2], [x+w-s,y+h], [x+s,y+h], [x,y+h/2]];
|
|
821
812
|
case 'lean_right': case 'lean_r':
|
|
822
813
|
s = Math.min(22, w/6);
|
|
823
|
-
return
|
|
814
|
+
return [[x+s,y], [x+w,y], [x+w-s,y+h], [x,y+h]];
|
|
824
815
|
case 'lean_left': case 'lean_l':
|
|
825
816
|
s = Math.min(22, w/6);
|
|
826
|
-
return
|
|
817
|
+
return [[x,y], [x+w-s,y], [x+w,y+h], [x+s,y+h]];
|
|
827
818
|
case 'trapezoid':
|
|
828
819
|
s = Math.min(22, w/6);
|
|
829
|
-
return
|
|
820
|
+
return [[x+s,y], [x+w-s,y], [x+w,y+h], [x,y+h]];
|
|
830
821
|
case 'inv_trapezoid':
|
|
831
822
|
s = Math.min(22, w/6);
|
|
832
|
-
return
|
|
823
|
+
return [[x,y], [x+w,y], [x+w-s,y+h], [x+s,y+h]];
|
|
824
|
+
}
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// The outline shapeEl draws, as a closed ring of points, its curves walked
|
|
829
|
+
// in short steps.
|
|
830
|
+
function outline(x, y, w, h, shape){
|
|
831
|
+
var poly = polyPoints(x, y, w, h, shape);
|
|
832
|
+
if (poly) return poly.map(function(p){ return { x:p[0], y:p[1] }; });
|
|
833
|
+
var ring = [];
|
|
834
|
+
var arc = function(cx, cy, rx, ry, a0, a1){
|
|
835
|
+
var n = Math.max(2, Math.ceil(Math.abs(a1 - a0) / (Math.PI/24)));
|
|
836
|
+
for (var i=0;i<=n;i++){
|
|
837
|
+
var t = a0 + (a1 - a0) * i/n;
|
|
838
|
+
ring.push({ x: cx + rx*Math.cos(t), y: cy + ry*Math.sin(t) });
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
var rounded = function(r){
|
|
842
|
+
r = Math.min(r, w/2, h/2);
|
|
843
|
+
arc(x+w-r, y+r, r, r, -Math.PI/2, 0);
|
|
844
|
+
arc(x+w-r, y+h-r, r, r, 0, Math.PI/2);
|
|
845
|
+
arc(x+r, y+h-r, r, r, Math.PI/2, Math.PI);
|
|
846
|
+
arc(x+r, y+r, r, r, Math.PI, Math.PI*1.5);
|
|
847
|
+
};
|
|
848
|
+
switch (shape){
|
|
849
|
+
case 'stadium': case 'pill': rounded(h/2); break;
|
|
850
|
+
case 'circle': case 'doublecircle': case 'state': arc(x+w/2, y+h/2, w/2, h/2, 0, Math.PI*2); break;
|
|
851
|
+
case 'cylinder': case 'database':
|
|
852
|
+
var e = Math.min(11, h/5);
|
|
853
|
+
arc(x+w/2, y+e, w/2, e, Math.PI, Math.PI*2);
|
|
854
|
+
arc(x+w/2, y+h-e, w/2, e, 0, Math.PI);
|
|
855
|
+
break;
|
|
856
|
+
case 'squareRect': case 'rect': case 'square': rounded(3); break;
|
|
857
|
+
default: rounded(8);
|
|
858
|
+
}
|
|
859
|
+
return ring;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// ELK knows a node only as its box, and ends every edge on the box. A shape
|
|
863
|
+
// that does not fill its box - a diamond most of all - would leave arrows
|
|
864
|
+
// stopping short in the air beside it, so each end is carried on, straight
|
|
865
|
+
// in across the side of the box it arrived at, until it meets the outline.
|
|
866
|
+
// The route is orthogonal, so that is the direction its last run already had.
|
|
867
|
+
function onOutline(p, a){
|
|
868
|
+
var ring = outline(a.x, a.y, a.w, a.h, a.n.shape);
|
|
869
|
+
var gap = [Math.abs(p.y - a.y), Math.abs(a.y + a.h - p.y), Math.abs(p.x - a.x), Math.abs(a.x + a.w - p.x)];
|
|
870
|
+
var side = gap.indexOf(Math.min.apply(null, gap));
|
|
871
|
+
var ux = [0, 0, 1, -1][side], uy = [1, -1, 0, 0][side];
|
|
872
|
+
// from just outside the box, the first crossing of the outline
|
|
873
|
+
var ox = p.x - ux*2, oy = p.y - uy*2, best = Infinity;
|
|
874
|
+
for (var i=0;i<ring.length;i++){
|
|
875
|
+
var q = ring[i], r = ring[(i+1) % ring.length];
|
|
876
|
+
var ex = r.x - q.x, ey = r.y - q.y, den = ux*ey - uy*ex;
|
|
877
|
+
if (Math.abs(den) < 1e-9) continue;
|
|
878
|
+
var qx = q.x - ox, qy = q.y - oy;
|
|
879
|
+
var t = (qx*ey - qy*ex) / den, s = (qx*uy - qy*ux) / den;
|
|
880
|
+
if (s >= -1e-6 && s <= 1 + 1e-6 && t >= 0 && t < best) best = t;
|
|
881
|
+
}
|
|
882
|
+
return best < Infinity ? { x: ox + ux*best, y: oy + uy*best } : p;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function shapeEl(x, y, w, h, shape){
|
|
886
|
+
var poly = polyPoints(x, y, w, h, shape);
|
|
887
|
+
if (poly) return mk('polygon', { points: poly.map(function(p){ return p[0]+','+p[1]; }).join(' ') });
|
|
888
|
+
switch (shape){
|
|
889
|
+
case 'stadium': case 'pill':
|
|
890
|
+
return mk('rect', { x:x, y:y, width:w, height:h, rx:h/2, ry:h/2 });
|
|
891
|
+
case 'circle': case 'doublecircle': case 'state':
|
|
892
|
+
return mk('ellipse', { cx:x+w/2, cy:y+h/2, rx:w/2, ry:h/2 });
|
|
893
|
+
case 'cylinder': case 'database':
|
|
894
|
+
var e = Math.min(11, h/5);
|
|
895
|
+
return mk('path', { d:'M'+x+','+(y+e)+' A'+(w/2)+','+e+' 0 0 1 '+(x+w)+','+(y+e)+
|
|
896
|
+
' L'+(x+w)+','+(y+h-e)+' A'+(w/2)+','+e+' 0 0 1 '+x+','+(y+h-e)+' Z'+
|
|
897
|
+
' M'+x+','+(y+e)+' A'+(w/2)+','+e+' 0 0 0 '+(x+w)+','+(y+e) });
|
|
833
898
|
case 'squareRect': case 'rect': case 'square':
|
|
834
899
|
return mk('rect', { x:x, y:y, width:w, height:h, rx:3, ry:3 });
|
|
835
900
|
default:
|
|
@@ -1020,22 +1085,46 @@ export const DIAGRAMS_JS = String.raw`
|
|
|
1020
1085
|
|
|
1021
1086
|
// Right-angle bends look mechanical when they are exactly square; a small
|
|
1022
1087
|
// radius reads as a route rather than as a staircase.
|
|
1088
|
+
var BEND_R = 7;
|
|
1089
|
+
|
|
1090
|
+
// where the curve that rounds the bend at b starts and ends, or null if
|
|
1091
|
+
// there is no room for one
|
|
1092
|
+
function bendAt(a, b, c, r){
|
|
1093
|
+
var l1=Math.hypot(b.x-a.x,b.y-a.y), l2=Math.hypot(c.x-b.x,c.y-b.y);
|
|
1094
|
+
if (!l1 || !l2) return null;
|
|
1095
|
+
var rr = Math.min(r, l1/2, l2/2);
|
|
1096
|
+
return { from: { x: b.x+(a.x-b.x)/l1*rr, y: b.y+(a.y-b.y)/l1*rr },
|
|
1097
|
+
to: { x: b.x+(c.x-b.x)/l2*rr, y: b.y+(c.y-b.y)/l2*rr } };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1023
1100
|
function roundedPath(pts, r){
|
|
1024
1101
|
if (pts.length < 2) return '';
|
|
1025
|
-
if (pts.length === 2) return 'M'+pts[0].x+','+pts[0].y+' L'+pts[1].x+','+pts[1].y;
|
|
1026
1102
|
var d = 'M'+pts[0].x+','+pts[0].y;
|
|
1027
1103
|
for (var i=1;i<pts.length-1;i++){
|
|
1028
|
-
var
|
|
1029
|
-
|
|
1030
|
-
if (!l1 || !l2){ d += ' L'+b.x+','+b.y; continue; }
|
|
1031
|
-
var rr = Math.min(r, l1/2, l2/2);
|
|
1032
|
-
d += ' L'+(b.x+(a.x-b.x)/l1*rr)+','+(b.y+(a.y-b.y)/l1*rr) +
|
|
1033
|
-
' Q'+b.x+','+b.y+' '+(b.x+(c.x-b.x)/l2*rr)+','+(b.y+(c.y-b.y)/l2*rr);
|
|
1104
|
+
var b = pts[i], k = bendAt(pts[i-1], b, pts[i+1], r);
|
|
1105
|
+
d += k ? ' L'+k.from.x+','+k.from.y+' Q'+b.x+','+b.y+' '+k.to.x+','+k.to.y : ' L'+b.x+','+b.y;
|
|
1034
1106
|
}
|
|
1035
1107
|
var e = pts[pts.length-1];
|
|
1036
1108
|
return d + ' L'+e.x+','+e.y;
|
|
1037
1109
|
}
|
|
1038
1110
|
|
|
1111
|
+
// The route as roundedPath draws it, its curves walked in short steps. A
|
|
1112
|
+
// point found on this is on the line the reader sees; one found on the
|
|
1113
|
+
// square corner it rounds off sits beside the line, in the air.
|
|
1114
|
+
function roundedPts(pts, r){
|
|
1115
|
+
var out = [pts[0]];
|
|
1116
|
+
for (var i=1;i<pts.length-1;i++){
|
|
1117
|
+
var b = pts[i], k = bendAt(pts[i-1], b, pts[i+1], r);
|
|
1118
|
+
if (!k){ out.push(b); continue; }
|
|
1119
|
+
for (var j=0;j<=8;j++){
|
|
1120
|
+
var t = j/8, u = 1-t;
|
|
1121
|
+
out.push({ x: u*u*k.from.x + 2*u*t*b.x + t*t*k.to.x, y: u*u*k.from.y + 2*u*t*b.y + t*t*k.to.y });
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
out.push(pts[pts.length-1]);
|
|
1125
|
+
return out;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1039
1128
|
function nearestOnPath(pts, px, py){
|
|
1040
1129
|
var best = null, bd = Infinity;
|
|
1041
1130
|
for (var i=0;i<pts.length-1;i++){
|
|
@@ -1187,9 +1276,15 @@ export const DIAGRAMS_JS = String.raw`
|
|
|
1187
1276
|
runs.push([sec.startPoint].concat(sec.bendPoints || [], [sec.endPoint]).map(shift));
|
|
1188
1277
|
});
|
|
1189
1278
|
if (!runs.length) return;
|
|
1279
|
+
// both ends on the shape itself, not on the box ELK knows it by
|
|
1280
|
+
if (!isEr){
|
|
1281
|
+
var first = runs[0], last = runs[runs.length-1];
|
|
1282
|
+
if (abs[src] && !abs[src].n.isGroup) first[0] = onOutline(first[0], abs[src]);
|
|
1283
|
+
if (abs[dst] && !abs[dst].n.isGroup) last[last.length-1] = onOutline(last[last.length-1], abs[dst]);
|
|
1284
|
+
}
|
|
1190
1285
|
|
|
1191
1286
|
runs.forEach(function(pts, i){
|
|
1192
|
-
var d = roundedPath(pts,
|
|
1287
|
+
var d = roundedPath(pts, BEND_R);
|
|
1193
1288
|
g.appendChild(mk('path', { d:d, fill:'none' }, 'dg-edge-hit'));
|
|
1194
1289
|
var line = mk('path', { d:d, fill:'none' }, 'dg-edge-line');
|
|
1195
1290
|
if (info.pattern === 'dashed') line.style.strokeDasharray = '7 4';
|
|
@@ -1232,7 +1327,7 @@ export const DIAGRAMS_JS = String.raw`
|
|
|
1232
1327
|
var cx = lb.x + o.x + lb.width/2, cy = lb.y + o.y + lb.height/2;
|
|
1233
1328
|
var pw = info._lw || lb.width, ph = info._lh || lb.height;
|
|
1234
1329
|
var lx = cx - pw/2, ly = cy - ph/2;
|
|
1235
|
-
var anchor = pts.length > 1 ? nearestOnPath(pts, cx, cy) : null;
|
|
1330
|
+
var anchor = pts.length > 1 ? nearestOnPath(roundedPts(pts, BEND_R), cx, cy) : null;
|
|
1236
1331
|
if (anchor){
|
|
1237
1332
|
var dx = anchor.x - cx, dy = anchor.y - cy;
|
|
1238
1333
|
var hw = pw/2 + 3, hh = ph/2 + 3, ax = cx, ay = cy;
|
package/src/collect.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
-
import { esc, plain, slugify,
|
|
15
|
+
import { esc, plain, slugify, parkFences, renderBlocks, takeQuote } from './markdown.mjs';
|
|
16
16
|
import { nestSections, SECNUM_RE } from './sections.mjs';
|
|
17
17
|
import { titleCase } from './config.mjs';
|
|
18
18
|
|
|
@@ -93,7 +93,7 @@ export function buildDoc(entry, byFile, cfg, raw = fs.readFileSync(entry.abs, 'u
|
|
|
93
93
|
const id = byFile.get(entry.file);
|
|
94
94
|
const seen = new Map();
|
|
95
95
|
const ctx = {
|
|
96
|
-
blocks: [], headings: [], byFile, numbers: new Map(),
|
|
96
|
+
blocks: [], headings: [], byFile, numbers: new Map(), outline: [], reviews: cfg.reviews !== false,
|
|
97
97
|
uniqueSlug(base) {
|
|
98
98
|
const key = base || 'section';
|
|
99
99
|
const n = seen.get(key) || 0;
|
|
@@ -114,23 +114,19 @@ export function buildDoc(entry, byFile, cfg, raw = fs.readFileSync(entry.abs, 'u
|
|
|
114
114
|
lines.splice(h1, 1);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
// the
|
|
117
|
+
// the blockquote under it - all of it, every paragraph - becomes the subtitle
|
|
118
118
|
let j = 0;
|
|
119
119
|
while (j < lines.length && !lines[j].trim()) j++;
|
|
120
120
|
if (j < lines.length && /^ {0,3}>/.test(lines[j])) {
|
|
121
|
-
const lede =
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
j++;
|
|
125
|
-
}
|
|
126
|
-
subtitle = inline(lede.join(' '), ctx);
|
|
127
|
-
lines.splice(0, j);
|
|
121
|
+
const lede = takeQuote(lines, j);
|
|
122
|
+
subtitle = renderBlocks(lede.buf, ctx);
|
|
123
|
+
lines.splice(0, lede.end);
|
|
128
124
|
}
|
|
129
125
|
|
|
130
126
|
html = nestSections(renderBlocks(lines, ctx, true), ctx);
|
|
131
127
|
} else {
|
|
132
128
|
const kind = extOf(entry.file).slice(1);
|
|
133
|
-
subtitle = 'Kept verbatim as <code>' + esc(kind) + '</code
|
|
129
|
+
subtitle = '<p>Kept verbatim as <code>' + esc(kind) + '</code>.</p>';
|
|
134
130
|
html = '<div class="code-wrap"><span class="code-lang">' + esc(kind) +
|
|
135
131
|
'</span><pre class="code"><code>' + esc(raw) + '</code></pre></div>';
|
|
136
132
|
}
|
|
@@ -139,6 +135,7 @@ export function buildDoc(entry, byFile, cfg, raw = fs.readFileSync(entry.abs, 'u
|
|
|
139
135
|
|
|
140
136
|
return {
|
|
141
137
|
id, title, subtitle, html, badge,
|
|
138
|
+
outline: ctx.outline,
|
|
142
139
|
group: entry.group,
|
|
143
140
|
file: entry.rel || entry.file,
|
|
144
141
|
words: raw.split(/\s+/).filter(Boolean).length,
|
package/src/config.mjs
CHANGED
|
@@ -24,7 +24,7 @@ export const CLI = process.env.npm_command === 'exec' ? 'npx lowkey-doc' : 'lowk
|
|
|
24
24
|
|
|
25
25
|
const DEFAULTS = {
|
|
26
26
|
// Shown in the sidebar, the browser tab, and used to namespace the
|
|
27
|
-
// per-reader state (which document was open, folded sections, comments).
|
|
27
|
+
// per-reader state (which document was open, folded sections, comments, reviews).
|
|
28
28
|
title: null, // null -> derived from the project folder
|
|
29
29
|
// Where the documents are, relative to the project root.
|
|
30
30
|
docs: 'docs',
|
|
@@ -68,6 +68,7 @@ const DEFAULTS = {
|
|
|
68
68
|
},
|
|
69
69
|
// Reader features. Turn off what a given project does not want.
|
|
70
70
|
comments: true,
|
|
71
|
+
reviews: true,
|
|
71
72
|
search: true,
|
|
72
73
|
};
|
|
73
74
|
|
package/src/markdown.mjs
CHANGED
|
@@ -303,8 +303,9 @@ export function inline(src, ctx) {
|
|
|
303
303
|
s = s.replace(/~~([\s\S]+?)~~/g, '<del>$1</del>');
|
|
304
304
|
s = s.replace(/\*\*([\s\S]+?)\*\*/g, '<strong>$1</strong>');
|
|
305
305
|
s = s.replace(/(^|[^\w])__(\S[\s\S]*?\S|\S)__(?!\w)/g, '$1<strong>$2</strong>');
|
|
306
|
-
|
|
307
|
-
s = s.replace(/(^|[
|
|
306
|
+
// like strong, emphasis may run across a line break, never across a blank line
|
|
307
|
+
s = s.replace(/(^|[^*])\*(?!\s)((?:[^*\n]|\n(?!\n))+?)(?<!\s)\*(?!\*)/g, '$1<em>$2</em>');
|
|
308
|
+
s = s.replace(/(^|[^\w\\])_(?!\s)((?:[^_\n]|\n(?!\n))+?)(?<!\s)_(?!\w)/g, '$1<em>$2</em>');
|
|
308
309
|
|
|
309
310
|
// 6. bare urls left in plain text
|
|
310
311
|
s = s.replace(/(^|[\s(])(https?:\/\/[^\s<)]+[^\s<).,;:])/g,
|
|
@@ -359,6 +360,23 @@ const isDelim = (line) => {
|
|
|
359
360
|
};
|
|
360
361
|
const listMark = (line) => line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
|
|
361
362
|
const indentOf = (line) => (line.match(/^\s*/) || [''])[0].length;
|
|
363
|
+
const isRule = (line) => /^ {0,3}([-*_])(\s*\1){2,}\s*$/.test(line);
|
|
364
|
+
const HEADING_RE = /^(#{1,6})\s+(.+?)\s*#*\s*$/;
|
|
365
|
+
|
|
366
|
+
// The blockquote that starts at lines[i]: its lines without their markers, and
|
|
367
|
+
// the index just past it. A line with no marker still belongs to it when it
|
|
368
|
+
// carries on the quote's last paragraph, unless it starts a block of its own.
|
|
369
|
+
export function takeQuote(lines, i) {
|
|
370
|
+
const buf = [];
|
|
371
|
+
for (; i < lines.length; i++) {
|
|
372
|
+
const line = lines[i];
|
|
373
|
+
if (/^ {0,3}>/.test(line)) buf.push(line.replace(/^ {0,3}> ?/, ''));
|
|
374
|
+
else if (buf.length && buf[buf.length - 1].trim() && line.trim() &&
|
|
375
|
+
!listMark(line) && !isRule(line) && !HEADING_RE.test(line) && !BLOCK_RE.test(line)) buf.push(line);
|
|
376
|
+
else break;
|
|
377
|
+
}
|
|
378
|
+
return { buf, end: i };
|
|
379
|
+
}
|
|
362
380
|
|
|
363
381
|
export function renderBlocks(lines, ctx, asArray) {
|
|
364
382
|
const out = [];
|
|
@@ -380,7 +398,7 @@ export function renderBlocks(lines, ctx, asArray) {
|
|
|
380
398
|
const park = line.match(BLOCK_RE);
|
|
381
399
|
if (park) { flush(); out.push(ctx.blocks[+park[1]]); i++; continue; }
|
|
382
400
|
|
|
383
|
-
const head = line.match(
|
|
401
|
+
const head = line.match(HEADING_RE);
|
|
384
402
|
if (head) {
|
|
385
403
|
flush();
|
|
386
404
|
const level = head[1].length;
|
|
@@ -393,16 +411,13 @@ export function renderBlocks(lines, ctx, asArray) {
|
|
|
393
411
|
i++; continue;
|
|
394
412
|
}
|
|
395
413
|
|
|
396
|
-
if (
|
|
414
|
+
if (isRule(line)) { flush(); out.push('<hr>'); i++; continue; }
|
|
397
415
|
|
|
398
416
|
if (/^ {0,3}>/.test(line)) {
|
|
399
417
|
flush();
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
i++;
|
|
404
|
-
}
|
|
405
|
-
out.push('<blockquote>' + renderBlocks(buf, ctx) + '</blockquote>');
|
|
418
|
+
const quote = takeQuote(lines, i);
|
|
419
|
+
out.push('<blockquote>' + renderBlocks(quote.buf, ctx) + '</blockquote>');
|
|
420
|
+
i = quote.end;
|
|
406
421
|
continue;
|
|
407
422
|
}
|
|
408
423
|
|
package/src/page.mjs
CHANGED
|
@@ -17,6 +17,7 @@ export function renderPage({ cfg, docs, fontCss, mermaidTag, elkTag }) {
|
|
|
17
17
|
nodeTextWidth: cfg.diagrams.nodeTextWidth,
|
|
18
18
|
edgeTextWidth: cfg.diagrams.edgeTextWidth,
|
|
19
19
|
comments: cfg.comments !== false,
|
|
20
|
+
reviews: cfg.reviews !== false,
|
|
20
21
|
margin: cfg.layout.margin,
|
|
21
22
|
};
|
|
22
23
|
// fixed margins are set on the root, so the page is laid out right from its first paint
|
package/src/sections.mjs
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
const FOLD_SVG = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" ' +
|
|
10
10
|
'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 6L8 10.5 12.5 6"/></svg>';
|
|
11
|
+
const CHECK_SVG = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" ' +
|
|
12
|
+
'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6.3"/>' +
|
|
13
|
+
'<path d="M5.3 8.2l1.9 1.9 3.6-3.9"/></svg>';
|
|
14
|
+
// The word on the button is drawn by CSS, so it is never part of the heading's
|
|
15
|
+
// text: not copied with it, not matched by search, not quoted by a comment.
|
|
16
|
+
const REVIEW_BTN = '<button class="rv" type="button" aria-pressed="false" aria-label="Reviewed">' + CHECK_SVG + '</button>';
|
|
11
17
|
|
|
12
18
|
const HEAD_RE = /^<h([1-6]) id="([^"]+)">([\s\S]*)<\/h[1-6]>$/;
|
|
13
19
|
const ANCHOR_RE = /^<a class="h-anchor"[^>]*>#<\/a>/;
|
|
@@ -30,6 +36,11 @@ export function nestSections(blocks, ctx) {
|
|
|
30
36
|
stack[stack.length - 1].kids.push(node);
|
|
31
37
|
stack.push(node);
|
|
32
38
|
}
|
|
39
|
+
// the shape of the sections alone, which the page needs for reviews without
|
|
40
|
+
// having to read it back out of the HTML
|
|
41
|
+
const outline = (node) => node.kids.filter((k) => typeof k !== 'string')
|
|
42
|
+
.map((k) => ({ id: k.id, kids: outline(k) }));
|
|
43
|
+
ctx.outline = outline(root);
|
|
33
44
|
return emitSections(root, [], ctx);
|
|
34
45
|
}
|
|
35
46
|
|
|
@@ -55,7 +66,7 @@ function emitSections(node, prefix, ctx) {
|
|
|
55
66
|
'<button class="sec-tog" type="button" tabindex="-1" aria-label="Fold section">' + FOLD_SVG + '</button>' +
|
|
56
67
|
'<span class="sec-num">' + label + '</span>' +
|
|
57
68
|
'</span>' +
|
|
58
|
-
'<span class="sec-txt">' + text + '</span>' + anchor +
|
|
69
|
+
'<span class="sec-txt">' + text + '</span>' + anchor + (ctx.reviews ? REVIEW_BTN : '') +
|
|
59
70
|
'</h' + kid.level + '>';
|
|
60
71
|
return '<section class="sec" data-sec="' + kid.id + '">' + head +
|
|
61
72
|
'<div class="sec-body">' + emitSections(kid, num, ctx) + '</div></section>';
|