@sametalmdr/word-editor 0.2.0-alpha → 0.3.0-alpha

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +108 -70
  2. package/LICENSE +21 -21
  3. package/README.md +286 -247
  4. package/dist/canvas/box-clipboard.js +238 -238
  5. package/dist/canvas/box-selection.js +80 -80
  6. package/dist/canvas/canvas-editor.js +502 -502
  7. package/dist/canvas/controller.js +717 -717
  8. package/dist/canvas/geometry.js +131 -131
  9. package/dist/canvas/gesture.d.ts +0 -1
  10. package/dist/canvas/gesture.js +289 -289
  11. package/dist/canvas/handles.js +212 -212
  12. package/dist/canvas/menu.js +82 -82
  13. package/dist/canvas/mode-controller.js +81 -81
  14. package/dist/canvas/nudge.js +108 -108
  15. package/dist/canvas/overlay.js +149 -149
  16. package/dist/canvas/snap.js +132 -132
  17. package/dist/canvas/viewport.js +112 -112
  18. package/dist/canvas/z-order.js +171 -171
  19. package/dist/commands/alignment.js +150 -150
  20. package/dist/commands/box.js +272 -272
  21. package/dist/commands/clipboard.js +93 -93
  22. package/dist/commands/graphemes.js +112 -112
  23. package/dist/commands/marks.js +153 -153
  24. package/dist/commands/paragraph-props.js +176 -176
  25. package/dist/commands/size.js +368 -368
  26. package/dist/commands/text.js +291 -291
  27. package/dist/docx.d.ts +1 -0
  28. package/dist/docx.js +15 -13
  29. package/dist/editor/editing-host.js +434 -434
  30. package/dist/element.js +41 -41
  31. package/dist/fonts/OFL-Carlito.txt +94 -0
  32. package/dist/fonts/calibri-metrics.d.ts +27 -0
  33. package/dist/fonts/calibri-metrics.js +88 -0
  34. package/dist/history/history.js +433 -433
  35. package/dist/index.js +63 -63
  36. package/dist/input/beforeinput.js +224 -224
  37. package/dist/input/clipboard.js +107 -107
  38. package/dist/input/composition.js +341 -341
  39. package/dist/input/keymap.js +54 -54
  40. package/dist/io/ooxml/custgeom.js +72 -72
  41. package/dist/io/ooxml/drawingml.js +232 -232
  42. package/dist/io/ooxml/edits.js +67 -67
  43. package/dist/io/ooxml/export-splice.js +880 -880
  44. package/dist/io/ooxml/export.js +365 -365
  45. package/dist/io/ooxml/fill.d.ts +93 -8
  46. package/dist/io/ooxml/fill.js +618 -562
  47. package/dist/io/ooxml/fit.d.ts +252 -0
  48. package/dist/io/ooxml/fit.js +598 -0
  49. package/dist/io/ooxml/import-gate.js +200 -200
  50. package/dist/io/ooxml/import.js +691 -691
  51. package/dist/io/ooxml/inflate.js +126 -126
  52. package/dist/io/ooxml/media.js +87 -87
  53. package/dist/io/ooxml/ns.d.ts +2 -0
  54. package/dist/io/ooxml/ns.js +26 -21
  55. package/dist/io/ooxml/numbering.js +290 -290
  56. package/dist/io/ooxml/opc-write.js +171 -171
  57. package/dist/io/ooxml/provenance.js +93 -93
  58. package/dist/io/ooxml/recipe.js +708 -708
  59. package/dist/io/ooxml/relationships.js +59 -59
  60. package/dist/io/ooxml/run-fonts.d.ts +89 -0
  61. package/dist/io/ooxml/run-fonts.js +378 -0
  62. package/dist/io/ooxml/slots.d.ts +4 -0
  63. package/dist/io/ooxml/slots.js +582 -581
  64. package/dist/io/ooxml/styles-resolve.js +257 -257
  65. package/dist/io/ooxml/units.js +43 -43
  66. package/dist/io/ooxml/value.js +120 -120
  67. package/dist/io/ooxml/xml-scan.js +616 -616
  68. package/dist/io/ooxml/zip-container.js +228 -228
  69. package/dist/io/ooxml/zip-reject.js +106 -106
  70. package/dist/measure/placement.js +83 -83
  71. package/dist/measure/report.js +75 -75
  72. package/dist/model/ids.js +70 -70
  73. package/dist/model/normalize.js +146 -146
  74. package/dist/model/position.js +283 -283
  75. package/dist/model/transaction.js +209 -209
  76. package/dist/platform/capabilities.js +25 -25
  77. package/dist/react/index.js +21 -21
  78. package/dist/selection/controller.js +315 -315
  79. package/dist/selection/dom-map.js +334 -334
  80. package/dist/state/editor-state.js +109 -109
  81. package/dist/styles.css +66 -66
  82. package/package.json +94 -93
@@ -1,106 +1,106 @@
1
- // The five zip-format features this reader refuses to guess about (§3.5,
2
- // ADR-0020 Karar 4). Each gets its OWN check and its OWN message — a single
3
- // "corrupt file" error would not tell a caller WHICH unsupported feature it
4
- // hit, and CLAUDE.md's own rule against guessing OOXML/format semantics
5
- // applies exactly here: ZIP64, an encrypted entry, a data-descriptor-only
6
- // entry, a multi-disk archive, or an unknown compression method could each
7
- // be handled in more than one plausible way, and a wrong guess corrupts a
8
- // document silently. Ported from scripts/opc-bytes.py's read_package() —
9
- // same five checks, same reasoning, not reinvented.
10
- //
11
- // Plain Error with a descriptive message, not a custom class — this matches
12
- // every other src/ file's error convention (grep confirms zero custom Error
13
- // subclasses anywhere in src/) and Python's own ImportRejected/OpcError,
14
- // which are also single classes distinguished by message, not a hierarchy.
15
-
16
- const ZIP64_SENTINEL = 0xffffffff;
17
- const FLAG_ENCRYPTED = 0x0001;
18
- const FLAG_DATA_DESCRIPTOR = 0x0008;
19
- const METHOD_STORE = 0;
20
- const METHOD_DEFLATE = 8;
21
-
22
- /**
23
- * A zip comment can follow the EOCD record, so the EOCD locator search
24
- * window is bounded but not zero-length. The ZIP64 EOCD locator (if present)
25
- * sits immediately before the ZIP64 EOCD record, itself immediately before
26
- * the classic EOCD — so "does a ZIP64 locator signature appear anywhere
27
- * before the classic EOCD we found" is the same check opc-bytes.py makes
28
- * (`data.rfind(ZIP64_EOCD_LOCATOR_SIG, 0, eocd_off) >= 0`).
29
- * @param {Uint8Array} bytes
30
- * @param {number} eocdOffset
31
- * @returns {void}
32
- */
33
- export function rejectZip64Locator(bytes, eocdOffset) {
34
- const sig = [0x50, 0x4b, 0x06, 0x07]; // "PK\x06\x07"
35
- for (let i = eocdOffset - 4; i >= 0; i--) {
36
- if (bytes[i] === sig[0] && bytes[i + 1] === sig[1] && bytes[i + 2] === sig[2] && bytes[i + 3] === sig[3]) {
37
- throw new Error(
38
- "zip: ZIP64 end-of-central-directory locator found — refusing (this reader handles classic zip only)"
39
- );
40
- }
41
- }
42
- }
43
-
44
- /**
45
- * @param {{diskWithCd: number, thisDisk: number, cdOffset: number, cdSize: number}} eocdFields
46
- * @returns {void}
47
- */
48
- export function rejectZip64EocdSentinel(eocdFields) {
49
- if (eocdFields.cdOffset === ZIP64_SENTINEL || eocdFields.cdSize === ZIP64_SENTINEL) {
50
- throw new Error("zip: ZIP64 sentinel (0xFFFFFFFF) in the end-of-central-directory record — refusing");
51
- }
52
- if (eocdFields.diskWithCd !== 0 || eocdFields.thisDisk !== 0) {
53
- throw new Error(
54
- `zip: multi-disk archive (thisDisk=${eocdFields.thisDisk}, diskWithCd=${eocdFields.diskWithCd}) — refusing`
55
- );
56
- }
57
- }
58
-
59
- /**
60
- * @param {string} name
61
- * @param {number} csize
62
- * @param {number} usize
63
- * @param {number} localOffset
64
- * @returns {void}
65
- */
66
- export function rejectEntryZip64Sentinel(name, csize, usize, localOffset) {
67
- if (csize === ZIP64_SENTINEL || usize === ZIP64_SENTINEL || localOffset === ZIP64_SENTINEL) {
68
- throw new Error(`zip: ${JSON.stringify(name)} has a ZIP64 sentinel (0xFFFFFFFF) in its central directory entry — refusing`);
69
- }
70
- }
71
-
72
- /**
73
- * @param {string} name
74
- * @param {number} flags the central directory entry's general-purpose bit flag
75
- * @returns {void}
76
- */
77
- export function rejectEntryEncrypted(name, flags) {
78
- if ((flags & FLAG_ENCRYPTED) !== 0) {
79
- throw new Error(`zip: ${JSON.stringify(name)} is encrypted — refusing`);
80
- }
81
- }
82
-
83
- /**
84
- * @param {string} name
85
- * @param {number} flags
86
- * @returns {void}
87
- */
88
- export function rejectEntryDataDescriptor(name, flags) {
89
- if ((flags & FLAG_DATA_DESCRIPTOR) !== 0) {
90
- throw new Error(
91
- `zip: ${JSON.stringify(name)} uses a data descriptor (general-purpose flag bit 3) — its sizes are ` +
92
- "not in the local header, refusing rather than guessing"
93
- );
94
- }
95
- }
96
-
97
- /**
98
- * @param {string} name
99
- * @param {number} method
100
- * @returns {void}
101
- */
102
- export function rejectEntryUnknownMethod(name, method) {
103
- if (method !== METHOD_STORE && method !== METHOD_DEFLATE) {
104
- throw new Error(`zip: ${JSON.stringify(name)} uses compression method ${method} — refusing (store/deflate only)`);
105
- }
106
- }
1
+ // The five zip-format features this reader refuses to guess about (§3.5,
2
+ // ADR-0020 Karar 4). Each gets its OWN check and its OWN message — a single
3
+ // "corrupt file" error would not tell a caller WHICH unsupported feature it
4
+ // hit, and CLAUDE.md's own rule against guessing OOXML/format semantics
5
+ // applies exactly here: ZIP64, an encrypted entry, a data-descriptor-only
6
+ // entry, a multi-disk archive, or an unknown compression method could each
7
+ // be handled in more than one plausible way, and a wrong guess corrupts a
8
+ // document silently. Ported from scripts/opc-bytes.py's read_package() —
9
+ // same five checks, same reasoning, not reinvented.
10
+ //
11
+ // Plain Error with a descriptive message, not a custom class — this matches
12
+ // every other src/ file's error convention (grep confirms zero custom Error
13
+ // subclasses anywhere in src/) and Python's own ImportRejected/OpcError,
14
+ // which are also single classes distinguished by message, not a hierarchy.
15
+
16
+ const ZIP64_SENTINEL = 0xffffffff;
17
+ const FLAG_ENCRYPTED = 0x0001;
18
+ const FLAG_DATA_DESCRIPTOR = 0x0008;
19
+ const METHOD_STORE = 0;
20
+ const METHOD_DEFLATE = 8;
21
+
22
+ /**
23
+ * A zip comment can follow the EOCD record, so the EOCD locator search
24
+ * window is bounded but not zero-length. The ZIP64 EOCD locator (if present)
25
+ * sits immediately before the ZIP64 EOCD record, itself immediately before
26
+ * the classic EOCD — so "does a ZIP64 locator signature appear anywhere
27
+ * before the classic EOCD we found" is the same check opc-bytes.py makes
28
+ * (`data.rfind(ZIP64_EOCD_LOCATOR_SIG, 0, eocd_off) >= 0`).
29
+ * @param {Uint8Array} bytes
30
+ * @param {number} eocdOffset
31
+ * @returns {void}
32
+ */
33
+ export function rejectZip64Locator(bytes, eocdOffset) {
34
+ const sig = [0x50, 0x4b, 0x06, 0x07]; // "PK\x06\x07"
35
+ for (let i = eocdOffset - 4; i >= 0; i--) {
36
+ if (bytes[i] === sig[0] && bytes[i + 1] === sig[1] && bytes[i + 2] === sig[2] && bytes[i + 3] === sig[3]) {
37
+ throw new Error(
38
+ "zip: ZIP64 end-of-central-directory locator found — refusing (this reader handles classic zip only)"
39
+ );
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * @param {{diskWithCd: number, thisDisk: number, cdOffset: number, cdSize: number}} eocdFields
46
+ * @returns {void}
47
+ */
48
+ export function rejectZip64EocdSentinel(eocdFields) {
49
+ if (eocdFields.cdOffset === ZIP64_SENTINEL || eocdFields.cdSize === ZIP64_SENTINEL) {
50
+ throw new Error("zip: ZIP64 sentinel (0xFFFFFFFF) in the end-of-central-directory record — refusing");
51
+ }
52
+ if (eocdFields.diskWithCd !== 0 || eocdFields.thisDisk !== 0) {
53
+ throw new Error(
54
+ `zip: multi-disk archive (thisDisk=${eocdFields.thisDisk}, diskWithCd=${eocdFields.diskWithCd}) — refusing`
55
+ );
56
+ }
57
+ }
58
+
59
+ /**
60
+ * @param {string} name
61
+ * @param {number} csize
62
+ * @param {number} usize
63
+ * @param {number} localOffset
64
+ * @returns {void}
65
+ */
66
+ export function rejectEntryZip64Sentinel(name, csize, usize, localOffset) {
67
+ if (csize === ZIP64_SENTINEL || usize === ZIP64_SENTINEL || localOffset === ZIP64_SENTINEL) {
68
+ throw new Error(`zip: ${JSON.stringify(name)} has a ZIP64 sentinel (0xFFFFFFFF) in its central directory entry — refusing`);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * @param {string} name
74
+ * @param {number} flags the central directory entry's general-purpose bit flag
75
+ * @returns {void}
76
+ */
77
+ export function rejectEntryEncrypted(name, flags) {
78
+ if ((flags & FLAG_ENCRYPTED) !== 0) {
79
+ throw new Error(`zip: ${JSON.stringify(name)} is encrypted — refusing`);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * @param {string} name
85
+ * @param {number} flags
86
+ * @returns {void}
87
+ */
88
+ export function rejectEntryDataDescriptor(name, flags) {
89
+ if ((flags & FLAG_DATA_DESCRIPTOR) !== 0) {
90
+ throw new Error(
91
+ `zip: ${JSON.stringify(name)} uses a data descriptor (general-purpose flag bit 3) — its sizes are ` +
92
+ "not in the local header, refusing rather than guessing"
93
+ );
94
+ }
95
+ }
96
+
97
+ /**
98
+ * @param {string} name
99
+ * @param {number} method
100
+ * @returns {void}
101
+ */
102
+ export function rejectEntryUnknownMethod(name, method) {
103
+ if (method !== METHOD_STORE && method !== METHOD_DEFLATE) {
104
+ throw new Error(`zip: ${JSON.stringify(name)} uses compression method ${method} — refusing (store/deflate only)`);
105
+ }
106
+ }
@@ -1,83 +1,83 @@
1
- // A `PageMeasureReport`'s ACTIVE entry (ADR-0019 Karar 7), converted to
2
- // client-px placements a `WarningLayer` can paint — the measure-layer twin
3
- // of `overlay.js`'s `handlePlacements`/`guidePlacements`. Pure: page-pt
4
- // geometry in, client-px placements out. No src/, DOM, or history import.
5
- //
6
- // Only the ACTIVE box ever produces placements (§7's own two-tier design):
7
- // the passive tier is a status-line tally, never painted on the canvas.
8
- // `within-noise`/`fits` with no bleed and no out-of-page produce `null` too
9
- // — even the active box gets nothing to show when there is nothing to warn
10
- // about.
11
-
12
- import { pageToClientRect } from "../canvas/viewport.js";
13
-
14
- /**
15
- * @typedef {object} BadgePlacement
16
- * @property {number} left client px, anchored at the box's own top-right corner
17
- * @property {number} top client px
18
- * @property {string} text
19
- */
20
-
21
- /**
22
- * @typedef {object} WarningPlacements
23
- * @property {{left: number, top: number, width: number, height: number}} outline the active box's own declared rect, client px
24
- * @property {BadgePlacement | null} overflowBadge
25
- * @property {BadgePlacement | null} outOfPageBadge
26
- * @property {{left: number, top: number, width: number, height: number}[]} bleedOutlines one per neighbor the active box's content reaches into
27
- */
28
-
29
- /**
30
- * @param {import("./extent.js").OverflowVerdict} overflow
31
- * @returns {string}
32
- */
33
- function overflowLabel(overflow) {
34
- const maxBy = Math.max(overflow.topBy, overflow.bottomBy, overflow.leftBy, overflow.rightBy);
35
- return `+${Math.round(maxBy)}pt`;
36
- }
37
-
38
- /**
39
- * @param {import("./page-bounds.js").OutOfPageVerdict} outOfPage
40
- * @returns {string}
41
- */
42
- function outOfPageLabel(outOfPage) {
43
- const maxBy = Math.max(outOfPage.leftBy, outOfPage.topBy, outOfPage.rightBy, outOfPage.bottomBy);
44
- return `${Math.round(maxBy)}pt off-page`;
45
- }
46
-
47
- /**
48
- * @param {{left: number, top: number, width: number, height: number}} outline
49
- * @param {string} text
50
- * @returns {BadgePlacement}
51
- */
52
- function badgeAt(outline, text) {
53
- return { left: outline.left + outline.width, top: outline.top, text };
54
- }
55
-
56
- /**
57
- * @param {import("./report.js").PageMeasureReport} report
58
- * @param {import("../canvas/viewport.js").PageRect} pageRect
59
- * @returns {WarningPlacements | null}
60
- */
61
- export function warningPlacements(report, pageRect) {
62
- const active = report.active;
63
- if (active === null) {
64
- return null;
65
- }
66
-
67
- const hasOverflow = active.overflow.kind === "overflow";
68
- const hasBleed = active.bleeds.length > 0;
69
- const hasOutOfPage = active.outOfPage !== null;
70
- if (!hasOverflow && !hasBleed && !hasOutOfPage) {
71
- return null; // fits/within-noise, on-page, no bleed — the active box has nothing to show either
72
- }
73
-
74
- const outline = pageToClientRect(pageRect, active.declaredRect);
75
- return {
76
- outline,
77
- overflowBadge: hasOverflow ? badgeAt(outline, overflowLabel(active.overflow)) : null,
78
- outOfPageBadge: hasOutOfPage
79
- ? badgeAt(outline, outOfPageLabel(/** @type {import("./page-bounds.js").OutOfPageVerdict} */ (active.outOfPage)))
80
- : null,
81
- bleedOutlines: active.bleeds.map((hit) => pageToClientRect(pageRect, hit.rect)),
82
- };
83
- }
1
+ // A `PageMeasureReport`'s ACTIVE entry (ADR-0019 Karar 7), converted to
2
+ // client-px placements a `WarningLayer` can paint — the measure-layer twin
3
+ // of `overlay.js`'s `handlePlacements`/`guidePlacements`. Pure: page-pt
4
+ // geometry in, client-px placements out. No src/, DOM, or history import.
5
+ //
6
+ // Only the ACTIVE box ever produces placements (§7's own two-tier design):
7
+ // the passive tier is a status-line tally, never painted on the canvas.
8
+ // `within-noise`/`fits` with no bleed and no out-of-page produce `null` too
9
+ // — even the active box gets nothing to show when there is nothing to warn
10
+ // about.
11
+
12
+ import { pageToClientRect } from "../canvas/viewport.js";
13
+
14
+ /**
15
+ * @typedef {object} BadgePlacement
16
+ * @property {number} left client px, anchored at the box's own top-right corner
17
+ * @property {number} top client px
18
+ * @property {string} text
19
+ */
20
+
21
+ /**
22
+ * @typedef {object} WarningPlacements
23
+ * @property {{left: number, top: number, width: number, height: number}} outline the active box's own declared rect, client px
24
+ * @property {BadgePlacement | null} overflowBadge
25
+ * @property {BadgePlacement | null} outOfPageBadge
26
+ * @property {{left: number, top: number, width: number, height: number}[]} bleedOutlines one per neighbor the active box's content reaches into
27
+ */
28
+
29
+ /**
30
+ * @param {import("./extent.js").OverflowVerdict} overflow
31
+ * @returns {string}
32
+ */
33
+ function overflowLabel(overflow) {
34
+ const maxBy = Math.max(overflow.topBy, overflow.bottomBy, overflow.leftBy, overflow.rightBy);
35
+ return `+${Math.round(maxBy)}pt`;
36
+ }
37
+
38
+ /**
39
+ * @param {import("./page-bounds.js").OutOfPageVerdict} outOfPage
40
+ * @returns {string}
41
+ */
42
+ function outOfPageLabel(outOfPage) {
43
+ const maxBy = Math.max(outOfPage.leftBy, outOfPage.topBy, outOfPage.rightBy, outOfPage.bottomBy);
44
+ return `${Math.round(maxBy)}pt off-page`;
45
+ }
46
+
47
+ /**
48
+ * @param {{left: number, top: number, width: number, height: number}} outline
49
+ * @param {string} text
50
+ * @returns {BadgePlacement}
51
+ */
52
+ function badgeAt(outline, text) {
53
+ return { left: outline.left + outline.width, top: outline.top, text };
54
+ }
55
+
56
+ /**
57
+ * @param {import("./report.js").PageMeasureReport} report
58
+ * @param {import("../canvas/viewport.js").PageRect} pageRect
59
+ * @returns {WarningPlacements | null}
60
+ */
61
+ export function warningPlacements(report, pageRect) {
62
+ const active = report.active;
63
+ if (active === null) {
64
+ return null;
65
+ }
66
+
67
+ const hasOverflow = active.overflow.kind === "overflow";
68
+ const hasBleed = active.bleeds.length > 0;
69
+ const hasOutOfPage = active.outOfPage !== null;
70
+ if (!hasOverflow && !hasBleed && !hasOutOfPage) {
71
+ return null; // fits/within-noise, on-page, no bleed — the active box has nothing to show either
72
+ }
73
+
74
+ const outline = pageToClientRect(pageRect, active.declaredRect);
75
+ return {
76
+ outline,
77
+ overflowBadge: hasOverflow ? badgeAt(outline, overflowLabel(active.overflow)) : null,
78
+ outOfPageBadge: hasOutOfPage
79
+ ? badgeAt(outline, outOfPageLabel(/** @type {import("./page-bounds.js").OutOfPageVerdict} */ (active.outOfPage)))
80
+ : null,
81
+ bleedOutlines: active.bleeds.map((hit) => pageToClientRect(pageRect, hit.rect)),
82
+ };
83
+ }
@@ -1,75 +1,75 @@
1
- // One page's worth of already-classified box results, reorganized into the
2
- // two tiers ADR-0019 Karar 7 decided on: an ACTIVE box (0 or 1, whichever
3
- // was just touched) and a PASSIVE tally of everything else. No src/, DOM, or
4
- // history import — this file makes no rendering decision, it only shapes
5
- // data a later warning layer reads.
6
- //
7
- // 🔴 TAŞMA (overflow/bleed) and SAYFA DIŞI (out-of-page) are counted
8
- // SEPARATELY, never folded into one number — ADR-0019 Karar 4: one is a
9
- // content problem ("bu kutuya fazla yazdın"), the other a placement problem
10
- // ("bu kutu sayfadan çıktı"), and a single combined count would not tell a
11
- // user which one to go fix.
12
- //
13
- // The active tier is 0..1 by CONSTRUCTION here, not by a rule this file
14
- // enforces: `activeId` names at most one entry, so `entries.find` can return
15
- // at most one result. A future multi-select active tier would be a
16
- // different function, not a relaxed version of this one.
17
-
18
- import { requireMeasureMetrics } from "./extent.js";
19
-
20
- /**
21
- * @typedef {object} BoxMeasureEntry
22
- * @property {import("../model/node.js").NodeId} id
23
- * @property {import("../canvas/geometry.js").Rect} declaredRect the box's own declared rect — placement.js positions a badge/outline from this, not by looking the box up a second time
24
- * @property {import("./extent.js").OverflowVerdict} overflow
25
- * @property {import("../canvas/geometry.js").Rect | null} [overflowRect] from extent.js's own `overflowRect` — placement.js draws the excess region from this when present
26
- * @property {import("./bleed.js").BleedHit[]} bleeds
27
- * @property {import("./page-bounds.js").OutOfPageVerdict | null} outOfPage
28
- * @property {boolean} [stale] true when this entry is the last DOM-measured value shown during an open gesture, not a fresh read (ADR-0019 §2.2 rule 2) — a display concern only, this file's own bucketing logic never reads it
29
- */
30
-
31
- /**
32
- * @typedef {object} PageMeasureReport
33
- * @property {BoxMeasureEntry | null} active
34
- * @property {object} passive
35
- * @property {number} passive.overflowing boxes whose OWN overflow.kind is "overflow" (never the active box)
36
- * @property {number} passive.bleeding boxes whose OWN `bleeds` is non-empty — a SOURCE count, not a target count: one box bleeding into six neighbors is +1 here, not +6. Never the active box, and (as of the caller's own gating — see MEAS13) never a within-noise source either, matching Karar 4's "within-noise never warns" for the active tier
37
- * @property {number} passive.withinNoise boxes whose OWN overflow.kind is "within-noise" (never the active box)
38
- * @property {number} passive.outOfPage boxes with a non-null outOfPage verdict (never the active box)
39
- */
40
-
41
- /**
42
- * @param {BoxMeasureEntry[]} entries every measured box on the page
43
- * @param {import("../model/node.js").NodeId | null} activeId the box just touched, or null if none
44
- * @param {import("./extent.js").MeasureMetrics} metrics
45
- * @returns {PageMeasureReport}
46
- */
47
- export function pageReport(entries, activeId, metrics) {
48
- requireMeasureMetrics(metrics);
49
-
50
- const active = entries.find((entry) => entry.id === activeId) ?? null;
51
-
52
- let overflowing = 0;
53
- let bleeding = 0;
54
- let withinNoise = 0;
55
- let outOfPage = 0;
56
-
57
- for (const entry of entries) {
58
- if (entry.id === activeId) {
59
- continue; // passive is "diğer her şey" (ADR-0019 Karar 7) — the active box is never double-counted
60
- }
61
- if (entry.overflow.kind === "overflow") {
62
- overflowing += 1;
63
- } else if (entry.overflow.kind === "within-noise") {
64
- withinNoise += 1;
65
- }
66
- if (entry.bleeds.length > 0) {
67
- bleeding += 1; // ONE increment per SOURCE box, regardless of how many neighbors its own bleeds[] names
68
- }
69
- if (entry.outOfPage !== null) {
70
- outOfPage += 1;
71
- }
72
- }
73
-
74
- return { active, passive: { overflowing, bleeding, withinNoise, outOfPage } };
75
- }
1
+ // One page's worth of already-classified box results, reorganized into the
2
+ // two tiers ADR-0019 Karar 7 decided on: an ACTIVE box (0 or 1, whichever
3
+ // was just touched) and a PASSIVE tally of everything else. No src/, DOM, or
4
+ // history import — this file makes no rendering decision, it only shapes
5
+ // data a later warning layer reads.
6
+ //
7
+ // 🔴 TAŞMA (overflow/bleed) and SAYFA DIŞI (out-of-page) are counted
8
+ // SEPARATELY, never folded into one number — ADR-0019 Karar 4: one is a
9
+ // content problem ("bu kutuya fazla yazdın"), the other a placement problem
10
+ // ("bu kutu sayfadan çıktı"), and a single combined count would not tell a
11
+ // user which one to go fix.
12
+ //
13
+ // The active tier is 0..1 by CONSTRUCTION here, not by a rule this file
14
+ // enforces: `activeId` names at most one entry, so `entries.find` can return
15
+ // at most one result. A future multi-select active tier would be a
16
+ // different function, not a relaxed version of this one.
17
+
18
+ import { requireMeasureMetrics } from "./extent.js";
19
+
20
+ /**
21
+ * @typedef {object} BoxMeasureEntry
22
+ * @property {import("../model/node.js").NodeId} id
23
+ * @property {import("../canvas/geometry.js").Rect} declaredRect the box's own declared rect — placement.js positions a badge/outline from this, not by looking the box up a second time
24
+ * @property {import("./extent.js").OverflowVerdict} overflow
25
+ * @property {import("../canvas/geometry.js").Rect | null} [overflowRect] from extent.js's own `overflowRect` — placement.js draws the excess region from this when present
26
+ * @property {import("./bleed.js").BleedHit[]} bleeds
27
+ * @property {import("./page-bounds.js").OutOfPageVerdict | null} outOfPage
28
+ * @property {boolean} [stale] true when this entry is the last DOM-measured value shown during an open gesture, not a fresh read (ADR-0019 §2.2 rule 2) — a display concern only, this file's own bucketing logic never reads it
29
+ */
30
+
31
+ /**
32
+ * @typedef {object} PageMeasureReport
33
+ * @property {BoxMeasureEntry | null} active
34
+ * @property {object} passive
35
+ * @property {number} passive.overflowing boxes whose OWN overflow.kind is "overflow" (never the active box)
36
+ * @property {number} passive.bleeding boxes whose OWN `bleeds` is non-empty — a SOURCE count, not a target count: one box bleeding into six neighbors is +1 here, not +6. Never the active box, and (as of the caller's own gating — see MEAS13) never a within-noise source either, matching Karar 4's "within-noise never warns" for the active tier
37
+ * @property {number} passive.withinNoise boxes whose OWN overflow.kind is "within-noise" (never the active box)
38
+ * @property {number} passive.outOfPage boxes with a non-null outOfPage verdict (never the active box)
39
+ */
40
+
41
+ /**
42
+ * @param {BoxMeasureEntry[]} entries every measured box on the page
43
+ * @param {import("../model/node.js").NodeId | null} activeId the box just touched, or null if none
44
+ * @param {import("./extent.js").MeasureMetrics} metrics
45
+ * @returns {PageMeasureReport}
46
+ */
47
+ export function pageReport(entries, activeId, metrics) {
48
+ requireMeasureMetrics(metrics);
49
+
50
+ const active = entries.find((entry) => entry.id === activeId) ?? null;
51
+
52
+ let overflowing = 0;
53
+ let bleeding = 0;
54
+ let withinNoise = 0;
55
+ let outOfPage = 0;
56
+
57
+ for (const entry of entries) {
58
+ if (entry.id === activeId) {
59
+ continue; // passive is "diğer her şey" (ADR-0019 Karar 7) — the active box is never double-counted
60
+ }
61
+ if (entry.overflow.kind === "overflow") {
62
+ overflowing += 1;
63
+ } else if (entry.overflow.kind === "within-noise") {
64
+ withinNoise += 1;
65
+ }
66
+ if (entry.bleeds.length > 0) {
67
+ bleeding += 1; // ONE increment per SOURCE box, regardless of how many neighbors its own bleeds[] names
68
+ }
69
+ if (entry.outOfPage !== null) {
70
+ outOfPage += 1;
71
+ }
72
+ }
73
+
74
+ return { active, passive: { overflowing, bleeding, withinNoise, outOfPage } };
75
+ }