@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.
- package/CHANGELOG.md +108 -70
- package/LICENSE +21 -21
- package/README.md +286 -247
- package/dist/canvas/box-clipboard.js +238 -238
- package/dist/canvas/box-selection.js +80 -80
- package/dist/canvas/canvas-editor.js +502 -502
- package/dist/canvas/controller.js +717 -717
- package/dist/canvas/geometry.js +131 -131
- package/dist/canvas/gesture.d.ts +0 -1
- package/dist/canvas/gesture.js +289 -289
- package/dist/canvas/handles.js +212 -212
- package/dist/canvas/menu.js +82 -82
- package/dist/canvas/mode-controller.js +81 -81
- package/dist/canvas/nudge.js +108 -108
- package/dist/canvas/overlay.js +149 -149
- package/dist/canvas/snap.js +132 -132
- package/dist/canvas/viewport.js +112 -112
- package/dist/canvas/z-order.js +171 -171
- package/dist/commands/alignment.js +150 -150
- package/dist/commands/box.js +272 -272
- package/dist/commands/clipboard.js +93 -93
- package/dist/commands/graphemes.js +112 -112
- package/dist/commands/marks.js +153 -153
- package/dist/commands/paragraph-props.js +176 -176
- package/dist/commands/size.js +368 -368
- package/dist/commands/text.js +291 -291
- package/dist/docx.d.ts +1 -0
- package/dist/docx.js +15 -13
- package/dist/editor/editing-host.js +434 -434
- package/dist/element.js +41 -41
- package/dist/fonts/OFL-Carlito.txt +94 -0
- package/dist/fonts/calibri-metrics.d.ts +27 -0
- package/dist/fonts/calibri-metrics.js +88 -0
- package/dist/history/history.js +433 -433
- package/dist/index.js +63 -63
- package/dist/input/beforeinput.js +224 -224
- package/dist/input/clipboard.js +107 -107
- package/dist/input/composition.js +341 -341
- package/dist/input/keymap.js +54 -54
- package/dist/io/ooxml/custgeom.js +72 -72
- package/dist/io/ooxml/drawingml.js +232 -232
- package/dist/io/ooxml/edits.js +67 -67
- package/dist/io/ooxml/export-splice.js +880 -880
- package/dist/io/ooxml/export.js +365 -365
- package/dist/io/ooxml/fill.d.ts +93 -8
- package/dist/io/ooxml/fill.js +618 -562
- package/dist/io/ooxml/fit.d.ts +252 -0
- package/dist/io/ooxml/fit.js +598 -0
- package/dist/io/ooxml/import-gate.js +200 -200
- package/dist/io/ooxml/import.js +691 -691
- package/dist/io/ooxml/inflate.js +126 -126
- package/dist/io/ooxml/media.js +87 -87
- package/dist/io/ooxml/ns.d.ts +2 -0
- package/dist/io/ooxml/ns.js +26 -21
- package/dist/io/ooxml/numbering.js +290 -290
- package/dist/io/ooxml/opc-write.js +171 -171
- package/dist/io/ooxml/provenance.js +93 -93
- package/dist/io/ooxml/recipe.js +708 -708
- package/dist/io/ooxml/relationships.js +59 -59
- package/dist/io/ooxml/run-fonts.d.ts +89 -0
- package/dist/io/ooxml/run-fonts.js +378 -0
- package/dist/io/ooxml/slots.d.ts +4 -0
- package/dist/io/ooxml/slots.js +582 -581
- package/dist/io/ooxml/styles-resolve.js +257 -257
- package/dist/io/ooxml/units.js +43 -43
- package/dist/io/ooxml/value.js +120 -120
- package/dist/io/ooxml/xml-scan.js +616 -616
- package/dist/io/ooxml/zip-container.js +228 -228
- package/dist/io/ooxml/zip-reject.js +106 -106
- package/dist/measure/placement.js +83 -83
- package/dist/measure/report.js +75 -75
- package/dist/model/ids.js +70 -70
- package/dist/model/normalize.js +146 -146
- package/dist/model/position.js +283 -283
- package/dist/model/transaction.js +209 -209
- package/dist/platform/capabilities.js +25 -25
- package/dist/react/index.js +21 -21
- package/dist/selection/controller.js +315 -315
- package/dist/selection/dom-map.js +334 -334
- package/dist/state/editor-state.js +109 -109
- package/dist/styles.css +66 -66
- 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
|
+
}
|
package/dist/measure/report.js
CHANGED
|
@@ -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
|
+
}
|