paperlint 14.0.0 → 14.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/dist/skills/build-label-page/scripts/label-page-core.d.ts +68 -1
- package/dist/skills/build-label-page/scripts/label-page-presets.d.ts +10 -0
- package/package.json +1 -1
- package/skills/build-label-page/SKILL.md +6 -2
- package/skills/build-label-page/SKILL.md.spec.ts +5 -1
- package/skills/build-label-page/references/closed-choice-generator.md +129 -5
- package/skills/build-label-page/scripts/fixtures/blocks-items.json +214 -0
- package/skills/build-label-page/scripts/fixtures/linter-config-witness-items.json +448 -0
- package/skills/build-label-page/scripts/label-page-blocks.test.ts +335 -0
- package/skills/build-label-page/scripts/label-page-browser.test.ts +328 -2
- package/skills/build-label-page/scripts/label-page-core.js +154 -12
- package/skills/build-label-page/scripts/label-page-core.test.ts +54 -2
- package/skills/build-label-page/scripts/label-page-core.ts +253 -31
- package/skills/build-label-page/scripts/label-page-html.test.ts +22 -0
- package/skills/build-label-page/scripts/label-page-presets.js +222 -0
- package/skills/build-label-page/scripts/label-page-presets.test.ts +175 -0
- package/skills/build-label-page/scripts/label-page-presets.ts +257 -0
- package/skills/build-label-page/scripts/label-page.js +8 -2
- package/skills/build-label-page/scripts/label-page.ts +8 -2
- package/skills/build-label-page/scripts/page-template.html +237 -30
|
@@ -82,6 +82,27 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
82
82
|
background: var(--good); margin-right: 8px; vertical-align: middle; }
|
|
83
83
|
.status.missing { color: var(--mark); }
|
|
84
84
|
.status.failed { color: var(--bad); }
|
|
85
|
+
/* The content blocks (#380): a code excerpt with its line numbers and the marked line lit, references,
|
|
86
|
+
lists and tables folded away. A long text shows three lines and a «more». */
|
|
87
|
+
a { color: var(--accent); }
|
|
88
|
+
summary { cursor: pointer; color: var(--accent); min-height: 44px; display: flex; align-items: center; gap: 8px; }
|
|
89
|
+
.situation { color: var(--strong); font-weight: 500; }
|
|
90
|
+
pre.numbered { white-space: normal; padding: 8px 0; }
|
|
91
|
+
pre.numbered .line { display: grid; grid-template-columns: 2ch 5ch 1fr; white-space: pre-wrap; padding: 0 10px 0 0; }
|
|
92
|
+
/* The marked line reads as marked without its colour: a marker in the gutter and a label after the code. */
|
|
93
|
+
pre.numbered .g { color: var(--mark); text-align: center; user-select: none; }
|
|
94
|
+
pre.numbered .this { color: var(--mark); font-weight: 700; margin-left: 1ch; white-space: nowrap; user-select: none; }
|
|
95
|
+
pre.numbered .n { color: var(--muted); text-align: right; padding-right: 1.5ch; user-select: none; }
|
|
96
|
+
pre.numbered .hit { background: var(--mark-bg); }
|
|
97
|
+
pre.numbered .hit .n { color: var(--mark); font-weight: 700; }
|
|
98
|
+
.rulebox { display: flex; flex-direction: column; gap: 8px; }
|
|
99
|
+
.clamped { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
|
100
|
+
/* The clamp cuts at the padding box, so a code block's bottom padding is a border: no fourth line peeks in. */
|
|
101
|
+
pre.clamped { padding-bottom: 0; border-bottom: 10px solid var(--surface); box-shadow: 0 1px 0 var(--line); }
|
|
102
|
+
button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); padding: 0 4px; align-self: flex-start; }
|
|
103
|
+
.setting { display: flex; flex-direction: column; gap: 2px; padding-top: 12px; border-top: 1px solid var(--line); }
|
|
104
|
+
.match.yes { color: var(--good); }
|
|
105
|
+
.match.no { color: var(--bad); }
|
|
85
106
|
</style>
|
|
86
107
|
<main id="app"><p class="muted">Loading…</p></main>
|
|
87
108
|
<script type="application/json" id="data">__DATA__</script>
|
|
@@ -100,6 +121,9 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
100
121
|
whyTimers: {}, // "<item id>|<question id>" -> the pending save of a reason being typed
|
|
101
122
|
whyBoxes: {}, // "<item id>|<question id>" -> the reason box of the last render
|
|
102
123
|
typingIn: null, // the item whose reason box has the keyboard: shown even when complete
|
|
124
|
+
lastAnswered: null, // the item answered last: shown under "Next unanswered" until the next answer
|
|
125
|
+
pending: {}, // item id -> answers chosen whose reason is still empty: NOT in the store
|
|
126
|
+
refusedWhy: {}, // item id -> true when an empty reason was refused for its answer
|
|
103
127
|
mine: {}, // item/group key -> the answers this page has just written (ahead of the store)
|
|
104
128
|
inflight: {}, // item/group key -> writes not yet confirmed
|
|
105
129
|
failed: {}, // item/group key -> error code of the last refused write
|
|
@@ -115,9 +139,7 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
115
139
|
|
|
116
140
|
// ── what "done" means ────────────────────────────────────────────────────────────────────
|
|
117
141
|
function answersOf(id) {
|
|
118
|
-
|
|
119
|
-
var d = state.answers[id];
|
|
120
|
-
return d && d.answers ? d.answers : {};
|
|
142
|
+
return state.pending[id] || savedOf(id);
|
|
121
143
|
}
|
|
122
144
|
// A stored value is an answer only if the build offers it (DATA.allowed, the same lists the
|
|
123
145
|
// export reads from key.json): after a rebuild that removed an option, or a typo merged from chat,
|
|
@@ -141,6 +163,7 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
141
163
|
return d[qid];
|
|
142
164
|
}
|
|
143
165
|
function needsWhy(q, v) { return (DATA.allowed.needs_why[q.id] || []).indexOf(v) !== -1; }
|
|
166
|
+
function questionOf(qid) { return DATA.questions.filter(function (q) { return q.id === qid; })[0]; }
|
|
144
167
|
// What an item still needs: an answer, or the reason its answer asks for (named "<short> (why)").
|
|
145
168
|
function missingFor(id) {
|
|
146
169
|
var a = answersOf(id), r = reasonsOf(id);
|
|
@@ -202,25 +225,71 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
202
225
|
function canWrite() {
|
|
203
226
|
return db && state.loaded.answers && state.loaded.groups && state.loaded.practice && state.store === "ok";
|
|
204
227
|
}
|
|
228
|
+
// An answer that asks why is not written while its reason is empty (#380): it waits on the page, and
|
|
229
|
+
// the store keeps what it had, so an export never meets an answer without the reason it needs. Every
|
|
230
|
+
// write leaves out each such answer, not only the one just picked: on a page of several questions the
|
|
231
|
+
// next answer would otherwise carry the waiting one into the store without its reason.
|
|
232
|
+
// A waiting answer that replaces a saved one leaves the saved one in the write until its reason comes.
|
|
233
|
+
// A stored answer to a question this page no longer asks (removed or renamed between builds) is kept
|
|
234
|
+
// as it was and never checked: the page has no question to check it against.
|
|
235
|
+
function resolved(answers, reasons, saved) {
|
|
236
|
+
var out = {}, waiting = false;
|
|
237
|
+
Object.keys(answers).forEach(function (q) {
|
|
238
|
+
var asked = questionOf(q);
|
|
239
|
+
if (!(asked && needsWhy(asked, answers[q]) && !(reasons[q] || "").trim())) out[q] = answers[q];
|
|
240
|
+
else {
|
|
241
|
+
waiting = true;
|
|
242
|
+
if (Object.prototype.hasOwnProperty.call(saved, q)) out[q] = saved[q];
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
return { ready: out, waiting: waiting };
|
|
246
|
+
}
|
|
247
|
+
// What the store holds for an item, or what this page last wrote to it ahead of the store.
|
|
248
|
+
function savedOf(id) {
|
|
249
|
+
if (state.mine[id]) return state.mine[id];
|
|
250
|
+
var d = state.answers[id];
|
|
251
|
+
return d && d.answers ? d.answers : {};
|
|
252
|
+
}
|
|
253
|
+
function keep(id, next, reasons) {
|
|
254
|
+
var r = resolved(next, reasons, savedOf(id)), ready = r.ready;
|
|
255
|
+
if (r.waiting) state.pending[id] = next;
|
|
256
|
+
else delete state.pending[id];
|
|
257
|
+
delete state.refusedWhy[id];
|
|
258
|
+
state.mine[id] = ready;
|
|
259
|
+
write(id, "answers", id, { answers: ready, reasons: reasons, savedAt: new Date().toISOString(), source: "page" });
|
|
260
|
+
render();
|
|
261
|
+
}
|
|
205
262
|
function answer(id, qid, value) {
|
|
206
263
|
if (!canWrite()) return;
|
|
207
264
|
var next = Object.assign({}, answersOf(id));
|
|
208
265
|
next[qid] = value;
|
|
209
|
-
state.
|
|
210
|
-
|
|
211
|
-
|
|
266
|
+
state.lastAnswered = id;
|
|
267
|
+
var draft = draftOf(id, qid);
|
|
268
|
+
var why = draft !== undefined ? draft : reasonsOf(id)[qid] || "";
|
|
269
|
+
if (needsWhy(questionOf(qid), value) && !why.trim()) {
|
|
270
|
+
state.pending[id] = next;
|
|
271
|
+
render();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
var reasons = Object.assign({}, reasonsOf(id));
|
|
275
|
+
if (why.trim()) reasons[qid] = why;
|
|
276
|
+
keep(id, next, reasons);
|
|
212
277
|
}
|
|
213
|
-
// A reason saves when the labeller leaves the box (the text equivalent of a tap).
|
|
278
|
+
// A reason saves when the labeller leaves the box (the text equivalent of a tap); an empty one is refused.
|
|
214
279
|
function setReason(id, qid, text) {
|
|
215
280
|
clearTimeout(state.whyTimers[id + "|" + qid]);
|
|
216
281
|
delete state.whyTimers[id + "|" + qid];
|
|
217
282
|
if (!canWrite()) return;
|
|
283
|
+
var a = answersOf(id);
|
|
284
|
+
if (needsWhy(questionOf(qid), a[qid]) && !text.trim()) {
|
|
285
|
+
state.refusedWhy[id] = true;
|
|
286
|
+
render();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
218
289
|
var why = Object.assign({}, reasonsOf(id));
|
|
219
290
|
why[qid] = text;
|
|
220
291
|
state.mineWhy[id] = why;
|
|
221
|
-
|
|
222
|
-
write(id, "answers", id, { answers: answersOf(id), reasons: why, savedAt: new Date().toISOString(), source: "page" });
|
|
223
|
-
render();
|
|
292
|
+
keep(id, Object.assign({}, a), why);
|
|
224
293
|
}
|
|
225
294
|
// The first answer to a practice question is the one scored: it cannot be changed afterwards.
|
|
226
295
|
function answerPractice(pid, qid, value) {
|
|
@@ -257,8 +326,8 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
257
326
|
});
|
|
258
327
|
return n;
|
|
259
328
|
}
|
|
260
|
-
// `withExamples`: the practice shows each answer's example on its button
|
|
261
|
-
// short, the examples stay on the first screen.
|
|
329
|
+
// `withExamples`: the practice shows each answer's example on its button, after `labels.practice_example`;
|
|
330
|
+
// the real items keep the buttons short, the examples stay on the first screen.
|
|
262
331
|
// A code block shows what a labeller would otherwise count by eye: leading and trailing whitespace as
|
|
263
332
|
// faint marks (a space as a middle dot, a tab as an arrow) and each line's length in characters.
|
|
264
333
|
function visible(ws) { return ws.replace(/ /g, "\u00b7").replace(/\t/g, "\u2192"); }
|
|
@@ -281,7 +350,7 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
281
350
|
var b = el("button", "");
|
|
282
351
|
if (o.icon) b.appendChild(el("span", "icon", o.icon));
|
|
283
352
|
b.appendChild(el("span", "label", o.label));
|
|
284
|
-
if (withExamples && o.example) b.appendChild(rich("span", "ex", o.example));
|
|
353
|
+
if (withExamples && o.example) b.appendChild(rich("span", "ex", DATA.labels.practice_example + o.example));
|
|
285
354
|
b.type = "button";
|
|
286
355
|
b.setAttribute("aria-pressed", String(current === o.value));
|
|
287
356
|
b.disabled = disabled;
|
|
@@ -296,7 +365,10 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
296
365
|
}
|
|
297
366
|
function statusLine(key, missing, savedAt) {
|
|
298
367
|
var s = el("p", "status");
|
|
299
|
-
if (state.
|
|
368
|
+
if (state.pending[key] || state.refusedWhy[key]) {
|
|
369
|
+
s.className = "status missing";
|
|
370
|
+
s.textContent = "Not saved yet: this answer needs a reason. Write it above, or pick another answer.";
|
|
371
|
+
} else if (state.failed[key]) {
|
|
300
372
|
s.className = "status failed";
|
|
301
373
|
s.textContent = "Not saved (" + state.failed[key] + "). Tap the answer again.";
|
|
302
374
|
} else if (state.inflight[key]) {
|
|
@@ -312,15 +384,14 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
312
384
|
return s;
|
|
313
385
|
}
|
|
314
386
|
function itemCard(item, off) {
|
|
315
|
-
var done = itemDone(item.id);
|
|
316
387
|
var card = el("article", "item");
|
|
317
|
-
card.appendChild(el("div", "id", item.id));
|
|
388
|
+
card.appendChild(el("div", "id", fill(DATA.labels.place, { id: item.id, n: DATA.items.indexOf(item) + 1, total: DATA.items.length })));
|
|
318
389
|
evidence(card, item);
|
|
319
390
|
var a = answersOf(item.id);
|
|
320
391
|
askOne(card, a, state.editing[item.id], function (q, v) {
|
|
321
392
|
delete state.editing[item.id];
|
|
322
393
|
answer(item.id, q.id, v);
|
|
323
|
-
}, function (q) { state.editing[item.id] = q.id; render(); }, off);
|
|
394
|
+
}, function (q) { state.editing[item.id] = q.id; render(); }, off, false, item.situation);
|
|
324
395
|
DATA.questions.forEach(function (q) {
|
|
325
396
|
var o = q.options.filter(function (x) { return x.value === a[q.id] && x.ask_why; })[0];
|
|
326
397
|
if (!o) return;
|
|
@@ -344,6 +415,7 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
344
415
|
});
|
|
345
416
|
var saved = state.answers[item.id] && state.answers[item.id].savedAt;
|
|
346
417
|
card.appendChild(statusLine(item.id, missingFor(item.id), saved));
|
|
418
|
+
blocksBelow(card, item);
|
|
347
419
|
return card;
|
|
348
420
|
}
|
|
349
421
|
// A labelled row: a short word, then what it labels.
|
|
@@ -361,17 +433,136 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
361
433
|
p.appendChild(rich("span", "", u.text.slice(at + u.mark.length)));
|
|
362
434
|
return p;
|
|
363
435
|
}
|
|
436
|
+
// A situation's text with each `{line}` written out: «line N: » and the marked line's code as code
|
|
437
|
+
// (the build passes `line` whenever the text holds the token).
|
|
438
|
+
function situated(tag, cls, text, line) {
|
|
439
|
+
var n = el(tag, cls);
|
|
440
|
+
String(text).split("{line}").forEach(function (part, i) {
|
|
441
|
+
if (i) {
|
|
442
|
+
n.appendChild(el("span", "", "line " + line.n + ": "));
|
|
443
|
+
n.appendChild(el("code", "", line.text));
|
|
444
|
+
}
|
|
445
|
+
if (part) n.appendChild(rich("span", "", part));
|
|
446
|
+
});
|
|
447
|
+
return n;
|
|
448
|
+
}
|
|
449
|
+
// A long text shows its first three lines and a «more» that opens the rest.
|
|
450
|
+
function clamp(node, text) {
|
|
451
|
+
if (String(text).length < 200 && String(text).split("\n").length <= 3) return node;
|
|
452
|
+
var wrap = el("div", "rulebox");
|
|
453
|
+
node.className += " clamped";
|
|
454
|
+
var more = el("button", "more", "more");
|
|
455
|
+
more.type = "button";
|
|
456
|
+
more.addEventListener("click", function () {
|
|
457
|
+
node.className = node.className.replace(" clamped", "");
|
|
458
|
+
wrap.removeChild(more);
|
|
459
|
+
});
|
|
460
|
+
wrap.appendChild(node);
|
|
461
|
+
wrap.appendChild(more);
|
|
462
|
+
return wrap;
|
|
463
|
+
}
|
|
464
|
+
function link(text, url) {
|
|
465
|
+
var a = el("a", "", text);
|
|
466
|
+
a.href = url;
|
|
467
|
+
a.target = "_blank";
|
|
468
|
+
a.rel = "noopener";
|
|
469
|
+
return a;
|
|
470
|
+
}
|
|
471
|
+
function plainCode(text, cls) {
|
|
472
|
+
var pre = el("pre", cls || "");
|
|
473
|
+
pre.textContent = text;
|
|
474
|
+
return pre;
|
|
475
|
+
}
|
|
476
|
+
// `{name}` in a page label, from `vars`.
|
|
477
|
+
function fill(text, vars) {
|
|
478
|
+
return String(text).replace(/\{(\w+)\}/g, function (m, k) { return k in vars ? String(vars[k]) : m; });
|
|
479
|
+
}
|
|
480
|
+
// A code excerpt: its file (a link when there is one), its lines numbered, the marked line with a
|
|
481
|
+
// gutter marker and a label besides its background, and the caption under it.
|
|
482
|
+
function excerptBlock(ex) {
|
|
483
|
+
var box = el("div", "rulebox");
|
|
484
|
+
if (ex.file) {
|
|
485
|
+
var name = el("p");
|
|
486
|
+
name.appendChild(ex.url ? link(ex.file, ex.url) : el("code", "", ex.file));
|
|
487
|
+
box.appendChild(name);
|
|
488
|
+
}
|
|
489
|
+
var pre = el("pre", "numbered");
|
|
490
|
+
ex.lines.forEach(function (l) {
|
|
491
|
+
var line = el("div", "line" + (l.mark ? " hit" : ""));
|
|
492
|
+
line.appendChild(el("span", "g", l.mark ? "\u25b6" : ""));
|
|
493
|
+
line.appendChild(el("span", "n", String(l.n)));
|
|
494
|
+
var code = line.appendChild(el("span", "", l.text));
|
|
495
|
+
if (l.mark) code.appendChild(el("span", "this", ex.mark_label || "\u25c0 this line"));
|
|
496
|
+
pre.appendChild(line);
|
|
497
|
+
});
|
|
498
|
+
box.appendChild(pre);
|
|
499
|
+
if (ex.caption) box.appendChild(rich("p", "said", ex.caption));
|
|
500
|
+
return box;
|
|
501
|
+
}
|
|
502
|
+
// A reference entry: its title, then each field (a paragraph, a code block on a labelled row, or a
|
|
503
|
+
// quiet note), then its link.
|
|
504
|
+
function referenceBlock(r) {
|
|
505
|
+
var box = el("div", "rulebox");
|
|
506
|
+
box.appendChild(rich("p", "", r.title));
|
|
507
|
+
r.fields.forEach(function (f) {
|
|
508
|
+
if (f.style === "note") box.appendChild(el("p", "muted", f.text));
|
|
509
|
+
else if (f.style === "code") box.appendChild(row(f.label || "", clamp(plainCode(f.text), f.text)));
|
|
510
|
+
else box.appendChild(clamp(rich("p", "", (f.label ? f.label + ": " : "") + f.text), f.text));
|
|
511
|
+
});
|
|
512
|
+
if (r.url) box.appendChild(el("p")).appendChild(link(r.url_label || r.url, r.url));
|
|
513
|
+
return box;
|
|
514
|
+
}
|
|
515
|
+
function folded(summary) {
|
|
516
|
+
var d = el("details");
|
|
517
|
+
d.appendChild(el("summary", "", summary));
|
|
518
|
+
return d;
|
|
519
|
+
}
|
|
520
|
+
function references(x) {
|
|
521
|
+
var d = folded(x.summary);
|
|
522
|
+
x.entries.forEach(function (r) { d.appendChild(row(r.label || "", referenceBlock(r))); });
|
|
523
|
+
return d;
|
|
524
|
+
}
|
|
525
|
+
function moreList(m) {
|
|
526
|
+
var d = folded(m.summary);
|
|
527
|
+
var ul = el("ul");
|
|
528
|
+
m.items.forEach(function (f) { ul.appendChild(rich("li", "", f)); });
|
|
529
|
+
if (m.rest) ul.appendChild(el("li", "muted", m.rest));
|
|
530
|
+
d.appendChild(ul);
|
|
531
|
+
return d;
|
|
532
|
+
}
|
|
533
|
+
// A folded table: its note, then each row in the order given: label, value as code, plain words, mark.
|
|
534
|
+
function table(t) {
|
|
535
|
+
var d = folded(t.summary);
|
|
536
|
+
if (t.note) d.appendChild(el("p", "muted", t.note));
|
|
537
|
+
t.rows.forEach(function (r) {
|
|
538
|
+
var s = el("div", "setting");
|
|
539
|
+
s.appendChild(el("span", "lbl", r.label));
|
|
540
|
+
s.appendChild(el("p")).appendChild(el("code", "", r.value));
|
|
541
|
+
if (r.plain) s.appendChild(rich("p", "", r.plain));
|
|
542
|
+
if (r.mark) s.appendChild(el("p", "match " + r.mark.tone, r.mark.text));
|
|
543
|
+
d.appendChild(s);
|
|
544
|
+
});
|
|
545
|
+
return d;
|
|
546
|
+
}
|
|
547
|
+
// Below the question and its answers: the references, the list of more like it and the table, each
|
|
548
|
+
// folded, so the question sits right under the code it names.
|
|
549
|
+
function blocksBelow(card, item) {
|
|
550
|
+
if (item.references) card.appendChild(references(item.references));
|
|
551
|
+
if (item.more) card.appendChild(moreList(item.more));
|
|
552
|
+
if (item.table) card.appendChild(table(item.table));
|
|
553
|
+
}
|
|
364
554
|
// What a card shows above its question, one labelled row per field (an item and a practice item alike).
|
|
365
555
|
function evidence(card, item) {
|
|
366
556
|
if (item.title) card.appendChild(rich("h3", "", item.title));
|
|
367
|
-
|
|
557
|
+
var body = rich("p", "body", item.body);
|
|
558
|
+
card.appendChild(DATA.labels.body ? row(DATA.labels.body, body) : body);
|
|
559
|
+
if (item.repeat && item.repeat.of > 1) card.appendChild(el("p", "muted", fill(DATA.labels.repeat, item.repeat)));
|
|
368
560
|
if (item.code) card.appendChild(codeBlock(item.code));
|
|
369
|
-
if (item.unit) card.appendChild(row(item.unit.of > 1 ?
|
|
561
|
+
if (item.unit) card.appendChild(row(item.unit.of > 1 ? fill(DATA.labels.line_part, { line: DATA.labels.line, part: item.unit.part, of: item.unit.of }) : DATA.labels.line,
|
|
370
562
|
markedLine(item.unit)));
|
|
371
563
|
// A rule the item names, with what it does in the linter's own words (looked up at build time).
|
|
372
564
|
(item.rules || []).forEach(function (r) {
|
|
373
|
-
card.appendChild(row("Rule", rich("p", "", r.linter + " `" + r.id + "`" + (r.family ? " (" + r.family + ")" : "") +
|
|
374
|
-
": " + r.description)));
|
|
565
|
+
card.appendChild(row("Rule", rich("p", "", r.linter + " `" + r.id + "`" + (r.family ? " (" + r.family + ")" : "") + ": " + r.description)));
|
|
375
566
|
});
|
|
376
567
|
if (item.readings) {
|
|
377
568
|
card.appendChild(row("Reading A", rich("p", "", item.readings.a), "reading a"));
|
|
@@ -379,6 +570,8 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
379
570
|
}
|
|
380
571
|
if (item.file) card.appendChild(row("File", rich("p", "", item.file)));
|
|
381
572
|
if (item.context) card.appendChild(rich("p", "muted", item.context));
|
|
573
|
+
if (item.situation) card.appendChild(situated("p", "situation", item.situation.summary, item.situation.line));
|
|
574
|
+
if (item.excerpt) card.appendChild(excerptBlock(item.excerpt));
|
|
382
575
|
}
|
|
383
576
|
function labelOf(q, v) {
|
|
384
577
|
var o = q.options.filter(function (x) { return x.value === v; })[0];
|
|
@@ -386,7 +579,7 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
386
579
|
}
|
|
387
580
|
// One question per screen: the answered ones stay as a line (with "Change"), and only one question
|
|
388
581
|
// is asked: the one being changed, else the first unanswered.
|
|
389
|
-
function askOne(card, a, editing, onPick, onChange, off, withExamples) {
|
|
582
|
+
function askOne(card, a, editing, onPick, onChange, off, withExamples, situation) {
|
|
390
583
|
var open = DATA.questions.filter(function (q) { return !valid(DATA.allowed.questions[q.id], a[q.id]); });
|
|
391
584
|
var asked = DATA.questions.filter(function (q) { return q.id === editing; })[0] || open[0];
|
|
392
585
|
DATA.questions.forEach(function (q) {
|
|
@@ -403,15 +596,22 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
403
596
|
});
|
|
404
597
|
if (!asked) return;
|
|
405
598
|
var ask = el("div", "ask");
|
|
406
|
-
|
|
599
|
+
// A card's situation asks the page's first question in its own words; the others keep theirs.
|
|
600
|
+
ask.appendChild(situation && asked === DATA.questions[0] ? situated("p", "q", situation.question, situation.line) : rich("p", "q", asked.text));
|
|
407
601
|
ask.appendChild(options(asked.options, a[asked.id], function (v) { onPick(asked, v); }, off, withExamples));
|
|
408
602
|
card.appendChild(ask);
|
|
409
603
|
}
|
|
604
|
+
// Open until the practice is done; then folded into a toggle, so the cards start near the top.
|
|
410
605
|
function howTo() {
|
|
411
606
|
var sec = el("section");
|
|
412
|
-
|
|
607
|
+
var body = sec;
|
|
608
|
+
if (practiceOpen()) sec.appendChild(el("h2", "", "📖 How to answer"));
|
|
609
|
+
else {
|
|
610
|
+
body = sec.appendChild(el("details"));
|
|
611
|
+
body.appendChild(el("summary", "", "📖 How to answer"));
|
|
612
|
+
}
|
|
413
613
|
DATA.questions.forEach(function (q) {
|
|
414
|
-
|
|
614
|
+
body.appendChild(el("p", "q", q.text));
|
|
415
615
|
var list = el("div", "examples");
|
|
416
616
|
q.options.forEach(function (o) {
|
|
417
617
|
var row = el("div", "example");
|
|
@@ -419,7 +619,7 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
419
619
|
row.appendChild(rich("span", "muted", "For example: " + o.example));
|
|
420
620
|
list.appendChild(row);
|
|
421
621
|
});
|
|
422
|
-
|
|
622
|
+
body.appendChild(list);
|
|
423
623
|
});
|
|
424
624
|
return sec;
|
|
425
625
|
}
|
|
@@ -428,13 +628,14 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
428
628
|
card.appendChild(el("div", "id", "Practice " + p.id));
|
|
429
629
|
evidence(card, p);
|
|
430
630
|
var a = practiceAnswers(p.id);
|
|
431
|
-
askOne(card, a, undefined, function (q, v) { answerPractice(p.id, q.id, v); }, null, off, true);
|
|
631
|
+
askOne(card, a, undefined, function (q, v) { answerPractice(p.id, q.id, v); }, null, off, true, p.situation);
|
|
432
632
|
if (practiceAnswered(p)) {
|
|
433
633
|
var expected = DATA.questions.map(function (q) { return labelOf(q, p.expected[q.id]); }).join(" · ");
|
|
434
634
|
card.appendChild(rich("p", practiceMatched(p) ? "status saved" : "quote",
|
|
435
|
-
(practiceMatched(p) ? "Matched. " : "Expected: " + expected + ". ") + p.why));
|
|
635
|
+
(practiceMatched(p) ? "Matched. " : "Not matched. Expected: " + expected + ". ") + p.why));
|
|
436
636
|
}
|
|
437
637
|
card.appendChild(statusLine("practice:" + p.id, [], null));
|
|
638
|
+
blocksBelow(card, p);
|
|
438
639
|
return card;
|
|
439
640
|
}
|
|
440
641
|
function practiceBlock(off) {
|
|
@@ -470,7 +671,7 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
470
671
|
sec.appendChild(fam);
|
|
471
672
|
}
|
|
472
673
|
groupItems(g.id).forEach(function (i) {
|
|
473
|
-
if (onlyOpen && itemDone(i.id) && i.id !== state.typingIn) return;
|
|
674
|
+
if (onlyOpen && itemDone(i.id) && i.id !== state.typingIn && i.id !== state.lastAnswered) return;
|
|
474
675
|
sec.appendChild(itemCard(i, off));
|
|
475
676
|
});
|
|
476
677
|
return sec;
|
|
@@ -536,6 +737,12 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
536
737
|
fill.style.width = (total ? (100 * done / total) : 0) + "%";
|
|
537
738
|
bar.appendChild(fill);
|
|
538
739
|
progress.appendChild(bar);
|
|
740
|
+
// Done means every item AND every group's familiarity, as «Next unanswered» counts it.
|
|
741
|
+
var rated = DATA.groups.every(function (g) { return familiarity(g.id) !== undefined; });
|
|
742
|
+
if (total && done === total && rated && state.filter === "all") {
|
|
743
|
+
progress.appendChild(el("p", "status saved", "All " + total + " items are complete. Tell the agent you are " +
|
|
744
|
+
"done; you can still change any answer."));
|
|
745
|
+
}
|
|
539
746
|
|
|
540
747
|
var filters = el("div", "tabs");
|
|
541
748
|
[["all", "All items"], ["next", "Next unanswered"]].forEach(function (f) {
|
|
@@ -571,7 +778,7 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
|
|
|
571
778
|
progress.appendChild(note);
|
|
572
779
|
}
|
|
573
780
|
DATA.groups.forEach(function (g) {
|
|
574
|
-
var typingHere = groupItems(g.id).some(function (i) { return i.id === state.typingIn; });
|
|
781
|
+
var typingHere = groupItems(g.id).some(function (i) { return i.id === state.typingIn || i.id === state.lastAnswered; });
|
|
575
782
|
if (onlyOpen && groupDone(g.id) && !typingHere) return;
|
|
576
783
|
app.appendChild(groupBlock(g, off, onlyOpen));
|
|
577
784
|
});
|