paperlint 13.3.2 → 13.5.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.
@@ -40,6 +40,7 @@
40
40
  * "seed": 7, fixes the order; change it and every id changes
41
41
  * "groups": {"python": "Python"},
42
42
  * "labeller_knows": ["python"], optional: items of other groups are not put on the page
43
+ * "familiarity": false, optional: no "how well do you know" question (approval page)
43
44
  * "familiarity_question": "How well do you know {group}?", optional
44
45
  * "questions": [{"id": "label", "text": "Is this correct?", "short": "correct?",
45
46
  * "options": [{"value": "yes", "label": "Yes", "example": "one real case",
@@ -52,7 +53,11 @@
52
53
  * "practice": [{"body": "…", "expected": {"label": "yes"}, "why": "one line"}, …], 3 or more
53
54
  * "practice_round": 1, optional; raise it after rewording, the practice reopens
54
55
  * "items": [{"source_id": "case-0412-b", "group": "python", "body": "…", "code": "…",
55
- * "context": "…"}]
56
+ * "context": "…",
57
+ * "unit": {"id": "a.md:12", "text": "the whole line", "part": 1, "of": 2}}]
58
+ * unit: optional; one requirement of a line that holds
59
+ * several. Every part of the line is an item, in one group;
60
+ * id (never shown) tells two places with the same words apart.
56
61
  * }
57
62
  *
58
63
  * TypeScript, compiled beside itself to `label-page.js` by `npm run build`: a consumer runs the skill
@@ -90,6 +90,9 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
90
90
  editing: {}, // item id -> the question id the labeller tapped "Change" on
91
91
  mineWhy: {}, // item id -> the reasons this page has just written (ahead of the store)
92
92
  draftWhy: {}, // item id -> {question id: text typed and not yet saved}
93
+ whyTimers: {}, // "<item id>|<question id>" -> the pending save of a reason being typed
94
+ whyBoxes: {}, // "<item id>|<question id>" -> the reason box of the last render
95
+ typingIn: null, // the item whose reason box has the keyboard: shown even when complete
93
96
  mine: {}, // item/group key -> the answers this page has just written (ahead of the store)
94
97
  inflight: {}, // item/group key -> writes not yet confirmed
95
98
  failed: {}, // item/group key -> error code of the last refused write
@@ -99,6 +102,7 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
99
102
  };
100
103
  var db = null;
101
104
  var chains = {};
105
+ var WHY_PAUSE_MS = 800;
102
106
 
103
107
  try { if (localStorage.getItem("label-filter") === "next") state.filter = "next"; } catch (e) {}
104
108
 
@@ -142,7 +146,11 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
142
146
  }).filter(function (q) { return q !== null; });
143
147
  }
144
148
  function itemDone(id) { return missingFor(id).length === 0; }
149
+ // A page built with "familiarity": false asks no group anything (an approval page): every group
150
+ // counts as rated, and neither the question nor "how well you know it" is ever shown.
151
+ var ASKS_FAMILIARITY = DATA.familiarity.length > 0;
145
152
  function familiarity(g) {
153
+ if (!ASKS_FAMILIARITY) return null;
146
154
  var d = state.mine["group:" + g] || state.groups[g];
147
155
  return d && valid(DATA.allowed.familiarity, d.familiarity) ? d.familiarity : undefined;
148
156
  }
@@ -197,6 +205,8 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
197
205
  }
198
206
  // A reason saves when the labeller leaves the box (the text equivalent of a tap).
199
207
  function setReason(id, qid, text) {
208
+ clearTimeout(state.whyTimers[id + "|" + qid]);
209
+ delete state.whyTimers[id + "|" + qid];
200
210
  if (!canWrite()) return;
201
211
  var why = Object.assign({}, reasonsOf(id));
202
212
  why[qid] = text;
@@ -302,6 +312,11 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
302
312
  card.appendChild(rich("p", "body", item.body));
303
313
  if (item.code) card.appendChild(codeBlock(item.code));
304
314
  if (item.context) card.appendChild(rich("p", "muted", item.context));
315
+ // One requirement of a line that holds several: which one, and the whole line it came from.
316
+ if (item.unit) {
317
+ card.appendChild(el("p", "muted", "From the line (requirement " + item.unit.part + " of " + item.unit.of + "):"));
318
+ card.appendChild(rich("p", "quote", item.unit.text));
319
+ }
305
320
  var a = answersOf(item.id);
306
321
  askOne(card, a, state.editing[item.id], function (q, v) {
307
322
  delete state.editing[item.id];
@@ -315,8 +330,16 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
315
330
  var draft = draftOf(item.id, q.id);
316
331
  box.value = draft !== undefined ? draft : reasonsOf(item.id)[q.id] || "";
317
332
  box.disabled = off;
318
- // Every keystroke is kept as a draft, so a render from the store never erases unsaved text.
319
- box.addEventListener("input", function () { keepDraft(item.id, q.id, box.value); });
333
+ // Every keystroke is kept as a draft, so a render from the store never erases unsaved text,
334
+ // and a pause in typing saves it: a reason that waits for the box to lose focus is lost when
335
+ // the labeller leaves the page from inside the box (measured: the store kept the drop alone).
336
+ box.addEventListener("input", function () {
337
+ keepDraft(item.id, q.id, box.value);
338
+ var k = item.id + "|" + q.id;
339
+ clearTimeout(state.whyTimers[k]);
340
+ state.whyTimers[k] = setTimeout(function () { setReason(item.id, q.id, box.value); }, WHY_PAUSE_MS);
341
+ });
342
+ state.whyBoxes[item.id + "|" + q.id] = box;
320
343
  box.addEventListener("change", function () { setReason(item.id, q.id, box.value); });
321
344
  card.appendChild(box);
322
345
  });
@@ -407,20 +430,56 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
407
430
  function groupBlock(g, off, onlyOpen) {
408
431
  var sec = el("section");
409
432
  sec.appendChild(el("h2", "", g.label));
410
- var fam = el("div", "ask");
411
- fam.appendChild(el("p", "q", DATA.familiarity_question.replace("{group}", g.label)));
412
- fam.appendChild(options(DATA.familiarity, familiarity(g.id), function (v) { setFamiliarity(g.id, v); }, off));
413
- var gs = state.groups[g.id];
414
- fam.appendChild(statusLine("group:" + g.id,
415
- familiarity(g.id) === undefined ? [{ text: "an answer" }] : [], gs && gs.savedAt));
416
- sec.appendChild(fam);
433
+ if (ASKS_FAMILIARITY) {
434
+ var fam = el("div", "ask");
435
+ fam.appendChild(el("p", "q", DATA.familiarity_question.replace("{group}", g.label)));
436
+ fam.appendChild(options(DATA.familiarity, familiarity(g.id), function (v) { setFamiliarity(g.id, v); }, off));
437
+ var gs = state.groups[g.id];
438
+ fam.appendChild(statusLine("group:" + g.id,
439
+ familiarity(g.id) === undefined ? [{ text: "an answer" }] : [], gs && gs.savedAt));
440
+ sec.appendChild(fam);
441
+ }
417
442
  groupItems(g.id).forEach(function (i) {
418
- if (onlyOpen && itemDone(i.id)) return;
443
+ if (onlyOpen && itemDone(i.id) && i.id !== state.typingIn) return;
419
444
  sec.appendChild(itemCard(i, off));
420
445
  });
421
446
  return sec;
422
447
  }
448
+ // Every reason typed and not yet saved goes to the store now: the page is being left.
449
+ function flushReasons() {
450
+ Object.keys(state.whyTimers).forEach(function (k) {
451
+ var at = k.indexOf("|"), id = k.slice(0, at), qid = k.slice(at + 1);
452
+ var draft = draftOf(id, qid);
453
+ if (draft === undefined) { clearTimeout(state.whyTimers[k]); delete state.whyTimers[k]; return; }
454
+ setReason(id, qid, draft);
455
+ });
456
+ }
457
+ // The page is rebuilt on every store snapshot; the reason box being typed in gets the keyboard back,
458
+ // with its caret where it was, or the rest of the sentence goes nowhere.
459
+ // Chrome fires "change" on a focused box while a render is removing it, and that change saves and
460
+ // renders: a render started inside a render would paint with no box focused and hide the item being
461
+ // typed in. It is skipped; the write it started renders again when the store confirms it.
462
+ var painting = false;
423
463
  function render() {
464
+ if (painting) return;
465
+ painting = true;
466
+ try { renderOnce(); } finally { painting = false; }
467
+ }
468
+ function renderOnce() {
469
+ var typing = null;
470
+ Object.keys(state.whyBoxes).forEach(function (k) {
471
+ var b = state.whyBoxes[k];
472
+ if (document.activeElement === b) typing = { key: k, start: b.selectionStart, end: b.selectionEnd };
473
+ });
474
+ state.whyBoxes = {};
475
+ // Under "Next unanswered" the save that completes an item would hide it, box and all, mid-sentence:
476
+ // the item being typed in stays on the page until the labeller leaves its box.
477
+ state.typingIn = typing ? typing.key.slice(0, typing.key.indexOf("|")) : null;
478
+ paint();
479
+ var again = typing && state.whyBoxes[typing.key];
480
+ if (again) { again.focus(); again.setSelectionRange(typing.start, typing.end); }
481
+ }
482
+ function paint() {
424
483
  var off = !canWrite();
425
484
  var done = DATA.items.filter(function (i) { return itemDone(i.id); }).length;
426
485
  var total = DATA.items.length;
@@ -482,7 +541,8 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
482
541
  progress.appendChild(note);
483
542
  }
484
543
  DATA.groups.forEach(function (g) {
485
- if (onlyOpen && groupDone(g.id)) return;
544
+ var typingHere = groupItems(g.id).some(function (i) { return i.id === state.typingIn; });
545
+ if (onlyOpen && groupDone(g.id) && !typingHere) return;
486
546
  app.appendChild(groupBlock(g, off, onlyOpen));
487
547
  });
488
548
  }
@@ -499,6 +559,10 @@ pre { overflow-x: auto; background: var(--surface); border: 1px solid var(--line
499
559
  }
500
560
  function broken() { state.store = "none"; render(); }
501
561
 
562
+ document.addEventListener("visibilitychange", function () {
563
+ if (document.visibilityState !== "visible") flushReasons();
564
+ });
565
+ window.addEventListener("pagehide", flushReasons);
502
566
  render();
503
567
  if (!window.claude || typeof window.claude.use !== "function") { broken(); return; }
504
568
  window.claude.use("db").then(function (store) {