@stll/folio-core 0.16.0 → 0.17.1
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/controller/fontReadiness.js +2 -0
- package/dist/docx/server/extractDocxText.d.ts +21 -2
- package/dist/docx/server/extractDocxText.js +130 -51
- package/dist/docx/xmlParser.d.ts +14 -1
- package/dist/docx/xmlParser.js +50 -3
- package/dist/layout-bridge/convert/toFlowBlocks.js +7 -0
- package/dist/layout-engine/measure/cache.js +1 -1
- package/dist/layout-engine/measure/measureContainer.js +25 -7
- package/dist/layout-engine/measure/measureHelpers.js +1 -0
- package/dist/layout-engine/measure/measureTypes.d.ts +5 -0
- package/dist/layout-engine/types.d.ts +9 -0
- package/dist/layout-painter/cursiveJoiners.d.ts +80 -0
- package/dist/layout-painter/cursiveJoiners.js +199 -0
- package/dist/layout-painter/renderPage.js +1 -0
- package/dist/layout-painter/renderParagraph.js +54 -11
- package/dist/server.d.ts +2 -2
- package/dist/utils/cursiveJoining.d.ts +49 -0
- package/dist/utils/cursiveJoining.js +97 -0
- package/dist/utils/joiningTypes.gen.d.ts +26 -0
- package/dist/utils/joiningTypes.gen.js +1136 -0
- package/dist/utils/scriptSegments.d.ts +36 -5
- package/dist/utils/scriptSegments.js +59 -8
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//#region src/layout-painter/cursiveJoiners.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Repair cursive letter connections that a face change severed.
|
|
4
|
+
*
|
|
5
|
+
* Browsers shape across an inline box boundary only while no shaping-relevant
|
|
6
|
+
* property changes. Colour and underline are safe, so tracked-change and
|
|
7
|
+
* comment marks already join. A face change is not: bold, italic, a different
|
|
8
|
+
* size or a different family selects another font, shaping stops there, and a
|
|
9
|
+
* cursive word split mid-word by such a run falls back to isolated forms and
|
|
10
|
+
* visibly comes apart. Word joins straight through the same boundary.
|
|
11
|
+
*
|
|
12
|
+
* The repair is a zero-width joiner on each side of the boundary, carried in its
|
|
13
|
+
* OWN span rather than appended to the run's text. That distinction is
|
|
14
|
+
* load-bearing: `data-pm-start`/`data-pm-end` spans map DOM text offsets back to
|
|
15
|
+
* ProseMirror positions for hit-testing, so growing a run's text node by a
|
|
16
|
+
* character would desync every offset after it. A joiner span carries no pm
|
|
17
|
+
* attributes, so the offset contract is untouched.
|
|
18
|
+
*
|
|
19
|
+
* Whether two runs share a face is decided by running the painter's own
|
|
20
|
+
* `applyRunStyles` over a probe element for each side and diffing the result,
|
|
21
|
+
* rather than by re-deriving the face from run fields. There is no second copy
|
|
22
|
+
* of the font decisions to drift from the first, and a property this module has
|
|
23
|
+
* never heard of counts as a face change: over-inserting a joiner is invisible,
|
|
24
|
+
* missing one is not.
|
|
25
|
+
*
|
|
26
|
+
* KNOWN RESIDUAL — the measurer does not see this repair. Canvas `measureText`
|
|
27
|
+
* ignores a joiner at a string edge (measuring the two halves with and without
|
|
28
|
+
* one returns bit-identical widths), while DOM layout applies the joined forms.
|
|
29
|
+
* So a repaired word paints at a different width than the measurer reserved, by
|
|
30
|
+
* roughly the difference between isolated and medial advances.
|
|
31
|
+
*
|
|
32
|
+
* Which DIRECTION depends on the font: measured against two Arabic fallback
|
|
33
|
+
* faces the divergence was -2.7px on one and +2.5px on the other. So it is a
|
|
34
|
+
* bounded disagreement that can push a line either way, NOT a safe
|
|
35
|
+
* over-reservation, and a line can therefore come out marginally too long. It
|
|
36
|
+
* arises only on words that contain a face change, and
|
|
37
|
+
* `tests/visual/measure-parity.spec.ts` holds it to a budget. Closing it needs widths from a real shaper
|
|
38
|
+
* rather than canvas; until then the trade is a correct rendering against a
|
|
39
|
+
* slightly wrong measurement, which beats a measurement that exactly matches a
|
|
40
|
+
* visibly broken rendering.
|
|
41
|
+
*/
|
|
42
|
+
/** Marks a span as joiner furniture: no text of its own, no pm positions. */
|
|
43
|
+
declare const JOINER_DATASET_KEY = "docxJoiner";
|
|
44
|
+
/** `applyRunStyles`, injected so this module does not import the painter back. */
|
|
45
|
+
type ApplyRunStyles<TRun> = (element: HTMLElement, run: TRun) => void;
|
|
46
|
+
/** Which side(s) of a run need a joiner span. */
|
|
47
|
+
type JoinerSides = {
|
|
48
|
+
leading: boolean;
|
|
49
|
+
trailing: boolean;
|
|
50
|
+
};
|
|
51
|
+
type PlanOptions<TRun> = {
|
|
52
|
+
/** Runs in paint order, already sliced for the line and script-split. */
|
|
53
|
+
runs: readonly TRun[];
|
|
54
|
+
/** Text of a run, or undefined for a run that paints no text (image, break). */
|
|
55
|
+
textOf: (run: TRun) => string | undefined;
|
|
56
|
+
applyRunStyles: ApplyRunStyles<TRun>;
|
|
57
|
+
doc: Document;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Decide, for each run, whether it needs a joiner on either side.
|
|
61
|
+
*
|
|
62
|
+
* Only adjacent text-bearing runs are considered: an image or a tab between two
|
|
63
|
+
* words means they were never connected, so no joiner belongs there.
|
|
64
|
+
*/
|
|
65
|
+
declare function planCursiveJoiners<TRun>({ runs, textOf, applyRunStyles, doc }: PlanOptions<TRun>): Map<TRun, JoinerSides>;
|
|
66
|
+
/**
|
|
67
|
+
* Build a joiner span wearing `run`'s own painted styles, so shaping crosses
|
|
68
|
+
* from that run into the joiner and takes the joined form.
|
|
69
|
+
*
|
|
70
|
+
* Without the matching styles the joiner would sit in a different face and the
|
|
71
|
+
* repair would be a no-op that still looked applied.
|
|
72
|
+
*/
|
|
73
|
+
declare function createJoinerSpan<TRun>(run: TRun, applyRunStyles: ApplyRunStyles<TRun>, doc: Document): HTMLElement;
|
|
74
|
+
/**
|
|
75
|
+
* A run element wrapped with whatever joiners its boundaries need, ready to
|
|
76
|
+
* spread into `append`. Returns the element alone when no repair applies.
|
|
77
|
+
*/
|
|
78
|
+
declare function withCursiveJoiners<TRun>(runEl: HTMLElement, run: TRun, plan: Map<TRun, JoinerSides>, applyRunStyles: ApplyRunStyles<TRun>, doc: Document): HTMLElement[];
|
|
79
|
+
//#endregion
|
|
80
|
+
export { ApplyRunStyles, JOINER_DATASET_KEY, JoinerSides, createJoinerSpan, planCursiveJoiners, withCursiveJoiners };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { hasCursiveLetter, joinsAcrossBoundary } from "../utils/cursiveJoining.js";
|
|
2
|
+
//#region src/layout-painter/cursiveJoiners.ts
|
|
3
|
+
/**
|
|
4
|
+
* Repair cursive letter connections that a face change severed.
|
|
5
|
+
*
|
|
6
|
+
* Browsers shape across an inline box boundary only while no shaping-relevant
|
|
7
|
+
* property changes. Colour and underline are safe, so tracked-change and
|
|
8
|
+
* comment marks already join. A face change is not: bold, italic, a different
|
|
9
|
+
* size or a different family selects another font, shaping stops there, and a
|
|
10
|
+
* cursive word split mid-word by such a run falls back to isolated forms and
|
|
11
|
+
* visibly comes apart. Word joins straight through the same boundary.
|
|
12
|
+
*
|
|
13
|
+
* The repair is a zero-width joiner on each side of the boundary, carried in its
|
|
14
|
+
* OWN span rather than appended to the run's text. That distinction is
|
|
15
|
+
* load-bearing: `data-pm-start`/`data-pm-end` spans map DOM text offsets back to
|
|
16
|
+
* ProseMirror positions for hit-testing, so growing a run's text node by a
|
|
17
|
+
* character would desync every offset after it. A joiner span carries no pm
|
|
18
|
+
* attributes, so the offset contract is untouched.
|
|
19
|
+
*
|
|
20
|
+
* Whether two runs share a face is decided by running the painter's own
|
|
21
|
+
* `applyRunStyles` over a probe element for each side and diffing the result,
|
|
22
|
+
* rather than by re-deriving the face from run fields. There is no second copy
|
|
23
|
+
* of the font decisions to drift from the first, and a property this module has
|
|
24
|
+
* never heard of counts as a face change: over-inserting a joiner is invisible,
|
|
25
|
+
* missing one is not.
|
|
26
|
+
*
|
|
27
|
+
* KNOWN RESIDUAL — the measurer does not see this repair. Canvas `measureText`
|
|
28
|
+
* ignores a joiner at a string edge (measuring the two halves with and without
|
|
29
|
+
* one returns bit-identical widths), while DOM layout applies the joined forms.
|
|
30
|
+
* So a repaired word paints at a different width than the measurer reserved, by
|
|
31
|
+
* roughly the difference between isolated and medial advances.
|
|
32
|
+
*
|
|
33
|
+
* Which DIRECTION depends on the font: measured against two Arabic fallback
|
|
34
|
+
* faces the divergence was -2.7px on one and +2.5px on the other. So it is a
|
|
35
|
+
* bounded disagreement that can push a line either way, NOT a safe
|
|
36
|
+
* over-reservation, and a line can therefore come out marginally too long. It
|
|
37
|
+
* arises only on words that contain a face change, and
|
|
38
|
+
* `tests/visual/measure-parity.spec.ts` holds it to a budget. Closing it needs widths from a real shaper
|
|
39
|
+
* rather than canvas; until then the trade is a correct rendering against a
|
|
40
|
+
* slightly wrong measurement, which beats a measurement that exactly matches a
|
|
41
|
+
* visibly broken rendering.
|
|
42
|
+
*/
|
|
43
|
+
/** U+200D ZERO WIDTH JOINER — Joining_Type C, so it joins on both sides. */
|
|
44
|
+
const ZERO_WIDTH_JOINER = "";
|
|
45
|
+
/** Marks a span as joiner furniture: no text of its own, no pm positions. */
|
|
46
|
+
const JOINER_DATASET_KEY = "docxJoiner";
|
|
47
|
+
/**
|
|
48
|
+
* Inline properties that `applyRunStyles` may set which provably cannot change
|
|
49
|
+
* how text shapes, and so must NOT be read as a face change.
|
|
50
|
+
*
|
|
51
|
+
* A deny-list rather than an allow-list on purpose: an unlisted new property is
|
|
52
|
+
* treated as face-changing, which inserts a harmless extra joiner instead of
|
|
53
|
+
* silently stopping the repair. Fails toward the visible-correct rendering.
|
|
54
|
+
*/
|
|
55
|
+
const SHAPING_NEUTRAL_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
56
|
+
"color",
|
|
57
|
+
"--doc-run-color",
|
|
58
|
+
"backgroundColor",
|
|
59
|
+
"background-color",
|
|
60
|
+
"textDecoration",
|
|
61
|
+
"text-decoration",
|
|
62
|
+
"textDecorationColor",
|
|
63
|
+
"text-decoration-color",
|
|
64
|
+
"textDecorationStyle",
|
|
65
|
+
"text-decoration-style",
|
|
66
|
+
"textUnderlineOffset",
|
|
67
|
+
"text-underline-offset",
|
|
68
|
+
"verticalAlign",
|
|
69
|
+
"vertical-align",
|
|
70
|
+
"opacity",
|
|
71
|
+
"borderBottom",
|
|
72
|
+
"border-bottom",
|
|
73
|
+
"outline"
|
|
74
|
+
]);
|
|
75
|
+
/**
|
|
76
|
+
* The style declaration a run paints with, as a plain comparable record.
|
|
77
|
+
*
|
|
78
|
+
* Reading the keys the painter actually assigned (rather than enumerating a
|
|
79
|
+
* CSSStyleDeclaration) keeps this working against both the real DOM and the
|
|
80
|
+
* minimal fake the painter tests use.
|
|
81
|
+
*/
|
|
82
|
+
function paintedStyle(run, applyRunStyles, doc) {
|
|
83
|
+
const probe = doc.createElement("span");
|
|
84
|
+
applyRunStyles(probe, run);
|
|
85
|
+
const style = {};
|
|
86
|
+
for (const [property, value] of styleEntries(probe.style)) {
|
|
87
|
+
if (value === "") continue;
|
|
88
|
+
if (SHAPING_NEUTRAL_STYLE_PROPERTIES.has(property)) continue;
|
|
89
|
+
style[property] = value;
|
|
90
|
+
}
|
|
91
|
+
return style;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Enumerate the properties a declaration actually has set.
|
|
95
|
+
*
|
|
96
|
+
* A real `CSSStyleDeclaration` is not a plain object: `Object.entries` on one
|
|
97
|
+
* yields its INDEXED entries, so `["0", "font-family"]` rather than
|
|
98
|
+
* `["fontFamily", "Arial"]`, which compares the list of property names instead
|
|
99
|
+
* of their values. The painter's test fake IS a plain object. Both shapes have
|
|
100
|
+
* to work, and the difference between them is invisible in unit tests, so it is
|
|
101
|
+
* handled here explicitly rather than left to whichever one a caller happens to
|
|
102
|
+
* pass. Real declarations report kebab-case names, including custom properties.
|
|
103
|
+
*/
|
|
104
|
+
function styleEntries(style) {
|
|
105
|
+
const declaration = style;
|
|
106
|
+
if (typeof declaration.length === "number" && typeof declaration.item === "function" && typeof declaration.getPropertyValue === "function") {
|
|
107
|
+
const entries = [];
|
|
108
|
+
for (let index = 0; index < declaration.length; index++) {
|
|
109
|
+
const property = declaration.item(index);
|
|
110
|
+
entries.push([property, declaration.getPropertyValue(property)]);
|
|
111
|
+
}
|
|
112
|
+
return entries;
|
|
113
|
+
}
|
|
114
|
+
return Object.entries(style).filter((entry) => typeof entry[1] === "string");
|
|
115
|
+
}
|
|
116
|
+
const sameFace = (a, b) => {
|
|
117
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
118
|
+
for (const key of keys) if (a[key] !== b[key]) return false;
|
|
119
|
+
return true;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Decide, for each run, whether it needs a joiner on either side.
|
|
123
|
+
*
|
|
124
|
+
* Only adjacent text-bearing runs are considered: an image or a tab between two
|
|
125
|
+
* words means they were never connected, so no joiner belongs there.
|
|
126
|
+
*/
|
|
127
|
+
function planCursiveJoiners({ runs, textOf, applyRunStyles, doc }) {
|
|
128
|
+
const plan = /* @__PURE__ */ new Map();
|
|
129
|
+
if (runs.length < 2) return plan;
|
|
130
|
+
let lineHasCursive = false;
|
|
131
|
+
for (const run of runs) {
|
|
132
|
+
const text = textOf(run);
|
|
133
|
+
if (text !== void 0 && hasCursiveLetter(text)) {
|
|
134
|
+
lineHasCursive = true;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!lineHasCursive) return plan;
|
|
139
|
+
const styleCache = /* @__PURE__ */ new Map();
|
|
140
|
+
const styleOf = (run) => {
|
|
141
|
+
const cached = styleCache.get(run);
|
|
142
|
+
if (cached) return cached;
|
|
143
|
+
const computed = paintedStyle(run, applyRunStyles, doc);
|
|
144
|
+
styleCache.set(run, computed);
|
|
145
|
+
return computed;
|
|
146
|
+
};
|
|
147
|
+
const mark = (run, side) => {
|
|
148
|
+
const existing = plan.get(run);
|
|
149
|
+
if (existing) {
|
|
150
|
+
existing[side] = true;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
plan.set(run, {
|
|
154
|
+
leading: side === "leading",
|
|
155
|
+
trailing: side === "trailing"
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
for (let i = 0; i < runs.length - 1; i++) {
|
|
159
|
+
const before = runs[i];
|
|
160
|
+
const after = runs[i + 1];
|
|
161
|
+
const beforeText = textOf(before);
|
|
162
|
+
const afterText = textOf(after);
|
|
163
|
+
if (beforeText === void 0 || afterText === void 0) continue;
|
|
164
|
+
if (!joinsAcrossBoundary(beforeText, afterText)) continue;
|
|
165
|
+
if (sameFace(styleOf(before), styleOf(after))) continue;
|
|
166
|
+
mark(before, "trailing");
|
|
167
|
+
mark(after, "leading");
|
|
168
|
+
}
|
|
169
|
+
return plan;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Build a joiner span wearing `run`'s own painted styles, so shaping crosses
|
|
173
|
+
* from that run into the joiner and takes the joined form.
|
|
174
|
+
*
|
|
175
|
+
* Without the matching styles the joiner would sit in a different face and the
|
|
176
|
+
* repair would be a no-op that still looked applied.
|
|
177
|
+
*/
|
|
178
|
+
function createJoinerSpan(run, applyRunStyles, doc) {
|
|
179
|
+
const span = doc.createElement("span");
|
|
180
|
+
applyRunStyles(span, run);
|
|
181
|
+
span.dataset[JOINER_DATASET_KEY] = "true";
|
|
182
|
+
span.textContent = ZERO_WIDTH_JOINER;
|
|
183
|
+
return span;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* A run element wrapped with whatever joiners its boundaries need, ready to
|
|
187
|
+
* spread into `append`. Returns the element alone when no repair applies.
|
|
188
|
+
*/
|
|
189
|
+
function withCursiveJoiners(runEl, run, plan, applyRunStyles, doc) {
|
|
190
|
+
const sides = plan.get(run);
|
|
191
|
+
if (!sides) return [runEl];
|
|
192
|
+
const elements = [];
|
|
193
|
+
if (sides.leading) elements.push(createJoinerSpan(run, applyRunStyles, doc));
|
|
194
|
+
elements.push(runEl);
|
|
195
|
+
if (sides.trailing) elements.push(createJoinerSpan(run, applyRunStyles, doc));
|
|
196
|
+
return elements;
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
export { JOINER_DATASET_KEY, createJoinerSpan, planCursiveJoiners, withCursiveJoiners };
|
|
@@ -1323,6 +1323,7 @@ function runContentKey(run) {
|
|
|
1323
1323
|
if (run.color) parts.push(`c:${run.color}`);
|
|
1324
1324
|
if (run.highlight) parts.push(`hi:${run.highlight}`);
|
|
1325
1325
|
if (run.fontFamily) parts.push(`ff:${run.fontFamily}`);
|
|
1326
|
+
if (run.complexScriptFontFamily) parts.push(`cs:${run.complexScriptFontFamily}`);
|
|
1326
1327
|
if (run.eastAsiaFontFamily) parts.push(`ea:${run.eastAsiaFontFamily}`);
|
|
1327
1328
|
if (run.fontSize !== void 0) parts.push(`fs:${run.fontSize}`);
|
|
1328
1329
|
if (run.letterSpacing !== void 0) parts.push(`ls:${run.letterSpacing}`);
|
|
@@ -12,8 +12,9 @@ import { resolveFontFamily } from "../utils/fontResolver.js";
|
|
|
12
12
|
import "../utils/fontWeights.js";
|
|
13
13
|
import { inlineImageBoundingBox, parseRotationDegrees, rotatedBoundingBox } from "../utils/rotationBoundingBox.js";
|
|
14
14
|
import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
|
|
15
|
-
import { hasCjk, segmentByScript } from "../utils/scriptSegments.js";
|
|
15
|
+
import { SCRIPT_CLASS, hasCjk, hasComplexScript, segmentByScript } from "../utils/scriptSegments.js";
|
|
16
16
|
import { borderStrokeToCss, resolveParagraphBorderHorizontalOutsets } from "./borderStroke.js";
|
|
17
|
+
import { planCursiveJoiners, withCursiveJoiners } from "./cursiveJoiners.js";
|
|
17
18
|
import { getAutomaticTextColorForBackground } from "./documentColors.js";
|
|
18
19
|
import { applyImageBorder, applyImageVisualAttrs, hasImageCrop, hasImageVisualAttrs, wrapImageWithCrop } from "./renderImage.js";
|
|
19
20
|
import { resolveImageLineAlign } from "./renderUtils.js";
|
|
@@ -591,7 +592,10 @@ function renderFieldRun(run, doc, context) {
|
|
|
591
592
|
kind: "text",
|
|
592
593
|
text
|
|
593
594
|
};
|
|
594
|
-
if (
|
|
595
|
+
if (needsPerScriptSpans({
|
|
596
|
+
...resolvedRun,
|
|
597
|
+
text
|
|
598
|
+
})) {
|
|
595
599
|
const wrapper = doc.createElement("span");
|
|
596
600
|
applyPmPositions(wrapper, resolvedRun.pmStart, resolvedRun.pmEnd);
|
|
597
601
|
if (resolvedRun.horizontalScale && resolvedRun.horizontalScale !== 100) {
|
|
@@ -604,7 +608,7 @@ function renderFieldRun(run, doc, context) {
|
|
|
604
608
|
const segmentRun = {
|
|
605
609
|
...segmentBase,
|
|
606
610
|
text: segment.text,
|
|
607
|
-
...segment.
|
|
611
|
+
...scriptFontOverride(resolvedRun, segment.script)
|
|
608
612
|
};
|
|
609
613
|
wrapper.append(renderTextRun(segmentRun, doc));
|
|
610
614
|
}
|
|
@@ -719,6 +723,26 @@ function sliceRunsForLine(block, line) {
|
|
|
719
723
|
return result;
|
|
720
724
|
}
|
|
721
725
|
/**
|
|
726
|
+
* The font a script segment paints with, mirroring the measurer's
|
|
727
|
+
* `scriptFontFamily`. Returning undefined leaves the run's own `fontFamily`.
|
|
728
|
+
*/
|
|
729
|
+
const scriptFontOverride = (run, script) => {
|
|
730
|
+
if (script === SCRIPT_CLASS.eastAsia && run.eastAsiaFontFamily !== void 0) return { fontFamily: run.eastAsiaFontFamily };
|
|
731
|
+
if (script === SCRIPT_CLASS.complex && run.complexScriptFontFamily !== void 0) return { fontFamily: run.complexScriptFontFamily };
|
|
732
|
+
return {};
|
|
733
|
+
};
|
|
734
|
+
/**
|
|
735
|
+
* Whether a run needs per-script sibling spans: it carries a script-specific
|
|
736
|
+
* font slot AND text that selects it. A letter-spaced run is excluded because
|
|
737
|
+
* CSS letter-spacing does not bridge sibling spans, and the measurer skips the
|
|
738
|
+
* same case so the widths still agree.
|
|
739
|
+
*/
|
|
740
|
+
const needsPerScriptSpans = (run) => {
|
|
741
|
+
if (run.letterSpacing) return false;
|
|
742
|
+
if (run.eastAsiaFontFamily !== void 0 && hasCjk(run.text)) return true;
|
|
743
|
+
return run.complexScriptFontFamily !== void 0 && hasComplexScript(run.text);
|
|
744
|
+
};
|
|
745
|
+
/**
|
|
722
746
|
* Split each text run carrying an East-Asian font into per-script sub-runs, so
|
|
723
747
|
* CJK code points render with `eastAsiaFontFamily` and the rest with
|
|
724
748
|
* `fontFamily`. Each sub-run gets a contiguous, exact `pmStart`/`pmEnd` (the
|
|
@@ -730,7 +754,7 @@ function sliceRunsForLine(block, line) {
|
|
|
730
754
|
function splitTextRunsByEastAsia(runs) {
|
|
731
755
|
const result = [];
|
|
732
756
|
for (const run of runs) {
|
|
733
|
-
if (!isTextRun(run) ||
|
|
757
|
+
if (!isTextRun(run) || !needsPerScriptSpans(run)) {
|
|
734
758
|
result.push(run);
|
|
735
759
|
continue;
|
|
736
760
|
}
|
|
@@ -739,7 +763,7 @@ function splitTextRunsByEastAsia(runs) {
|
|
|
739
763
|
result.push({
|
|
740
764
|
...run,
|
|
741
765
|
text: segment.text,
|
|
742
|
-
...segment.
|
|
766
|
+
...scriptFontOverride(run, segment.script),
|
|
743
767
|
...run.pmStart !== void 0 ? {
|
|
744
768
|
pmStart: run.pmStart + offset,
|
|
745
769
|
pmEnd: run.pmStart + offset + segment.text.length
|
|
@@ -842,6 +866,7 @@ function runMeasureStyle(run) {
|
|
|
842
866
|
...run.letterSpacing !== void 0 ? { letterSpacing: run.letterSpacing } : {},
|
|
843
867
|
...run.smallCaps !== void 0 ? { smallCaps: run.smallCaps } : {},
|
|
844
868
|
...run.eastAsiaFontFamily !== void 0 ? { eastAsiaFontFamily: run.eastAsiaFontFamily } : {},
|
|
869
|
+
...run.complexScriptFontFamily !== void 0 ? { complexScriptFontFamily: run.complexScriptFontFamily } : {},
|
|
845
870
|
kerning: getRunFontKerningMode(run, 11) === FONT_KERNING_MODE.enabled
|
|
846
871
|
};
|
|
847
872
|
}
|
|
@@ -956,11 +981,14 @@ function createTextMeasurer(doc) {
|
|
|
956
981
|
if (style.bold) fontPrefixParts.push("700");
|
|
957
982
|
fontPrefixParts.push(`${fontSizePx}px`);
|
|
958
983
|
const fontPrefix = fontPrefixParts.join(" ");
|
|
959
|
-
|
|
960
|
-
|
|
984
|
+
const perScriptFallback = {
|
|
985
|
+
...style.eastAsiaFontFamily ? { [SCRIPT_CLASS.eastAsia]: resolveFontFamily(style.eastAsiaFontFamily).cssFallback } : {},
|
|
986
|
+
...style.complexScriptFontFamily ? { [SCRIPT_CLASS.complex]: resolveFontFamily(style.complexScriptFontFamily).cssFallback } : {}
|
|
987
|
+
};
|
|
988
|
+
if (!style.letterSpacing && (style.eastAsiaFontFamily && hasCjk(text) || style.complexScriptFontFamily && hasComplexScript(text))) {
|
|
961
989
|
let segmentedWidth = 0;
|
|
962
990
|
for (const segment of segmentByScript(text)) {
|
|
963
|
-
ctx.font = `${fontPrefix} ${segment.
|
|
991
|
+
ctx.font = `${fontPrefix} ${perScriptFallback[segment.script] ?? cssFallback}`;
|
|
964
992
|
segmentedWidth += ctx.measureText(segment.text).width;
|
|
965
993
|
}
|
|
966
994
|
return segmentedWidth;
|
|
@@ -985,10 +1013,24 @@ function renderLine(block, line, alignment, doc, options) {
|
|
|
985
1013
|
lineEl.style.boxSizing = "content-box";
|
|
986
1014
|
lineEl.style.height = `${line.lineHeight}px`;
|
|
987
1015
|
lineEl.style.lineHeight = `${line.lineHeight}px`;
|
|
1016
|
+
lineEl.dataset["measuredWidth"] = String(line.width);
|
|
988
1017
|
const splitRuns = splitTextRunsByEastAsia(sliceRunsForLine(block, line));
|
|
989
1018
|
const { runs: runsForLine, collapsedLeadingRuns: collapsedLeadingSpaceRuns, collapsedTrailingRuns: collapsedTrailingSpaceRuns } = splitCollapsibleLineEdgeSpaces(splitRuns, startsAfterSoftWrap(block, line));
|
|
990
1019
|
const isCollapsedLineEdgeSpaceRun = (run) => collapsedLeadingSpaceRuns.has(run) || collapsedTrailingSpaceRuns.has(run);
|
|
991
1020
|
const collapsedSpaceMeasureText = collapsedLeadingSpaceRuns.size > 0 || collapsedTrailingSpaceRuns.size > 0 ? createTextMeasurer(doc) : void 0;
|
|
1021
|
+
const applyJoinerRunStyles = (element, run) => {
|
|
1022
|
+
if (isTextRun(run) || isTabRun(run)) applyRunStyles(element, run);
|
|
1023
|
+
};
|
|
1024
|
+
const cursiveJoinerPlan = planCursiveJoiners({
|
|
1025
|
+
runs: runsForLine,
|
|
1026
|
+
textOf: (run) => isTextRun(run) ? toPaintedText(run.text) : void 0,
|
|
1027
|
+
applyRunStyles: applyJoinerRunStyles,
|
|
1028
|
+
doc
|
|
1029
|
+
});
|
|
1030
|
+
const withJoiners = (runEl, run) => {
|
|
1031
|
+
if (lineEl.style.display === "flex") return [runEl];
|
|
1032
|
+
return withCursiveJoiners(runEl, run, cursiveJoinerPlan, applyJoinerRunStyles, doc);
|
|
1033
|
+
};
|
|
992
1034
|
const renderLineTextRun = (run) => {
|
|
993
1035
|
const runEl = renderTextRun(run, doc);
|
|
994
1036
|
if (collapsedLeadingSpaceRuns.has(run)) runEl.dataset["collapsedLeadingSpaces"] = "true";
|
|
@@ -1032,10 +1074,11 @@ function renderLine(block, line, alignment, doc, options) {
|
|
|
1032
1074
|
if (alignment === "justify" && options) {
|
|
1033
1075
|
if (!options.isLastLine || options.paragraphEndsWithLineBreak) {
|
|
1034
1076
|
const firstLineIndentPx = options.isFirstLine ? options.firstLineIndentPx ?? 0 : 0;
|
|
1077
|
+
const firstLinePositiveIndentPx = Math.max(0, firstLineIndentPx);
|
|
1035
1078
|
const firstLineHangingPx = Math.max(0, -firstLineIndentPx);
|
|
1036
1079
|
const hasVisibleListMarker = options.isFirstLine && block.attrs?.listMarker && !block.attrs.listMarkerHidden;
|
|
1037
1080
|
const firstLineHangingExpansionPx = hasVisibleListMarker ? Math.min(firstLineHangingPx, Math.max(0, options.leftIndentPx ?? 0)) : firstLineHangingPx;
|
|
1038
|
-
const justifyCapacityPx = options.availableWidth + firstLineHangingExpansionPx;
|
|
1081
|
+
const justifyCapacityPx = options.availableWidth - firstLinePositiveIndentPx + firstLineHangingExpansionPx;
|
|
1039
1082
|
const overfullPx = line.width - justifyCapacityPx;
|
|
1040
1083
|
const shrinkableSpaces = countShrinkableSpaces(runsForLine.filter((run) => !isTextRun(run) || !isCollapsedLineEdgeSpaceRun(run)), options.context);
|
|
1041
1084
|
if (overfullPx > RIGHT_EDGE_EPSILON_PX && shrinkableSpaces > 0) {
|
|
@@ -1113,7 +1156,7 @@ function renderLine(block, line, alignment, doc, options) {
|
|
|
1113
1156
|
for (let j = i + 1; j < runsForLine.length; j++) {
|
|
1114
1157
|
const next = runsForLine[j];
|
|
1115
1158
|
if (isTabRun(next) || isLineBreakRun(next)) break;
|
|
1116
|
-
if (isTextRun(next)) lineEl.append(renderLineTextRun(next));
|
|
1159
|
+
if (isTextRun(next)) lineEl.append(...withJoiners(renderLineTextRun(next), next));
|
|
1117
1160
|
else if (isFieldRun(next) && options?.context) lineEl.append(renderFieldRun(next, doc, options.context));
|
|
1118
1161
|
else if (isImageRun(next)) {
|
|
1119
1162
|
if (isFloatingImageRun(next)) continue;
|
|
@@ -1133,7 +1176,7 @@ function renderLine(block, line, alignment, doc, options) {
|
|
|
1133
1176
|
currentX += tabWidth;
|
|
1134
1177
|
} else if (isTextRun(run)) {
|
|
1135
1178
|
const runEl = renderLineTextRun(run);
|
|
1136
|
-
lineEl.append(runEl);
|
|
1179
|
+
lineEl.append(...withJoiners(runEl, run));
|
|
1137
1180
|
if (isCollapsedLineEdgeSpaceRun(run)) continue;
|
|
1138
1181
|
if (!measureText) continue;
|
|
1139
1182
|
const fontSize = run.fontSize || 11;
|
package/dist/server.d.ts
CHANGED
|
@@ -18,8 +18,8 @@ import { EvaluateDocxXmlPatchProposalArgs, FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULT
|
|
|
18
18
|
import { FOLIO_DOCX_CONFORMANCE_CHECKS, FOLIO_DOCX_CONFORMANCE_ISSUE_CODES, FOLIO_DOCX_CONFORMANCE_PROFILE, FOLIO_DOCX_CONFORMANCE_REPORT_VERSION, FolioDocxConformanceCheck, FolioDocxConformanceCheckId, FolioDocxConformanceCheckStatus, FolioDocxConformanceIssue, FolioDocxConformanceIssueCode, FolioDocxConformanceReport, FolioDocxConformanceStatus, ValidateDocxConformanceOptions, validateDocxConformance } from "./docx/server/validateDocxConformance.js";
|
|
19
19
|
import { ApplyDocxXmlPatchProposalArgs, FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE, FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION, FolioDocxXmlPatchApplication, FolioDocxXmlPatchApplicationError, FolioDocxXmlPatchApplicationReceipt, UnsupportedFolioDocxXmlPatchApplicationProfileError, applyDocxXmlPatchProposal } from "./docx/server/applyDocxXmlPatchProposal.js";
|
|
20
20
|
import { docxToMarkdown } from "./docx/server/docxToMarkdown.js";
|
|
21
|
-
import { DocxParagraphSource, DocxTableRowKind, DocxTableRowPosition, ExtractedDocxParagraph, ExtractedDocxText, extractDocxText } from "./docx/server/extractDocxText.js";
|
|
21
|
+
import { DocxParagraphSource, DocxTableRowKind, DocxTableRowPosition, ExtractedDocxParagraph, ExtractedDocxTableCell, ExtractedDocxTableCellParagraph, ExtractedDocxText, extractDocxText } from "./docx/server/extractDocxText.js";
|
|
22
22
|
import { FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS, FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES, FOLIO_DOCX_PACKAGE_INSPECTION_VERSION, FolioDocxInspectedXmlPart, FolioDocxPackageInspection, FolioDocxPackageInspectionError, FolioDocxPackageInspectionErrorCode, FolioDocxPackageInspectionLimits, FolioDocxPackagePart, FolioDocxPackagePartKind, InspectDocxPackageOptions, inspectDocxPackage } from "./docx/server/inspectDocxPackage.js";
|
|
23
23
|
import { GenerateRedlineDocxOptions, GenerateRedlineDocxResult, GenerateRedlineUnprocessedStory, InvalidGenerateRedlineDocxOptionsError, generateRedlineDocx } from "./redline.js";
|
|
24
24
|
import { FOLIO_VERSION_COMPARISON_PRIVACY_TRANSFORMS, FOLIO_VERSION_COMPARISON_SCOPES, FolioBlockDiff, FolioCompareDocxVersionsOptions, FolioDocumentMetadataValue, FolioFormatProperty, FolioMetadataDiff, FolioStoryDiff, FolioVersionBlockHandle, FolioVersionComparisonPrivacyTransform, FolioVersionComparisonScope, FolioVersionDiff, FolioVersionDiffPrivacyOptions, FolioVersionDiffPrivacyReport, FolioVersionDiffSegment, FolioVersionDiffSummaryCounts, InvalidFolioVersionComparisonOptionsError, applyFolioVersionDiffPrivacy, compareDocxVersions, isFolioVersionComparisonPrivacyTransform, isFolioVersionComparisonScope } from "./version-comparison.js";
|
|
25
|
-
export { type ApplyDocxXmlPatchProposalArgs, type ApplyFolioAIEditsToBufferOptions, type ApplyFolioAIEditsToBufferResult, type CreateCommentReplyInput, type CreateEmptyDocumentOptions, DOCUMENT_PRESET_VERSION, DOCUMENT_STYLE_SET_VERSION, type DeriveBlockIdInput, type DocumentPreset, type DocumentStyleCatalog, type DocumentStyleCatalogEntry, type DocumentStyleSet, DocxArchiveError, type DocxArchiveOptions, type DocxParagraphSource, type DocxTableRowKind, type DocxTableRowPosition, EnsureParaIdsError, type EnsureParaIdsOptions, type EnsureParaIdsResult, type EvaluateDocxXmlPatchProposalArgs, type ExtractDocumentStyleSetOptions, type ExtractedDocxParagraph, type ExtractedDocxText, FOLIO_DOCUMENT_METADATA_PROPERTIES, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FOLIO_DOCUMENT_PRIVACY_TRANSFORMS, FOLIO_DOCX_CONFORMANCE_CHECKS, FOLIO_DOCX_CONFORMANCE_ISSUE_CODES, FOLIO_DOCX_CONFORMANCE_PROFILE, FOLIO_DOCX_CONFORMANCE_REPORT_VERSION, FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS, FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES, FOLIO_DOCX_PACKAGE_INSPECTION_VERSION, FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE, FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION, FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS, FOLIO_DOCX_XML_PATCH_PROPOSAL_ISSUE_CODES, FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE, FOLIO_DOCX_XML_PATCH_PROPOSAL_VERSION, FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FOLIO_VERSION_COMPARISON_PRIVACY_TRANSFORMS, FOLIO_VERSION_COMPARISON_SCOPES, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditSnapshot, type FolioAIInlineFormatting, type FolioAITextRangeHandle, type FolioApplyDocumentOperationsOptions, type FolioApplyDocumentOperationsToStoryOptions, type FolioApplyOperationsOptions, type FolioBlockDiff, type FolioBlockId, type FolioCompareDocxVersionsOptions, type FolioDocumentMetadataProperty, type FolioDocumentMetadataValue, type FolioDocumentNavigationTarget, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationStatus, type FolioDocumentOperationStory, type FolioDocumentOperationType, type FolioDocumentOperationUndoFailureReason, type FolioDocumentOperationUndoHandle, type FolioDocumentOperationUndoResult, type FolioDocumentOutline, type FolioDocumentOutlineEntry, FolioDocumentPrivacyArchiveError, type FolioDocumentPrivacyOptions, type FolioDocumentPrivacyReport, type FolioDocumentPrivacyTransform, type FolioDocumentSection, type FolioDocumentSectionHandle, type FolioDocumentSectionReadResult, type FolioDocumentStory, type FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, type FolioDocxConformanceCheck, type FolioDocxConformanceCheckId, type FolioDocxConformanceCheckStatus, type FolioDocxConformanceIssue, type FolioDocxConformanceIssueCode, type FolioDocxConformanceReport, type FolioDocxConformanceStatus, type FolioDocxInspectedXmlPart, type FolioDocxPackageInspection, FolioDocxPackageInspectionError, type FolioDocxPackageInspectionErrorCode, type FolioDocxPackageInspectionLimits, type FolioDocxPackagePart, type FolioDocxPackagePartKind, type FolioDocxPreparedXmlReplacement, FolioDocxReviewer, type FolioDocxReviewerOptions, type FolioDocxXmlPatchApplication, FolioDocxXmlPatchApplicationError, type FolioDocxXmlPatchApplicationReceipt, type FolioDocxXmlPatchProposal, type FolioDocxXmlPatchProposalEvaluation, type FolioDocxXmlPatchProposalIssue, type FolioDocxXmlPatchProposalIssueCode, type FolioDocxXmlPatchProposalLimits, type FolioDocxXmlReplacement, type FolioEditableDocumentStoryHandle, type FolioFormatProperty, type FolioMetadataDiff, type FolioReadReviewedStoryOptions, type FolioResolveReviewedStoryOptions, type FolioResolvedReviewedView, type FolioReviewChange, type FolioReviewChangeFilter, type FolioReviewChangeKind, type FolioReviewComment, type FolioReviewCommentFilter, type FolioReviewCommentReply, type FolioReviewReplyInput, type FolioReviewedStory, type FolioReviewedView, type FolioStoryDiff, type FolioVersionBlockHandle, type FolioVersionComparisonPrivacyTransform, type FolioVersionComparisonScope, type FolioVersionDiff, type FolioVersionDiffPrivacyOptions, type FolioVersionDiffPrivacyReport, type FolioVersionDiffSegment, type FolioVersionDiffSummaryCounts, type GenerateRedlineDocxOptions, type GenerateRedlineDocxResult, type GenerateRedlineUnprocessedStory, type InspectDocxPackageOptions, InvalidFolioDocumentOperationBatchError, InvalidFolioDocumentPrivacyOptionsError, InvalidFolioDocxXmlPatchProposalError, InvalidFolioDocxXmlPatchProposalOptionsError, InvalidFolioVersionComparisonOptionsError, InvalidGenerateRedlineDocxOptionsError, type RewriteDocxMetadataPrivacyResult, STELLA_STYLE_SET_NAME, UnsupportedFolioDocumentOperationVersionError, UnsupportedFolioDocxXmlPatchApplicationProfileError, UnsupportedFolioReviewedViewError, type ValidateDocxConformanceOptions, applyDocxXmlPatchProposal, applyFolioAIEditsToBuffer, applyFolioVersionDiffPrivacy, assertSupportedFolioDocumentOperationVersion, compareDocxVersions, createDocx, createEmptyDocument, createFolioAITextRangeHandle, createStellaStyleDocumentPreset, createStellaStyleSet, deriveBlockId, docxToMarkdown, ensureParaIds, evaluateDocxXmlPatchProposal, extractDocumentStyleSet, extractDocumentStyleSetFromDocx, extractDocxText, generateRedlineDocx, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioDocumentOutline, getFolioParaIdFromBlockId, hashFolioAIBlockText, inspectDocumentStyles, inspectDocumentStylesFromDocx, inspectDocxPackage, isFolioBlockId, isFolioDocumentOperationModeSupported, isFolioDocumentPrivacyTransform, isFolioResolvedReviewedView, isFolioReviewedView, isFolioVersionComparisonPrivacyTransform, isFolioVersionComparisonScope, isSequentialFolioBlockId, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, parseFolioDocxXmlPatchProposal, readFolioDocumentSection, replyToComment, rewriteDocxMetadataPrivacy, validateDocxConformance };
|
|
25
|
+
export { type ApplyDocxXmlPatchProposalArgs, type ApplyFolioAIEditsToBufferOptions, type ApplyFolioAIEditsToBufferResult, type CreateCommentReplyInput, type CreateEmptyDocumentOptions, DOCUMENT_PRESET_VERSION, DOCUMENT_STYLE_SET_VERSION, type DeriveBlockIdInput, type DocumentPreset, type DocumentStyleCatalog, type DocumentStyleCatalogEntry, type DocumentStyleSet, DocxArchiveError, type DocxArchiveOptions, type DocxParagraphSource, type DocxTableRowKind, type DocxTableRowPosition, EnsureParaIdsError, type EnsureParaIdsOptions, type EnsureParaIdsResult, type EvaluateDocxXmlPatchProposalArgs, type ExtractDocumentStyleSetOptions, type ExtractedDocxParagraph, type ExtractedDocxTableCell, type ExtractedDocxTableCellParagraph, type ExtractedDocxText, FOLIO_DOCUMENT_METADATA_PROPERTIES, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FOLIO_DOCUMENT_PRIVACY_TRANSFORMS, FOLIO_DOCX_CONFORMANCE_CHECKS, FOLIO_DOCX_CONFORMANCE_ISSUE_CODES, FOLIO_DOCX_CONFORMANCE_PROFILE, FOLIO_DOCX_CONFORMANCE_REPORT_VERSION, FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS, FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES, FOLIO_DOCX_PACKAGE_INSPECTION_VERSION, FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE, FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION, FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS, FOLIO_DOCX_XML_PATCH_PROPOSAL_ISSUE_CODES, FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE, FOLIO_DOCX_XML_PATCH_PROPOSAL_VERSION, FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FOLIO_VERSION_COMPARISON_PRIVACY_TRANSFORMS, FOLIO_VERSION_COMPARISON_SCOPES, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditSnapshot, type FolioAIInlineFormatting, type FolioAITextRangeHandle, type FolioApplyDocumentOperationsOptions, type FolioApplyDocumentOperationsToStoryOptions, type FolioApplyOperationsOptions, type FolioBlockDiff, type FolioBlockId, type FolioCompareDocxVersionsOptions, type FolioDocumentMetadataProperty, type FolioDocumentMetadataValue, type FolioDocumentNavigationTarget, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationStatus, type FolioDocumentOperationStory, type FolioDocumentOperationType, type FolioDocumentOperationUndoFailureReason, type FolioDocumentOperationUndoHandle, type FolioDocumentOperationUndoResult, type FolioDocumentOutline, type FolioDocumentOutlineEntry, FolioDocumentPrivacyArchiveError, type FolioDocumentPrivacyOptions, type FolioDocumentPrivacyReport, type FolioDocumentPrivacyTransform, type FolioDocumentSection, type FolioDocumentSectionHandle, type FolioDocumentSectionReadResult, type FolioDocumentStory, type FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, type FolioDocxConformanceCheck, type FolioDocxConformanceCheckId, type FolioDocxConformanceCheckStatus, type FolioDocxConformanceIssue, type FolioDocxConformanceIssueCode, type FolioDocxConformanceReport, type FolioDocxConformanceStatus, type FolioDocxInspectedXmlPart, type FolioDocxPackageInspection, FolioDocxPackageInspectionError, type FolioDocxPackageInspectionErrorCode, type FolioDocxPackageInspectionLimits, type FolioDocxPackagePart, type FolioDocxPackagePartKind, type FolioDocxPreparedXmlReplacement, FolioDocxReviewer, type FolioDocxReviewerOptions, type FolioDocxXmlPatchApplication, FolioDocxXmlPatchApplicationError, type FolioDocxXmlPatchApplicationReceipt, type FolioDocxXmlPatchProposal, type FolioDocxXmlPatchProposalEvaluation, type FolioDocxXmlPatchProposalIssue, type FolioDocxXmlPatchProposalIssueCode, type FolioDocxXmlPatchProposalLimits, type FolioDocxXmlReplacement, type FolioEditableDocumentStoryHandle, type FolioFormatProperty, type FolioMetadataDiff, type FolioReadReviewedStoryOptions, type FolioResolveReviewedStoryOptions, type FolioResolvedReviewedView, type FolioReviewChange, type FolioReviewChangeFilter, type FolioReviewChangeKind, type FolioReviewComment, type FolioReviewCommentFilter, type FolioReviewCommentReply, type FolioReviewReplyInput, type FolioReviewedStory, type FolioReviewedView, type FolioStoryDiff, type FolioVersionBlockHandle, type FolioVersionComparisonPrivacyTransform, type FolioVersionComparisonScope, type FolioVersionDiff, type FolioVersionDiffPrivacyOptions, type FolioVersionDiffPrivacyReport, type FolioVersionDiffSegment, type FolioVersionDiffSummaryCounts, type GenerateRedlineDocxOptions, type GenerateRedlineDocxResult, type GenerateRedlineUnprocessedStory, type InspectDocxPackageOptions, InvalidFolioDocumentOperationBatchError, InvalidFolioDocumentPrivacyOptionsError, InvalidFolioDocxXmlPatchProposalError, InvalidFolioDocxXmlPatchProposalOptionsError, InvalidFolioVersionComparisonOptionsError, InvalidGenerateRedlineDocxOptionsError, type RewriteDocxMetadataPrivacyResult, STELLA_STYLE_SET_NAME, UnsupportedFolioDocumentOperationVersionError, UnsupportedFolioDocxXmlPatchApplicationProfileError, UnsupportedFolioReviewedViewError, type ValidateDocxConformanceOptions, applyDocxXmlPatchProposal, applyFolioAIEditsToBuffer, applyFolioVersionDiffPrivacy, assertSupportedFolioDocumentOperationVersion, compareDocxVersions, createDocx, createEmptyDocument, createFolioAITextRangeHandle, createStellaStyleDocumentPreset, createStellaStyleSet, deriveBlockId, docxToMarkdown, ensureParaIds, evaluateDocxXmlPatchProposal, extractDocumentStyleSet, extractDocumentStyleSetFromDocx, extractDocxText, generateRedlineDocx, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioDocumentOutline, getFolioParaIdFromBlockId, hashFolioAIBlockText, inspectDocumentStyles, inspectDocumentStylesFromDocx, inspectDocxPackage, isFolioBlockId, isFolioDocumentOperationModeSupported, isFolioDocumentPrivacyTransform, isFolioResolvedReviewedView, isFolioReviewedView, isFolioVersionComparisonPrivacyTransform, isFolioVersionComparisonScope, isSequentialFolioBlockId, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, parseFolioDocxXmlPatchProposal, readFolioDocumentSection, replyToComment, rewriteDocxMetadataPrivacy, validateDocxConformance };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/utils/cursiveJoining.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Cursive-joining queries for scripts whose letters change shape by position.
|
|
4
|
+
*
|
|
5
|
+
* In Arabic, Syriac, N'Ko, Adlam and friends a letter's glyph depends on its
|
|
6
|
+
* neighbours: isolated, initial, medial or final. Shaping stops at an element
|
|
7
|
+
* boundary whenever the two sides resolve to a different font face, so a run
|
|
8
|
+
* split inside a word — bold on one letter, a different size, a different
|
|
9
|
+
* family — makes the word come apart visually, while Word joins straight
|
|
10
|
+
* through it. These predicates say whether a given split severed a connection,
|
|
11
|
+
* so the painter and the measurer can repair it identically.
|
|
12
|
+
*
|
|
13
|
+
* A boundary where both sides share a face is NOT a problem: browsers shape
|
|
14
|
+
* across inline boxes when no shaping-relevant property changes, so ordinary
|
|
15
|
+
* colour or underline marks (tracked changes, comment anchors) already join.
|
|
16
|
+
*
|
|
17
|
+
* The tables come from the pinned UCD via `scripts/generate-joining-types.ts`;
|
|
18
|
+
* see {@link joiningTypes.gen} for why they are generated rather than authored.
|
|
19
|
+
*/
|
|
20
|
+
/** Joining_Type D, L or C: this code point can connect to the one after it. */
|
|
21
|
+
declare const joinsToFollowing: (cp: number) => boolean;
|
|
22
|
+
/** Joining_Type D, R or C: this code point can connect to the one before it. */
|
|
23
|
+
declare const joinsToPreceding: (cp: number) => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Joining_Type T: invisible to joining. Combining marks (harakat, Syriac
|
|
26
|
+
* vowels) sit between two letters without breaking their connection, so they
|
|
27
|
+
* must be skipped when looking for a joining neighbour.
|
|
28
|
+
*/
|
|
29
|
+
declare const isJoiningTransparent: (cp: number) => boolean;
|
|
30
|
+
/** Joining_Type D, L or R: a cursive letter, as opposed to ZWJ or punctuation. */
|
|
31
|
+
declare const isCursiveLetter: (cp: number) => boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Whether the text contains any cursive letter at all.
|
|
34
|
+
*
|
|
35
|
+
* Callers gate on this so the all-Latin hot path never pays for joining
|
|
36
|
+
* analysis, mirroring how `hasCjk` gates script segmentation.
|
|
37
|
+
*/
|
|
38
|
+
declare function hasCursiveLetter(text: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Whether `before` and `after` were cursively connected before something split
|
|
41
|
+
* them apart.
|
|
42
|
+
*
|
|
43
|
+
* True means the two sides need a joiner to keep rendering as one word. False
|
|
44
|
+
* means the split is harmless: a space, a non-joining letter such as alef on
|
|
45
|
+
* the left of the cut, punctuation, or a script that does not join at all.
|
|
46
|
+
*/
|
|
47
|
+
declare function joinsAcrossBoundary(before: string, after: string): boolean;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { hasCursiveLetter, isCursiveLetter, isJoiningTransparent, joinsAcrossBoundary, joinsToFollowing, joinsToPreceding };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { JOINING_LETTER_RANGES, JOINING_TRANSPARENT_RANGES, JOINS_BACKWARD_RANGES, JOINS_FORWARD_RANGES } from "./joiningTypes.gen.js";
|
|
2
|
+
//#region src/utils/cursiveJoining.ts
|
|
3
|
+
/**
|
|
4
|
+
* Cursive-joining queries for scripts whose letters change shape by position.
|
|
5
|
+
*
|
|
6
|
+
* In Arabic, Syriac, N'Ko, Adlam and friends a letter's glyph depends on its
|
|
7
|
+
* neighbours: isolated, initial, medial or final. Shaping stops at an element
|
|
8
|
+
* boundary whenever the two sides resolve to a different font face, so a run
|
|
9
|
+
* split inside a word — bold on one letter, a different size, a different
|
|
10
|
+
* family — makes the word come apart visually, while Word joins straight
|
|
11
|
+
* through it. These predicates say whether a given split severed a connection,
|
|
12
|
+
* so the painter and the measurer can repair it identically.
|
|
13
|
+
*
|
|
14
|
+
* A boundary where both sides share a face is NOT a problem: browsers shape
|
|
15
|
+
* across inline boxes when no shaping-relevant property changes, so ordinary
|
|
16
|
+
* colour or underline marks (tracked changes, comment anchors) already join.
|
|
17
|
+
*
|
|
18
|
+
* The tables come from the pinned UCD via `scripts/generate-joining-types.ts`;
|
|
19
|
+
* see {@link joiningTypes.gen} for why they are generated rather than authored.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Membership test over a flat sorted `[start, end, …]` inclusive-range list.
|
|
23
|
+
*
|
|
24
|
+
* Binary search rather than `.some()`: the transparent table alone is 375
|
|
25
|
+
* ranges, and these run per code point on every measured word.
|
|
26
|
+
*/
|
|
27
|
+
function inRanges(ranges, cp) {
|
|
28
|
+
let low = 0;
|
|
29
|
+
let high = ranges.length / 2 - 1;
|
|
30
|
+
while (low <= high) {
|
|
31
|
+
const mid = low + high >> 1;
|
|
32
|
+
const start = ranges[mid * 2];
|
|
33
|
+
const end = ranges[mid * 2 + 1];
|
|
34
|
+
if (cp < start) high = mid - 1;
|
|
35
|
+
else if (cp > end) low = mid + 1;
|
|
36
|
+
else return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
/** Joining_Type D, L or C: this code point can connect to the one after it. */
|
|
41
|
+
const joinsToFollowing = (cp) => inRanges(JOINS_FORWARD_RANGES, cp);
|
|
42
|
+
/** Joining_Type D, R or C: this code point can connect to the one before it. */
|
|
43
|
+
const joinsToPreceding = (cp) => inRanges(JOINS_BACKWARD_RANGES, cp);
|
|
44
|
+
/**
|
|
45
|
+
* Joining_Type T: invisible to joining. Combining marks (harakat, Syriac
|
|
46
|
+
* vowels) sit between two letters without breaking their connection, so they
|
|
47
|
+
* must be skipped when looking for a joining neighbour.
|
|
48
|
+
*/
|
|
49
|
+
const isJoiningTransparent = (cp) => inRanges(JOINING_TRANSPARENT_RANGES, cp);
|
|
50
|
+
/** Joining_Type D, L or R: a cursive letter, as opposed to ZWJ or punctuation. */
|
|
51
|
+
const isCursiveLetter = (cp) => inRanges(JOINING_LETTER_RANGES, cp);
|
|
52
|
+
/**
|
|
53
|
+
* Whether the text contains any cursive letter at all.
|
|
54
|
+
*
|
|
55
|
+
* Callers gate on this so the all-Latin hot path never pays for joining
|
|
56
|
+
* analysis, mirroring how `hasCjk` gates script segmentation.
|
|
57
|
+
*/
|
|
58
|
+
function hasCursiveLetter(text) {
|
|
59
|
+
for (const ch of text) if (isCursiveLetter(ch.codePointAt(0))) return true;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
/** Last code point of `text` that joining can see, skipping transparents. */
|
|
63
|
+
function lastJoiningRelevantCodePoint(text) {
|
|
64
|
+
let index = text.length;
|
|
65
|
+
while (index > 0) {
|
|
66
|
+
const unit = text.charCodeAt(index - 1);
|
|
67
|
+
const isLowSurrogate = unit >= 56320 && unit <= 57343;
|
|
68
|
+
const high = isLowSurrogate ? text.charCodeAt(index - 2) : NaN;
|
|
69
|
+
const start = isLowSurrogate && high >= 55296 && high <= 56319 ? index - 2 : index - 1;
|
|
70
|
+
const cp = text.codePointAt(start);
|
|
71
|
+
if (!isJoiningTransparent(cp)) return cp;
|
|
72
|
+
index = start;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** First code point of `text` that joining can see, skipping transparents. */
|
|
76
|
+
function firstJoiningRelevantCodePoint(text) {
|
|
77
|
+
for (const ch of text) {
|
|
78
|
+
const cp = ch.codePointAt(0);
|
|
79
|
+
if (!isJoiningTransparent(cp)) return cp;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Whether `before` and `after` were cursively connected before something split
|
|
84
|
+
* them apart.
|
|
85
|
+
*
|
|
86
|
+
* True means the two sides need a joiner to keep rendering as one word. False
|
|
87
|
+
* means the split is harmless: a space, a non-joining letter such as alef on
|
|
88
|
+
* the left of the cut, punctuation, or a script that does not join at all.
|
|
89
|
+
*/
|
|
90
|
+
function joinsAcrossBoundary(before, after) {
|
|
91
|
+
const previous = lastJoiningRelevantCodePoint(before);
|
|
92
|
+
if (previous === void 0 || !joinsToFollowing(previous)) return false;
|
|
93
|
+
const next = firstJoiningRelevantCodePoint(after);
|
|
94
|
+
return next !== void 0 && joinsToPreceding(next);
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export { hasCursiveLetter, isCursiveLetter, isJoiningTransparent, joinsAcrossBoundary, joinsToFollowing, joinsToPreceding };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/utils/joiningTypes.gen.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* GENERATED FILE — do not edit.
|
|
4
|
+
*
|
|
5
|
+
* Unicode Joining_Type ranges, derived from the pinned UCD sources by
|
|
6
|
+
* `scripts/generate-joining-types.ts`. Regenerate with:
|
|
7
|
+
*
|
|
8
|
+
* bun run generate:joining-types
|
|
9
|
+
*
|
|
10
|
+
* ArabicShaping.txt: 17.0.0
|
|
11
|
+
* DerivedGeneralCategory.txt: 17.0.0
|
|
12
|
+
*
|
|
13
|
+
* Each table is a flat sorted `[start, end, …]` list of inclusive code-point
|
|
14
|
+
* ranges. Transparent is dominated by the General_Category Mn/Me/Cf default,
|
|
15
|
+
* which is why it is by far the largest table.
|
|
16
|
+
*/
|
|
17
|
+
/** Joining_Type D, L or C: connects to the FOLLOWING character. */
|
|
18
|
+
declare const JOINS_FORWARD_RANGES: readonly number[];
|
|
19
|
+
/** Joining_Type D, R or C: connects to the PRECEDING character. */
|
|
20
|
+
declare const JOINS_BACKWARD_RANGES: readonly number[];
|
|
21
|
+
/** Joining_Type T: transparent, skipped when looking for a joining neighbour. */
|
|
22
|
+
declare const JOINING_TRANSPARENT_RANGES: readonly number[];
|
|
23
|
+
/** Joining_Type D, L or R: a cursive letter. Gates the whole joining path. */
|
|
24
|
+
declare const JOINING_LETTER_RANGES: readonly number[];
|
|
25
|
+
//#endregion
|
|
26
|
+
export { JOINING_LETTER_RANGES, JOINING_TRANSPARENT_RANGES, JOINS_BACKWARD_RANGES, JOINS_FORWARD_RANGES };
|