@stll/folio-core 0.51.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compare/inline-provenance.js +18 -22
- package/dist/compare/section-boundary-properties.js +13 -0
- package/dist/generated/text_shaper.js +26 -0
- package/dist/generated/text_shaper_bg.wasm +0 -0
- package/dist/prosemirror/runStyleFormatting.d.ts +7 -1
- package/dist/prosemirror/runStyleFormatting.js +13 -7
- package/dist/shaping/shaper.d.ts +47 -4
- package/dist/shaping/shaper.js +46 -15
- package/dist/text-shaping.d.ts +4 -0
- package/dist/text-shaping.js +4 -0
- package/package.json +6 -1
|
@@ -3,7 +3,7 @@ import { expectHyperlinkMarkAttrs, expectRunPropertyChangeMarkAttrs, expectTable
|
|
|
3
3
|
import { getDocumentStyleResolver } from "../prosemirror/plugins/documentStyles.js";
|
|
4
4
|
import { applyMarksToRunFormattingRepresentation, expandRunFormattingCarrier, runFormattingCarrierReviewText } from "../prosemirror/runFormattingInlineCarriers.js";
|
|
5
5
|
import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "../prosemirror/runFormattingReconciliation.js";
|
|
6
|
-
import {
|
|
6
|
+
import { nearestParagraphRunStyleContext } from "../prosemirror/runStyleFormatting.js";
|
|
7
7
|
import { isTableCellRetainedInReviewView } from "../prosemirror/tableCellRevisionVisibility.js";
|
|
8
8
|
import { canonicalJson } from "../utils/canonicalJson.js";
|
|
9
9
|
//#region src/compare/inline-provenance.ts
|
|
@@ -19,10 +19,11 @@ const hyperlinkIdentity = (node) => {
|
|
|
19
19
|
});
|
|
20
20
|
};
|
|
21
21
|
const isInserted = (node) => node.marks.some(({ type }) => type.name === "insertion");
|
|
22
|
-
const carrierRepresentations = (carrier) => carrier.representations.map((representation) => ({
|
|
22
|
+
const carrierRepresentations = ({ carrier, paragraph }) => carrier.representations.map((representation) => ({
|
|
23
23
|
...representation,
|
|
24
24
|
from: representation.position,
|
|
25
|
-
to: representation.position + representation.node.nodeSize
|
|
25
|
+
to: representation.position + representation.node.nodeSize,
|
|
26
|
+
paragraph
|
|
26
27
|
}));
|
|
27
28
|
const targetBlockIdLookup = (anchors) => {
|
|
28
29
|
const ordered = Object.values(anchors).toSorted((left, right) => left.from - right.from || left.to - right.to);
|
|
@@ -53,7 +54,13 @@ const collectCarriers = ({ doc, targetSnapshot }) => {
|
|
|
53
54
|
const carriers = [];
|
|
54
55
|
const targetBlockIdAt = targetSnapshot ? targetBlockIdLookup(targetSnapshot.anchors) : void 0;
|
|
55
56
|
let unanchoredTargetCarrier = false;
|
|
57
|
+
const openParagraphs = [];
|
|
56
58
|
doc.descendants((node, position) => {
|
|
59
|
+
while ((openParagraphs.at(-1)?.end ?? Number.POSITIVE_INFINITY) <= position) openParagraphs.pop();
|
|
60
|
+
if (node.type.name === "paragraph") openParagraphs.push({
|
|
61
|
+
node,
|
|
62
|
+
end: position + node.nodeSize
|
|
63
|
+
});
|
|
57
64
|
if ((node.type.name === "tableCell" || node.type.name === "tableHeader") && !isTableCellRetainedInReviewView(expectTableCellAttrs(node).cellMarker?.kind, "final")) return false;
|
|
58
65
|
if (!node.isInline) return true;
|
|
59
66
|
const carrier = expandRunFormattingCarrier(node, position);
|
|
@@ -69,6 +76,7 @@ const collectCarriers = ({ doc, targetSnapshot }) => {
|
|
|
69
76
|
}
|
|
70
77
|
carriers.push({
|
|
71
78
|
carrier,
|
|
79
|
+
paragraph: openParagraphs.at(-1)?.node ?? null,
|
|
72
80
|
text: runFormattingCarrierReviewText(carrier),
|
|
73
81
|
targetBlockId
|
|
74
82
|
});
|
|
@@ -106,8 +114,8 @@ const matchCarrierStreams = ({ live, target }) => {
|
|
|
106
114
|
const targetIsText = targetCarrier.carrier.disposition === "text-run";
|
|
107
115
|
if (!liveIsText || !targetIsText) {
|
|
108
116
|
if (liveOffset !== 0 || targetOffset !== 0 || !sameCarrierShape(liveCarrier, targetCarrier)) return null;
|
|
109
|
-
const liveRepresentations = carrierRepresentations(liveCarrier
|
|
110
|
-
const targetRepresentations = carrierRepresentations(targetCarrier
|
|
117
|
+
const liveRepresentations = carrierRepresentations(liveCarrier);
|
|
118
|
+
const targetRepresentations = carrierRepresentations(targetCarrier);
|
|
111
119
|
for (const [index, liveRepresentation] of liveRepresentations.entries()) {
|
|
112
120
|
const targetRepresentation = targetRepresentations.at(index);
|
|
113
121
|
if (!targetRepresentation) return null;
|
|
@@ -123,8 +131,8 @@ const matchCarrierStreams = ({ live, target }) => {
|
|
|
123
131
|
}
|
|
124
132
|
const sharedLength = Math.min(liveText.length, targetText.length);
|
|
125
133
|
if (sharedLength === 0 || liveText.slice(0, sharedLength) !== targetText.slice(0, sharedLength)) return null;
|
|
126
|
-
const liveRepresentation = carrierRepresentations(liveCarrier
|
|
127
|
-
const targetRepresentation = carrierRepresentations(targetCarrier
|
|
134
|
+
const liveRepresentation = carrierRepresentations(liveCarrier).at(0);
|
|
135
|
+
const targetRepresentation = carrierRepresentations(targetCarrier).at(0);
|
|
128
136
|
if (!liveRepresentation || !targetRepresentation) return null;
|
|
129
137
|
matched.push({
|
|
130
138
|
live: {
|
|
@@ -152,12 +160,8 @@ const matchCarrierStreams = ({ live, target }) => {
|
|
|
152
160
|
}
|
|
153
161
|
return matched;
|
|
154
162
|
};
|
|
155
|
-
const authoredFormattingAt = ({
|
|
156
|
-
context:
|
|
157
|
-
doc,
|
|
158
|
-
pos: representation.from,
|
|
159
|
-
styleResolver
|
|
160
|
-
}),
|
|
163
|
+
const authoredFormattingAt = ({ representation, styleResolver }) => readAuthoredRunFormatting({
|
|
164
|
+
context: nearestParagraphRunStyleContext(representation.paragraph, styleResolver),
|
|
161
165
|
marks: representation.node.marks,
|
|
162
166
|
styleResolver
|
|
163
167
|
});
|
|
@@ -190,12 +194,10 @@ const matchInlineProvenance = ({ state, targetSnapshot, revisionStamp, originalR
|
|
|
190
194
|
const planned = [];
|
|
191
195
|
for (const segment of matched) {
|
|
192
196
|
const liveFormatting = authoredFormattingAt({
|
|
193
|
-
doc: state.doc,
|
|
194
197
|
representation: segment.live,
|
|
195
198
|
styleResolver: liveStyleResolver
|
|
196
199
|
});
|
|
197
200
|
const targetFormatting = authoredFormattingAt({
|
|
198
|
-
doc: targetDocument,
|
|
199
201
|
representation: segment.target,
|
|
200
202
|
styleResolver: targetStyleResolver
|
|
201
203
|
});
|
|
@@ -220,11 +222,7 @@ const matchInlineProvenance = ({ state, targetSnapshot, revisionStamp, originalR
|
|
|
220
222
|
let nextRevisionId = revisionStamp.idSeed;
|
|
221
223
|
const changedTargetBlockIds = /* @__PURE__ */ new Set();
|
|
222
224
|
for (const change of planned.toReversed()) {
|
|
223
|
-
const context =
|
|
224
|
-
doc: state.doc,
|
|
225
|
-
pos: change.live.from,
|
|
226
|
-
styleResolver: liveStyleResolver
|
|
227
|
-
});
|
|
225
|
+
const context = nearestParagraphRunStyleContext(change.live.paragraph, liveStyleResolver);
|
|
228
226
|
const formattingMarks = reconcileRunFormattingMarks({
|
|
229
227
|
authoredFormatting: change.formatting,
|
|
230
228
|
context,
|
|
@@ -321,11 +319,9 @@ const sameAuthoredInlineProvenance = (baseSnapshot, targetSnapshot) => {
|
|
|
321
319
|
const baseStyleResolver = styleResolverOf(baseSnapshot);
|
|
322
320
|
const targetStyleResolver = styleResolverOf(targetSnapshot);
|
|
323
321
|
return matched.every(({ live, target }) => hyperlinkIdentity(live.node) === hyperlinkIdentity(target.node) && canonicalJson(authoredFormattingAt({
|
|
324
|
-
doc: baseDocument,
|
|
325
322
|
representation: live,
|
|
326
323
|
styleResolver: baseStyleResolver
|
|
327
324
|
})) === canonicalJson(authoredFormattingAt({
|
|
328
|
-
doc: targetDocument,
|
|
329
325
|
representation: target,
|
|
330
326
|
styleResolver: targetStyleResolver
|
|
331
327
|
})));
|
|
@@ -108,9 +108,22 @@ const boundaryParagraphEntriesOf = (document) => {
|
|
|
108
108
|
if (!children) return null;
|
|
109
109
|
return children.flatMap((child) => child.type === "paragraph" ? [child.paragraph] : []);
|
|
110
110
|
};
|
|
111
|
+
const hasSectionEndpoint = (document) => {
|
|
112
|
+
let found = false;
|
|
113
|
+
document.forEach((node) => {
|
|
114
|
+
if (node.type.name === "paragraph" && expectParagraphAttrs(node)._sectionProperties !== void 0) found = true;
|
|
115
|
+
});
|
|
116
|
+
return found;
|
|
117
|
+
};
|
|
111
118
|
/** Stage section deltas after document operations, against the accepted projection. */
|
|
112
119
|
const stageSectionBoundaryProperties = ({ state, target, originalRevisionIdSeed, revisionStamp, author, maxRanges, mapTargetProperties }) => {
|
|
113
120
|
if (!Number.isSafeInteger(maxRanges) || maxRanges < 0) return { status: "budget-exceeded" };
|
|
121
|
+
if (!hasSectionEndpoint(target)) return {
|
|
122
|
+
status: "matched",
|
|
123
|
+
transaction: state.tr,
|
|
124
|
+
nextRevisionId: revisionStamp.idSeed,
|
|
125
|
+
rangeCount: 0
|
|
126
|
+
};
|
|
114
127
|
const reviewedState = resolveAllChangesInHeadlessStateWithMapping(state, "accept");
|
|
115
128
|
const reviewed = boundaryParagraphEntriesOf(reviewedState.state.doc);
|
|
116
129
|
const targets = boundaryParagraphEntriesOf(target);
|
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
// @ts-nocheck -- generated and verified by build-shaper-wasm.ts
|
|
2
2
|
/* @ts-self-types="./text_shaper.d.ts" */
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the bidirectional levels and visual order of one line.
|
|
6
|
+
*
|
|
7
|
+
* `right_to_left` is the paragraph direction, or `None` for rules P2 and P3.
|
|
8
|
+
* Returns `[paragraphLevel, charCount, level * charCount, visualIndex *
|
|
9
|
+
* charCount]`, counting characters as Unicode scalar values.
|
|
10
|
+
* @param {string} text
|
|
11
|
+
* @param {boolean | null} [right_to_left]
|
|
12
|
+
* @returns {Int32Array}
|
|
13
|
+
*/
|
|
14
|
+
export function resolveBidi(text, right_to_left) {
|
|
15
|
+
try {
|
|
16
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
17
|
+
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
18
|
+
const len0 = WASM_VECTOR_LEN;
|
|
19
|
+
wasm.resolveBidi(retptr, ptr0, len0, isLikeNone(right_to_left) ? 0xFFFFFF : right_to_left ? 1 : 0);
|
|
20
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
21
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
22
|
+
var v2 = getArrayI32FromWasm0(r0, r1).slice();
|
|
23
|
+
wasm.__wbindgen_export3(r0, r1 * 4, 4);
|
|
24
|
+
return v2;
|
|
25
|
+
} finally {
|
|
26
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
4
30
|
/**
|
|
5
31
|
* Shape one run of text with one face.
|
|
6
32
|
*
|
|
Binary file
|
|
@@ -52,6 +52,12 @@ type ParagraphRunStyleContextAtOptions = {
|
|
|
52
52
|
styleResolver?: RunStyleResolver | null;
|
|
53
53
|
};
|
|
54
54
|
declare const paragraphRunStyleContextAt: ({ doc, pos, styleResolver }: ParagraphRunStyleContextAtOptions) => ParagraphRunStyleContext;
|
|
55
|
+
/**
|
|
56
|
+
* {@link paragraphRunStyleContextAt} for a caller that already knows the
|
|
57
|
+
* nearest paragraph, or that there is none. A document walk knows it for free,
|
|
58
|
+
* where `doc.resolve` re-descends from the root once per run.
|
|
59
|
+
*/
|
|
60
|
+
declare const nearestParagraphRunStyleContext: (paragraph: Node | null, styleResolver?: RunStyleResolver | null) => ParagraphRunStyleContext;
|
|
55
61
|
type ResolveEffectiveRunStyleFormattingOptions = {
|
|
56
62
|
marks: readonly Mark[];
|
|
57
63
|
paragraphFormatting: document_d_exports.TextFormatting | undefined;
|
|
@@ -60,4 +66,4 @@ type ResolveEffectiveRunStyleFormattingOptions = {
|
|
|
60
66
|
/** Resolve the non-direct run cascade from shared document styles and the current paragraph. */
|
|
61
67
|
declare const resolveEffectiveRunStyleFormatting: ({ marks, paragraphFormatting, styleResolver }: ResolveEffectiveRunStyleFormattingOptions) => document_d_exports.TextFormatting | undefined;
|
|
62
68
|
//#endregion
|
|
63
|
-
export { ParagraphRunStyleContext, RunStyleResolver, getParagraphMarkSuppressionOverrides, hasDirectRunFormatting, paragraphFormattingForRun, paragraphRunStyleContext, paragraphRunStyleContextAt, resolveEffectiveRunStyleFormatting, resolveParagraphBodyRunFormatting, stripParagraphMarkFormattingForBodyRuns, stripParagraphMarkOnlyFormatting, suppressParagraphMarkFormatting };
|
|
69
|
+
export { ParagraphRunStyleContext, RunStyleResolver, getParagraphMarkSuppressionOverrides, hasDirectRunFormatting, nearestParagraphRunStyleContext, paragraphFormattingForRun, paragraphRunStyleContext, paragraphRunStyleContextAt, resolveEffectiveRunStyleFormatting, resolveParagraphBodyRunFormatting, stripParagraphMarkFormattingForBodyRuns, stripParagraphMarkOnlyFormatting, suppressParagraphMarkFormatting };
|
|
@@ -153,12 +153,18 @@ const paragraphRunStyleContextAt = ({ doc, pos, styleResolver }) => {
|
|
|
153
153
|
const ancestor = resolved.node(depth);
|
|
154
154
|
if (ancestor.type.name === "paragraph") return paragraphRunStyleContext(ancestor, styleResolver);
|
|
155
155
|
}
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
return nearestParagraphRunStyleContext(null, styleResolver);
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* {@link paragraphRunStyleContextAt} for a caller that already knows the
|
|
160
|
+
* nearest paragraph, or that there is none. A document walk knows it for free,
|
|
161
|
+
* where `doc.resolve` re-descends from the root once per run.
|
|
162
|
+
*/
|
|
163
|
+
const nearestParagraphRunStyleContext = (paragraph, styleResolver) => paragraph ? paragraphRunStyleContext(paragraph, styleResolver) : {
|
|
164
|
+
baseParagraphFormatting: void 0,
|
|
165
|
+
paragraphFormatting: void 0,
|
|
166
|
+
paragraphMarkFormatting: void 0,
|
|
167
|
+
paragraphMarkPrecedesStyle: false
|
|
162
168
|
};
|
|
163
169
|
/** Resolve the non-direct run cascade from shared document styles and the current paragraph. */
|
|
164
170
|
const resolveEffectiveRunStyleFormatting = ({ marks, paragraphFormatting, styleResolver }) => {
|
|
@@ -176,4 +182,4 @@ const resolveEffectiveRunStyleFormatting = ({ marks, paragraphFormatting, styleR
|
|
|
176
182
|
});
|
|
177
183
|
};
|
|
178
184
|
//#endregion
|
|
179
|
-
export { getParagraphMarkSuppressionOverrides, hasDirectRunFormatting, paragraphFormattingForRun, paragraphRunStyleContext, paragraphRunStyleContextAt, resolveEffectiveRunStyleFormatting, resolveParagraphBodyRunFormatting, stripParagraphMarkFormattingForBodyRuns, stripParagraphMarkOnlyFormatting, suppressParagraphMarkFormatting };
|
|
185
|
+
export { getParagraphMarkSuppressionOverrides, hasDirectRunFormatting, nearestParagraphRunStyleContext, paragraphFormattingForRun, paragraphRunStyleContext, paragraphRunStyleContextAt, resolveEffectiveRunStyleFormatting, resolveParagraphBodyRunFormatting, stripParagraphMarkFormattingForBodyRuns, stripParagraphMarkOnlyFormatting, suppressParagraphMarkFormatting };
|
package/dist/shaping/shaper.d.ts
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
* the Unicode data that engine needs, and a document in Latin, Cyrillic or
|
|
15
15
|
* Greek needs none of it: `getShaper` is called only once a run in a shaping
|
|
16
16
|
* script is about to be measured or painted, and never before.
|
|
17
|
+
*
|
|
18
|
+
* The same artifact resolves the bidirectional algorithm, because splitting a
|
|
19
|
+
* line into runs that each read one way is what shaping needs first.
|
|
17
20
|
*/
|
|
18
21
|
/** Which way a run advances. Resolved by the producer, never guessed here. */
|
|
19
22
|
declare const SHAPING_DIRECTION: {
|
|
@@ -57,22 +60,62 @@ type ShapedRun = {
|
|
|
57
60
|
/** Design units per em, so a caller scales without re-reading the face. */
|
|
58
61
|
readonly unitsPerEm: number;
|
|
59
62
|
};
|
|
63
|
+
/** The paragraph direction of a line whose bidirectional levels are resolved. */
|
|
64
|
+
declare const BIDI_DIRECTION: {
|
|
65
|
+
readonly leftToRight: "ltr";
|
|
66
|
+
readonly rightToLeft: "rtl";
|
|
67
|
+
/** Rules P2 and P3: the first strong character outside an isolate decides. */
|
|
68
|
+
readonly auto: "auto";
|
|
69
|
+
};
|
|
70
|
+
type BidiDirection = (typeof BIDI_DIRECTION)[keyof typeof BIDI_DIRECTION];
|
|
71
|
+
type ResolveBidiRequest = {
|
|
72
|
+
/** One paragraph laid out as one line; a paragraph separator does not split it. */
|
|
73
|
+
readonly text: string;
|
|
74
|
+
readonly direction: BidiDirection;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* One line after the Unicode Bidirectional Algorithm (UAX #9), rules P2
|
|
78
|
+
* through L2. Indices count code points, not UTF-16 units.
|
|
79
|
+
*/
|
|
80
|
+
type BidiLine = {
|
|
81
|
+
/** 0 when the paragraph reads left to right, 1 when right to left. */
|
|
82
|
+
readonly paragraphLevel: number;
|
|
83
|
+
/**
|
|
84
|
+
* Embedding level per code point, after rule L1; odd levels read right to
|
|
85
|
+
* left. Characters rule X9 removes (explicit formatting characters such as
|
|
86
|
+
* isolates) carry a level but draw nothing.
|
|
87
|
+
*/
|
|
88
|
+
readonly levels: readonly number[];
|
|
89
|
+
/** Code-point indices in visual order, left to right (rule L2). */
|
|
90
|
+
readonly visualOrder: readonly number[];
|
|
91
|
+
};
|
|
60
92
|
type Shaper = {
|
|
61
93
|
readonly shapeRun: (request: ShapeRunRequest) => ShapedRun;
|
|
94
|
+
readonly resolveBidi: (request: ResolveBidiRequest) => BidiLine;
|
|
62
95
|
};
|
|
63
96
|
declare const ShaperError_base: import("better-result").TaggedErrorClass<"ShaperError">;
|
|
64
97
|
declare class ShaperError extends ShaperError_base<{
|
|
65
98
|
message: string;
|
|
66
99
|
cause?: unknown;
|
|
67
100
|
}> {}
|
|
101
|
+
/**
|
|
102
|
+
* Where the artifact comes from when the caller already holds it: a runtime
|
|
103
|
+
* that cannot fetch its own package files (a single-file executable, an
|
|
104
|
+
* embedded asset) hands over the WebAssembly bytes instead.
|
|
105
|
+
*/
|
|
106
|
+
type ShaperSource = {
|
|
107
|
+
readonly wasm: Uint8Array | ArrayBuffer;
|
|
108
|
+
};
|
|
68
109
|
/**
|
|
69
110
|
* Loads the shaper, once, and hands back the single implementation both the
|
|
70
111
|
* measurer and the PDF backend use.
|
|
71
112
|
*
|
|
72
|
-
* Calling this is what fetches the artifact, so a caller asks
|
|
73
|
-
* establishing that some run actually needs shaping.
|
|
113
|
+
* Calling this without a source is what fetches the artifact, so a caller asks
|
|
114
|
+
* for it only after establishing that some run actually needs shaping. With a
|
|
115
|
+
* source, the bytes given are instantiated and nothing is fetched. Either way
|
|
116
|
+
* the first successful load is the one every later call shares.
|
|
74
117
|
*/
|
|
75
|
-
declare const getShaper: () => Promise<Shaper>;
|
|
118
|
+
declare const getShaper: (source?: ShaperSource) => Promise<Shaper>;
|
|
76
119
|
/**
|
|
77
120
|
* Whether the artifact has been loaded in this process.
|
|
78
121
|
*
|
|
@@ -89,4 +132,4 @@ declare const shaperLoaded: () => boolean;
|
|
|
89
132
|
*/
|
|
90
133
|
declare const resolvedShaper: () => Shaper | null;
|
|
91
134
|
//#endregion
|
|
92
|
-
export { SHAPING_DIRECTION, ShapeRunRequest, ShapedGlyph, ShapedRun, Shaper, ShaperError, ShapingDirection, getShaper, resolvedShaper, shaperLoaded };
|
|
135
|
+
export { BIDI_DIRECTION, BidiDirection, BidiLine, ResolveBidiRequest, SHAPING_DIRECTION, ShapeRunRequest, ShapedGlyph, ShapedRun, Shaper, ShaperError, ShaperSource, ShapingDirection, getShaper, resolvedShaper, shaperLoaded };
|
package/dist/shaping/shaper.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import __wbg_init, { shapeRun } from "../generated/text_shaper.js";
|
|
1
|
+
import __wbg_init, { resolveBidi, shapeRun } from "../generated/text_shaper.js";
|
|
2
2
|
import { TaggedError } from "better-result";
|
|
3
3
|
//#region src/shaping/shaper.ts
|
|
4
4
|
/**
|
|
@@ -16,12 +16,22 @@ import { TaggedError } from "better-result";
|
|
|
16
16
|
* the Unicode data that engine needs, and a document in Latin, Cyrillic or
|
|
17
17
|
* Greek needs none of it: `getShaper` is called only once a run in a shaping
|
|
18
18
|
* script is about to be measured or painted, and never before.
|
|
19
|
+
*
|
|
20
|
+
* The same artifact resolves the bidirectional algorithm, because splitting a
|
|
21
|
+
* line into runs that each read one way is what shaping needs first.
|
|
19
22
|
*/
|
|
20
23
|
/** Which way a run advances. Resolved by the producer, never guessed here. */
|
|
21
24
|
const SHAPING_DIRECTION = {
|
|
22
25
|
leftToRight: "ltr",
|
|
23
26
|
rightToLeft: "rtl"
|
|
24
27
|
};
|
|
28
|
+
/** The paragraph direction of a line whose bidirectional levels are resolved. */
|
|
29
|
+
const BIDI_DIRECTION = {
|
|
30
|
+
leftToRight: "ltr",
|
|
31
|
+
rightToLeft: "rtl",
|
|
32
|
+
/** Rules P2 and P3: the first strong character outside an isolate decides. */
|
|
33
|
+
auto: "auto"
|
|
34
|
+
};
|
|
25
35
|
var ShaperError = class extends TaggedError("ShaperError") {};
|
|
26
36
|
/**
|
|
27
37
|
* The flat buffer the WebAssembly entry returns:
|
|
@@ -51,26 +61,47 @@ const decode = (buffer) => {
|
|
|
51
61
|
unitsPerEm
|
|
52
62
|
};
|
|
53
63
|
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
/** `[paragraphLevel, count, level * count, visualIndex * count]`. */
|
|
65
|
+
const decodeBidi = (buffer) => {
|
|
66
|
+
const count = buffer[1] ?? 0;
|
|
67
|
+
const levelsAt = HEADER_FIELDS;
|
|
68
|
+
const orderAt = HEADER_FIELDS + count;
|
|
69
|
+
return {
|
|
70
|
+
paragraphLevel: buffer[0] ?? 0,
|
|
71
|
+
levels: Array.from(buffer.subarray(levelsAt, orderAt)),
|
|
72
|
+
visualOrder: Array.from(buffer.subarray(orderAt, orderAt + count))
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
const RIGHT_TO_LEFT_BY_BIDI_DIRECTION = {
|
|
76
|
+
ltr: false,
|
|
77
|
+
rtl: true,
|
|
78
|
+
auto: void 0
|
|
79
|
+
};
|
|
80
|
+
const shaper = {
|
|
81
|
+
resolveBidi: ({ text, direction }) => decodeBidi(resolveBidi(text, RIGHT_TO_LEFT_BY_BIDI_DIRECTION[direction])),
|
|
82
|
+
shapeRun: ({ font, faceIndex = 0, text, direction, script = "", language = "", featuresOn = [], featuresOff = [] }) => {
|
|
83
|
+
try {
|
|
84
|
+
return decode(shapeRun(font, faceIndex, text, direction === SHAPING_DIRECTION.rightToLeft, script, language, [...featuresOn], [...featuresOff]));
|
|
85
|
+
} catch (cause) {
|
|
86
|
+
throw new ShaperError({
|
|
87
|
+
message: "Could not shape a run of text",
|
|
88
|
+
cause
|
|
89
|
+
});
|
|
90
|
+
}
|
|
62
91
|
}
|
|
63
|
-
}
|
|
92
|
+
};
|
|
64
93
|
let loading;
|
|
65
94
|
/**
|
|
66
95
|
* Loads the shaper, once, and hands back the single implementation both the
|
|
67
96
|
* measurer and the PDF backend use.
|
|
68
97
|
*
|
|
69
|
-
* Calling this is what fetches the artifact, so a caller asks
|
|
70
|
-
* establishing that some run actually needs shaping.
|
|
98
|
+
* Calling this without a source is what fetches the artifact, so a caller asks
|
|
99
|
+
* for it only after establishing that some run actually needs shaping. With a
|
|
100
|
+
* source, the bytes given are instantiated and nothing is fetched. Either way
|
|
101
|
+
* the first successful load is the one every later call shares.
|
|
71
102
|
*/
|
|
72
|
-
const getShaper = () => {
|
|
73
|
-
loading ??= __wbg_init().then(() => {
|
|
103
|
+
const getShaper = (source) => {
|
|
104
|
+
loading ??= __wbg_init(source === void 0 ? void 0 : { module_or_path: source.wasm }).then(() => {
|
|
74
105
|
resolved = shaper;
|
|
75
106
|
return shaper;
|
|
76
107
|
}).catch((cause) => {
|
|
@@ -99,4 +130,4 @@ let resolved = null;
|
|
|
99
130
|
*/
|
|
100
131
|
const resolvedShaper = () => resolved;
|
|
101
132
|
//#endregion
|
|
102
|
-
export { SHAPING_DIRECTION, ShaperError, getShaper, resolvedShaper, shaperLoaded };
|
|
133
|
+
export { BIDI_DIRECTION, SHAPING_DIRECTION, ShaperError, getShaper, resolvedShaper, shaperLoaded };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { BIDI_DIRECTION, BidiLine, ResolveBidiRequest, SHAPING_DIRECTION, ShapeRunRequest, ShapedRun, Shaper, ShaperSource, getShaper } from "./shaping/shaper.js";
|
|
2
|
+
import { SfntFont, parseSfnt } from "./fonts/sfnt/parse.js";
|
|
3
|
+
import { TrueTypeSubset, subsetTrueType } from "./fonts/sfnt/subset.js";
|
|
4
|
+
export { BIDI_DIRECTION, type BidiLine, type ResolveBidiRequest, SHAPING_DIRECTION, type SfntFont, type ShapeRunRequest, type ShapedRun, type Shaper, type ShaperSource, type TrueTypeSubset, getShaper, parseSfnt, subsetTrueType };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { parseSfnt } from "./fonts/sfnt/parse.js";
|
|
2
|
+
import { subsetTrueType } from "./fonts/sfnt/subset.js";
|
|
3
|
+
import { BIDI_DIRECTION, SHAPING_DIRECTION, getShaper } from "./shaping/shaper.js";
|
|
4
|
+
export { BIDI_DIRECTION, SHAPING_DIRECTION, getShaper, parseSfnt, subsetTrueType };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"document-model",
|
|
@@ -106,6 +106,11 @@
|
|
|
106
106
|
"types": "./dist/docx/documentClone.d.ts",
|
|
107
107
|
"import": "./dist/docx/documentClone.js"
|
|
108
108
|
},
|
|
109
|
+
"./text-shaping": {
|
|
110
|
+
"types": "./dist/text-shaping.d.ts",
|
|
111
|
+
"import": "./dist/text-shaping.js"
|
|
112
|
+
},
|
|
113
|
+
"./text-shaping/wasm": "./dist/generated/text_shaper_bg.wasm",
|
|
109
114
|
"./docx/paragraphPropertySource": null,
|
|
110
115
|
"./internal/*": null,
|
|
111
116
|
"./*": {
|