@stll/folio-core 0.50.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.
@@ -18,9 +18,20 @@ const isTrackedSectionEndpointRemovalMeta = (value) => typeof value === "object"
18
18
  function countDocumentStructure(doc) {
19
19
  let paragraphs = 0;
20
20
  let sectionEndpoints = 0;
21
+ const blockRecords = [];
21
22
  const endpointRecords = [];
22
23
  const visit = (parent, parentPath) => {
23
24
  parent.forEach((node, _offset, index) => {
25
+ const path = parentPath.length === 0 ? `${index}` : `${parentPath}.${index}`;
26
+ blockRecords.push(node.type.name === "paragraph" ? [
27
+ path,
28
+ node.type.name,
29
+ node.attrs["paraId"] ?? null
30
+ ] : [
31
+ path,
32
+ node.type.name,
33
+ canonicalJson(node.attrs)
34
+ ]);
24
35
  if (node.type.name === "paragraph") {
25
36
  paragraphs++;
26
37
  const sectionProperties = sectionPropertiesOf(node);
@@ -34,34 +45,36 @@ function countDocumentStructure(doc) {
34
45
  }
35
46
  return;
36
47
  }
37
- if (node.childCount > 0) {
38
- const path = parentPath.length === 0 ? `${index}` : `${parentPath}.${index}`;
39
- visit(node, path);
40
- }
48
+ if (node.childCount > 0) visit(node, path);
41
49
  });
42
50
  };
43
51
  visit(doc, "");
44
52
  return {
53
+ blockStructureFingerprint: JSON.stringify(blockRecords),
45
54
  paragraphs,
46
55
  sectionEndpoints,
47
56
  sectionEndpointFingerprint: canonicalJson(endpointRecords)
48
57
  };
49
58
  }
59
+ const isUsableParaId = (value) => typeof value === "string" && value.length > 0 && value !== "00000000";
50
60
  /**
51
61
  * Collect paraIds of all paragraphs that overlap with the given range
52
62
  */
53
63
  function collectAffectedParaIds(doc, from, to) {
54
64
  const ids = /* @__PURE__ */ new Set();
65
+ const positions = /* @__PURE__ */ new Set();
55
66
  let hasUntracked = false;
56
- doc.nodesBetween(from, to, (node) => {
67
+ doc.nodesBetween(from, to, (node, pos) => {
57
68
  if (node.type.name === "paragraph") {
69
+ positions.add(pos);
58
70
  const paraId = node.attrs["paraId"];
59
- if (paraId) ids.add(paraId);
71
+ if (isUsableParaId(paraId)) ids.add(paraId);
60
72
  else hasUntracked = true;
61
73
  }
62
74
  });
63
75
  return {
64
76
  ids,
77
+ positions,
65
78
  hasUntracked
66
79
  };
67
80
  }
@@ -75,28 +88,40 @@ function collectAffectedParaIdsFromMarkLikeStep(doc, from, to) {
75
88
  const hi = Math.max(from, to);
76
89
  const primary = collectAffectedParaIds(doc, lo, hi > lo ? hi : lo + 1);
77
90
  if (primary.ids.size > 0 || primary.hasUntracked) return primary;
78
- try {
79
- const $p = doc.resolve(lo);
80
- for (let d = $p.depth; d >= 0; d--) {
81
- const n = $p.node(d);
82
- if (n.type.name === "paragraph") {
83
- const paraId = n.attrs["paraId"];
84
- if (paraId) return {
85
- ids: /* @__PURE__ */ new Set([paraId]),
86
- hasUntracked: false
87
- };
88
- return {
89
- ids: /* @__PURE__ */ new Set(),
90
- hasUntracked: true
91
- };
92
- }
91
+ const $p = doc.resolve(lo);
92
+ for (let depth = $p.depth; depth > 0; depth--) {
93
+ const node = $p.node(depth);
94
+ if (node.type.name === "paragraph") {
95
+ const paraId = node.attrs["paraId"];
96
+ return {
97
+ ids: new Set(isUsableParaId(paraId) ? [paraId] : []),
98
+ positions: /* @__PURE__ */ new Set([$p.before(depth)]),
99
+ hasUntracked: !isUsableParaId(paraId)
100
+ };
93
101
  }
94
- } catch {}
102
+ }
95
103
  return {
96
104
  ids: /* @__PURE__ */ new Set(),
105
+ positions: /* @__PURE__ */ new Set(),
97
106
  hasUntracked: false
98
107
  };
99
108
  }
109
+ /** Map an edited paragraph's interior so markup replacement retains its ownership. */
110
+ const mapParagraphPosition = (tr, position) => {
111
+ const mapped = tr.mapping.mapResult(position + 1, 1);
112
+ if (mapped.deletedAcross) return null;
113
+ const $position = tr.doc.resolve(mapped.pos);
114
+ for (let depth = $position.depth; depth > 0; depth--) if ($position.node(depth).type.name === "paragraph") return $position.before(depth);
115
+ return null;
116
+ };
117
+ const mapAffectedParagraphPositions = (tr, positions) => {
118
+ const mappedPositions = /* @__PURE__ */ new Set();
119
+ for (const position of positions) {
120
+ const mapped = mapParagraphPosition(tr, position);
121
+ if (mapped !== null) mappedPositions.add(mapped);
122
+ }
123
+ return mappedPositions;
124
+ };
100
125
  function mapStepPosition(remap, pos, assoc) {
101
126
  return remap.map(pos, assoc);
102
127
  }
@@ -107,6 +132,9 @@ function createParagraphChangeTrackerPlugin() {
107
132
  init(_config, state) {
108
133
  const counts = countDocumentStructure(state.doc);
109
134
  return {
135
+ affectedParagraphPositions: /* @__PURE__ */ new Set(),
136
+ hasUntrackedSourceChanges: false,
137
+ blockStructureFingerprint: counts.blockStructureFingerprint,
110
138
  changedParaIds: /* @__PURE__ */ new Set(),
111
139
  structuralChange: false,
112
140
  hasUntrackedChanges: false,
@@ -123,6 +151,9 @@ function createParagraphChangeTrackerPlugin() {
123
151
  if (meta === CLEAR_META) {
124
152
  const counts = countDocumentStructure(tr.doc);
125
153
  return {
154
+ affectedParagraphPositions: /* @__PURE__ */ new Set(),
155
+ hasUntrackedSourceChanges: false,
156
+ blockStructureFingerprint: counts.blockStructureFingerprint,
126
157
  changedParaIds: /* @__PURE__ */ new Set(),
127
158
  structuralChange: false,
128
159
  hasUntrackedChanges: false,
@@ -133,13 +164,22 @@ function createParagraphChangeTrackerPlugin() {
133
164
  };
134
165
  }
135
166
  if (meta === IGNORE_META) {
136
- const counts = tr.docChanged ? countDocumentStructure(tr.doc) : {
137
- paragraphs: prevState.paragraphCount,
138
- sectionEndpoints: prevState.sectionEndpointCount,
139
- sectionEndpointFingerprint: prevState.sectionEndpointFingerprint
140
- };
167
+ if (!tr.docChanged) return prevState;
168
+ const counts = countDocumentStructure(tr.doc);
169
+ const affectedParagraphPositions = mapAffectedParagraphPositions(tr, prevState.affectedParagraphPositions);
170
+ const changedParaIds = /* @__PURE__ */ new Set();
171
+ let unresolvedChanges = prevState.hasUntrackedSourceChanges;
172
+ for (const position of affectedParagraphPositions) {
173
+ const paraId = tr.doc.nodeAt(position)?.attrs["paraId"];
174
+ if (isUsableParaId(paraId)) changedParaIds.add(paraId);
175
+ else unresolvedChanges = true;
176
+ }
141
177
  return {
142
178
  ...prevState,
179
+ affectedParagraphPositions,
180
+ changedParaIds,
181
+ hasUntrackedChanges: unresolvedChanges,
182
+ blockStructureFingerprint: counts.blockStructureFingerprint,
143
183
  paragraphCount: counts.paragraphs,
144
184
  sectionEndpointCount: counts.sectionEndpoints,
145
185
  sectionEndpointRemoval: counts.sectionEndpointFingerprint === prevState.sectionEndpointFingerprint ? prevState.sectionEndpointRemoval : null,
@@ -162,6 +202,9 @@ function createParagraphChangeTrackerPlugin() {
162
202
  };
163
203
  } else sectionEndpointRemoval = null;
164
204
  const newState = {
205
+ affectedParagraphPositions: mapAffectedParagraphPositions(tr, prevState.affectedParagraphPositions),
206
+ hasUntrackedSourceChanges: prevState.hasUntrackedSourceChanges,
207
+ blockStructureFingerprint: counts.blockStructureFingerprint,
165
208
  changedParaIds: new Set(prevState.changedParaIds),
166
209
  structuralChange: prevState.structuralChange || meta === STRUCTURAL_META,
167
210
  hasUntrackedChanges: prevState.hasUntrackedChanges,
@@ -176,12 +219,13 @@ function createParagraphChangeTrackerPlugin() {
176
219
  const from = mapStepPosition(remap, range.from, 1);
177
220
  const to = mapStepPosition(remap, range.to, -1);
178
221
  if (to <= from) continue;
179
- const { ids, hasUntracked } = collectAffectedParaIdsFromMarkLikeStep(tr.doc, from, to);
222
+ const { ids, positions, hasUntracked } = collectAffectedParaIdsFromMarkLikeStep(tr.doc, from, to);
223
+ for (const position of positions) newState.affectedParagraphPositions.add(position);
180
224
  for (const id of ids) newState.changedParaIds.add(id);
181
225
  if (hasUntracked) newState.hasUntrackedChanges = true;
182
226
  }
183
227
  }
184
- if (prevState.paragraphCount !== newCount) newState.structuralChange = true;
228
+ if (prevState.blockStructureFingerprint !== counts.blockStructureFingerprint) newState.structuralChange = true;
185
229
  for (let stepIndex = 0; stepIndex < tr.steps.length; stepIndex++) {
186
230
  const step = tr.steps[stepIndex];
187
231
  const remap = tr.mapping.slice(stepIndex + 1);
@@ -189,7 +233,8 @@ function createParagraphChangeTrackerPlugin() {
189
233
  const from = mapStepPosition(remap, step.from, 1);
190
234
  const to = mapStepPosition(remap, step.to, -1);
191
235
  if (to <= from) continue;
192
- const { ids, hasUntracked } = collectAffectedParaIdsFromMarkLikeStep(tr.doc, from, to);
236
+ const { ids, positions, hasUntracked } = collectAffectedParaIdsFromMarkLikeStep(tr.doc, from, to);
237
+ for (const position of positions) newState.affectedParagraphPositions.add(position);
193
238
  for (const id of ids) newState.changedParaIds.add(id);
194
239
  if (hasUntracked) newState.hasUntrackedChanges = true;
195
240
  continue;
@@ -199,7 +244,8 @@ function createParagraphChangeTrackerPlugin() {
199
244
  const node = tr.doc.nodeAt(pos);
200
245
  if (!node) continue;
201
246
  const end = pos + node.nodeSize;
202
- const { ids, hasUntracked } = collectAffectedParaIds(tr.doc, pos, end);
247
+ const { ids, positions, hasUntracked } = collectAffectedParaIds(tr.doc, pos, end);
248
+ for (const position of positions) newState.affectedParagraphPositions.add(position);
203
249
  for (const id of ids) newState.changedParaIds.add(id);
204
250
  if (hasUntracked) newState.hasUntrackedChanges = true;
205
251
  continue;
@@ -208,11 +254,23 @@ function createParagraphChangeTrackerPlugin() {
208
254
  const from = mapStepPosition(remap, newStart, 1);
209
255
  const to = mapStepPosition(remap, newEnd, -1);
210
256
  if (to < from) return;
211
- const { ids, hasUntracked } = collectAffectedParaIds(tr.doc, from, to);
257
+ const { ids, positions, hasUntracked } = collectAffectedParaIds(tr.doc, from, to);
258
+ for (const position of positions) newState.affectedParagraphPositions.add(position);
212
259
  for (const id of ids) newState.changedParaIds.add(id);
213
260
  if (hasUntracked) newState.hasUntrackedChanges = true;
214
261
  });
215
262
  }
263
+ if (newState.hasUntrackedChanges || prevState.blockStructureFingerprint !== counts.blockStructureFingerprint) tr.before.descendants((node, position) => {
264
+ if (node.type.name !== "paragraph") return true;
265
+ if (!isUsableParaId(node.attrs["paraId"])) {
266
+ const mapped = mapParagraphPosition(tr, position);
267
+ if (mapped === null || newState.affectedParagraphPositions.has(mapped)) {
268
+ newState.hasUntrackedSourceChanges = true;
269
+ newState.hasUntrackedChanges = true;
270
+ }
271
+ }
272
+ return false;
273
+ });
216
274
  return newState;
217
275
  }
218
276
  }
@@ -231,7 +289,7 @@ function getChangedParagraphIds(state) {
231
289
  return getChangeTrackerState(state)?.changedParaIds ?? /* @__PURE__ */ new Set();
232
290
  }
233
291
  /**
234
- * Check if structural changes (paragraph add/delete) occurred
292
+ * Check if paragraph membership, order, or block structure changed
235
293
  */
236
294
  function hasStructuralChanges(state) {
237
295
  return getChangeTrackerState(state)?.structuralChange ?? false;
@@ -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
- baseParagraphFormatting: void 0,
158
- paragraphFormatting: void 0,
159
- paragraphMarkFormatting: void 0,
160
- paragraphMarkPrecedesStyle: false
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 };
@@ -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 for it only after
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 };
@@ -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
- const shaper = { shapeRun: ({ font, faceIndex = 0, text, direction, script = "", language = "", featuresOn = [], featuresOff = [] }) => {
55
- try {
56
- return decode(shapeRun(font, faceIndex, text, direction === SHAPING_DIRECTION.rightToLeft, script, language, [...featuresOn], [...featuresOff]));
57
- } catch (cause) {
58
- throw new ShaperError({
59
- message: "Could not shape a run of text",
60
- cause
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 for it only after
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.50.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
  "./*": {