@stll/folio-core 0.37.2 → 0.37.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/ai-edits/table-cell-mutations.js +10 -5
  2. package/dist/ai-edits/table-template.js +19 -5
  3. package/dist/controller/hiddenEditorManager.js +11 -0
  4. package/dist/controller/layoutPipeline.js +1 -1
  5. package/dist/display-list/build/textBoxPrimitives.js +34 -1
  6. package/dist/display-list/dom/renderDisplayListToDom.js +5 -2
  7. package/dist/display-list/types.d.ts +8 -4
  8. package/dist/docx/paragraphPropertySource.d.ts +82 -2
  9. package/dist/docx/paragraphPropertySource.js +569 -3
  10. package/dist/docx/parser.js +9 -0
  11. package/dist/docx/server/materializeYjsDocx.d.ts +1 -1
  12. package/dist/docx/server/materializeYjsDocx.js +21 -2
  13. package/dist/headless-layout.js +1 -1
  14. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +7 -1
  15. package/dist/layout-bridge/convert/headerFooterLayout.js +20 -3
  16. package/dist/layout-bridge/convert/toFlowBlocks.js +43 -12
  17. package/dist/layout-engine/index.js +20 -10
  18. package/dist/layout-engine/measure/measureBlocks.d.ts +7 -3
  19. package/dist/layout-engine/measure/measureBlocks.js +10 -7
  20. package/dist/layout-engine/measure/measureParagraph.d.ts +2 -0
  21. package/dist/layout-engine/measure/measureParagraph.js +1 -1
  22. package/dist/layout-engine/paginator.d.ts +1 -1
  23. package/dist/layout-engine/paginator.js +41 -4
  24. package/dist/layout-engine/textBoxFlow.d.ts +2 -0
  25. package/dist/layout-engine/textBoxFlow.js +9 -5
  26. package/dist/layout-engine/types.d.ts +6 -0
  27. package/dist/layout-painter/documentColors.d.ts +10 -1
  28. package/dist/layout-painter/documentColors.js +16 -1
  29. package/dist/layout-painter/renderParagraph.js +9 -24
  30. package/dist/layout-painter/renderTable.js +3 -3
  31. package/dist/layout-painter/renderTextBox.js +2 -1
  32. package/dist/pdf/pageSpace.d.ts +12 -1
  33. package/dist/pdf/pageSpace.js +24 -1
  34. package/dist/pdf/paint.js +2 -2
  35. package/dist/prosemirror/attrs/index.js +32 -8
  36. package/dist/prosemirror/commands/comments.js +12 -3
  37. package/dist/prosemirror/commands/tableCellMergeResolution.js +18 -11
  38. package/dist/prosemirror/conversion/fromProseDoc.js +114 -25
  39. package/dist/prosemirror/conversion/toProseDoc.js +101 -8
  40. package/dist/prosemirror/extensions/core/DocExtension.js +2 -0
  41. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -0
  42. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +5 -1
  43. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +132 -41
  44. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -61
  45. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +16 -0
  46. package/dist/prosemirror/schema/nodes.d.ts +14 -2
  47. package/dist/prosemirror/schema/nodes.js +7 -0
  48. package/dist/prosemirror/yjsParagraphSourceContract.d.ts +9 -0
  49. package/dist/prosemirror/yjsParagraphSourceContract.js +26 -0
  50. package/dist/utils/rotationBoundingBox.d.ts +5 -1
  51. package/dist/utils/rotationBoundingBox.js +9 -1
  52. package/package.json +2 -2
@@ -1,7 +1,8 @@
1
- import { recreateProseNodeWithParagraphPropertySource, setProseParagraphMarkupWithPropertySource, transferProseParagraphPropertySource } from "../../../docx/paragraphPropertySource.js";
1
+ import { PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR, getExplicitParagraphPropertySourceTransfers, getProseDocumentParagraphPropertySourceContract, getProseParagraphPropertySourceToken, paragraphPropertySourceTokenMatchesContract, recreateProseNodeWithParagraphPropertySource, setProseParagraphMarkupWithPropertySource, transferProseParagraphPropertySource } from "../../../docx/paragraphPropertySource.js";
2
2
  import { deterministicHexId, generateHexId } from "../../../utils/hexId.js";
3
3
  import { createExtension } from "../create.js";
4
4
  import { ignoreTrackedChanges } from "./ParagraphChangeTrackerExtension.js";
5
+ import { panic } from "better-result";
5
6
  import { Fragment } from "prosemirror-model";
6
7
  import { Plugin, PluginKey } from "prosemirror-state";
7
8
  //#region src/prosemirror/extensions/features/ParaIdAllocatorExtension.ts
@@ -73,61 +74,100 @@ const mintParaId = (taken, seed, pos) => {
73
74
  return id;
74
75
  };
75
76
  const isUsableParaId = (value) => typeof value === "string" && value.length > 0 && value !== "00000000";
76
- /**
77
- * For every valid paraId in the pre-transaction doc, the position its
78
- * paragraph ends up at after `transactions` — the occurrence that
79
- * rightfully keeps the id when the new doc holds duplicates. Ids whose
80
- * paragraph start was deleted by the steps are left out.
81
- */
82
- const mapKeeperPositions = (oldDoc, transactions) => {
83
- const keepers = /* @__PURE__ */ new Map();
77
+ const collectParagraphPropertySourceSeed = (doc) => {
78
+ const contract = getProseDocumentParagraphPropertySourceContract(doc);
79
+ if (!contract) return {
80
+ contract: null,
81
+ tokens: /* @__PURE__ */ new Set()
82
+ };
83
+ const counts = /* @__PURE__ */ new Map();
84
+ doc.descendants((node) => {
85
+ if (node.type.name !== "paragraph") return true;
86
+ const token = getProseParagraphPropertySourceToken(node);
87
+ if (paragraphPropertySourceTokenMatchesContract(token, contract)) counts.set(token, (counts.get(token) ?? 0) + 1);
88
+ return false;
89
+ });
90
+ return {
91
+ contract,
92
+ tokens: new Set([...counts].filter(([, count]) => count === 1).map(([token]) => token))
93
+ };
94
+ };
95
+ const mapParagraphKeepers = (oldDoc, transactions, sourceSeed) => {
96
+ const paraIds = /* @__PURE__ */ new Map();
97
+ const sourceTokens = /* @__PURE__ */ new Map();
84
98
  oldDoc.descendants((node, pos) => {
85
99
  if (node.type.name !== "paragraph") return true;
86
100
  const id = node.attrs["paraId"];
87
- if (!isUsableParaId(id) || keepers.has(id)) return false;
101
+ const token = getProseParagraphPropertySourceToken(node);
102
+ const mapsParaId = isUsableParaId(id) && !paraIds.has(id);
103
+ const mapsSourceToken = typeof token === "string" && sourceSeed.tokens.has(token);
104
+ if (!mapsParaId && !mapsSourceToken) return false;
88
105
  let mapped = pos;
106
+ let mappedInterior = pos + 1;
89
107
  let deleted = false;
90
- for (const tr of transactions) {
91
- const result = tr.mapping.mapResult(mapped);
92
- if (result.deleted) {
93
- deleted = true;
94
- break;
95
- }
96
- mapped = result.pos;
108
+ for (const transaction of transactions) {
109
+ const ownerResult = transaction.mapping.mapResult(mapped);
110
+ const interiorResult = transaction.mapping.mapResult(mappedInterior, -1);
111
+ deleted ||= interiorResult.deletedAcross;
112
+ mapped = ownerResult.pos;
113
+ mappedInterior = interiorResult.pos;
97
114
  }
98
- if (!deleted) keepers.set(id, mapped);
115
+ if (mapsParaId && !deleted) paraIds.set(id, mapped);
116
+ if (mapsSourceToken) sourceTokens.set(token, sourceTokens.has(token) ? { status: "ambiguous" } : {
117
+ pos: mapped,
118
+ status: deleted ? "deleted" : "mapped"
119
+ });
99
120
  return false;
100
121
  });
101
- return keepers;
122
+ return {
123
+ paraIds,
124
+ sourceTokens
125
+ };
102
126
  };
103
- const collectParaIdUpdates = (doc, keeperPositions, deterministicSeed = null) => {
104
- const missing = [];
105
- const occurrencesById = /* @__PURE__ */ new Map();
127
+ const collectParagraphCensus = (doc, sourceSeed) => {
128
+ const census = {
129
+ invalidSourceTokens: [],
130
+ missingParaIds: [],
131
+ paragraphs: /* @__PURE__ */ new Map(),
132
+ paraIds: /* @__PURE__ */ new Map(),
133
+ sourceTokens: /* @__PURE__ */ new Map()
134
+ };
135
+ const contractMatchesSeed = sourceSeed?.contract !== null && sourceSeed?.contract !== void 0 && getProseDocumentParagraphPropertySourceContract(doc) === sourceSeed.contract;
106
136
  doc.descendants((node, pos) => {
107
137
  if (node.type.name !== "paragraph") return true;
108
- const id = node.attrs["paraId"];
109
- if (!isUsableParaId(id)) missing.push({
138
+ const occurrence = {
110
139
  pos,
111
140
  attrs: node.attrs
112
- });
113
- else {
114
- const occurrences = occurrencesById.get(id) ?? [];
115
- occurrences.push({
116
- pos,
117
- attrs: node.attrs
118
- });
119
- occurrencesById.set(id, occurrences);
141
+ };
142
+ census.paragraphs.set(pos, occurrence);
143
+ const id = node.attrs["paraId"];
144
+ if (isUsableParaId(id)) {
145
+ const occurrences = census.paraIds.get(id) ?? [];
146
+ occurrences.push(occurrence);
147
+ census.paraIds.set(id, occurrences);
148
+ } else census.missingParaIds.push(occurrence);
149
+ if (sourceSeed) {
150
+ const token = getProseParagraphPropertySourceToken(node);
151
+ if (token !== null && token !== void 0) if (typeof token !== "string" || !contractMatchesSeed || !sourceSeed.contract || !sourceSeed.tokens.has(token) || !paragraphPropertySourceTokenMatchesContract(token, sourceSeed.contract)) census.invalidSourceTokens.push(occurrence);
152
+ else {
153
+ const occurrences = census.sourceTokens.get(token) ?? [];
154
+ occurrences.push(occurrence);
155
+ census.sourceTokens.set(token, occurrences);
156
+ }
120
157
  }
121
158
  return false;
122
159
  });
123
- const needFreshId = [...missing];
124
- for (const [id, occurrences] of occurrencesById) {
160
+ return census;
161
+ };
162
+ const collectParaIdUpdatesFromCensus = (census, keeperPositions, deterministicSeed = null) => {
163
+ const needFreshId = [...census.missingParaIds];
164
+ for (const [id, occurrences] of census.paraIds) {
125
165
  if (occurrences.length === 1) continue;
126
166
  const keeperPos = keeperPositions?.get(id);
127
- const keeper = occurrences.find((occurrence) => occurrence.pos === keeperPos) ?? occurrences[0];
167
+ const keeper = occurrences.find(({ pos }) => pos === keeperPos) ?? occurrences[0];
128
168
  for (const occurrence of occurrences) if (occurrence !== keeper) needFreshId.push(occurrence);
129
169
  }
130
- const taken = new Set(occurrencesById.keys());
170
+ const taken = new Set(census.paraIds.keys());
131
171
  const updates = [];
132
172
  for (const { pos, attrs } of needFreshId) {
133
173
  const newId = mintParaId(taken, deterministicSeed, pos);
@@ -140,9 +180,39 @@ const collectParaIdUpdates = (doc, keeperPositions, deterministicSeed = null) =>
140
180
  }
141
181
  });
142
182
  }
143
- updates.sort((a, b) => a.pos - b.pos);
183
+ updates.sort((left, right) => left.pos - right.pos);
144
184
  return updates;
145
185
  };
186
+ const collectParaIdUpdates = (doc) => collectParaIdUpdatesFromCensus(collectParagraphCensus(doc));
187
+ const collectParagraphPropertySourceUpdates = (census, keeperPositions, transactions) => {
188
+ const updates = /* @__PURE__ */ new Map();
189
+ for (const { pos } of census.invalidSourceTokens) updates.set(pos, null);
190
+ const explicitTransfers = transactions.flatMap((transaction) => [...getExplicitParagraphPropertySourceTransfers(transaction)]);
191
+ const explicitlySelected = new Set(explicitTransfers.map(({ selectedToken }) => selectedToken).filter((token) => token !== null));
192
+ const explicitlyDisplaced = new Set(explicitTransfers.map(({ displacedToken }) => displacedToken).filter((token) => token !== null));
193
+ const keptPositions = /* @__PURE__ */ new Set();
194
+ const keptTokens = /* @__PURE__ */ new Set();
195
+ for (const [token, occurrences] of census.sourceTokens) {
196
+ const mappedOwner = keeperPositions.get(token);
197
+ const mappedKeeper = mappedOwner?.status === "mapped" ? occurrences.find(({ pos }) => pos === mappedOwner.pos) : void 0;
198
+ const transferredKeeper = !mappedKeeper && explicitlySelected.has(token) && occurrences.length === 1 ? occurrences.at(0) : void 0;
199
+ const detachedKeeper = !mappedKeeper && !transferredKeeper && occurrences.length === 1 && (mappedOwner === void 0 || mappedOwner.status === "deleted" && mappedOwner.pos === occurrences.at(0)?.pos) ? occurrences.at(0) : void 0;
200
+ const keeper = mappedKeeper ?? transferredKeeper ?? detachedKeeper;
201
+ if (keeper) {
202
+ keptPositions.add(keeper.pos);
203
+ keptTokens.add(token);
204
+ }
205
+ for (const occurrence of occurrences) if (occurrence !== keeper) updates.set(occurrence.pos, null);
206
+ }
207
+ for (const [token, mappedOwner] of keeperPositions) {
208
+ if (mappedOwner.status !== "mapped" || keptTokens.has(token) || explicitlyDisplaced.has(token) || keptPositions.has(mappedOwner.pos) || !census.paragraphs.has(mappedOwner.pos)) continue;
209
+ updates.set(mappedOwner.pos, token);
210
+ }
211
+ return [...updates].map(([pos, token]) => ({
212
+ pos,
213
+ token
214
+ })).sort((left, right) => left.pos - right.pos);
215
+ };
146
216
  const collectInitialParaIds = (doc) => {
147
217
  let needsRewrite = false;
148
218
  const taken = /* @__PURE__ */ new Set();
@@ -215,12 +285,20 @@ const ensureParaIdsInState = (state) => {
215
285
  };
216
286
  const createParaIdAllocatorPlugin = () => new Plugin({
217
287
  key: paraIdAllocatorKey,
288
+ state: {
289
+ init: (_config, state) => collectParagraphPropertySourceSeed(state.doc),
290
+ apply: (_transaction, seed) => seed
291
+ },
218
292
  appendTransaction(transactions, oldState, newState) {
219
293
  if (!transactions.some((t) => t.docChanged)) return null;
220
- const keeperPositions = mapKeeperPositions(oldState.doc, transactions);
221
- const seed = transactions.map((transaction) => transaction.getMeta(deterministicParaIdSeedKey)).find((value) => typeof value === "string") ?? null;
222
- const updates = collectParaIdUpdates(newState.doc, keeperPositions, seed);
223
- if (updates.length === 0) return null;
294
+ const paragraphSourceSeed = paraIdAllocatorKey.getState(oldState);
295
+ if (!paragraphSourceSeed) panic("ParaId allocator lost its paragraph-property source seed");
296
+ const keeperPositions = mapParagraphKeepers(oldState.doc, transactions, paragraphSourceSeed);
297
+ const deterministicSeed = transactions.map((transaction) => transaction.getMeta(deterministicParaIdSeedKey)).find((value) => typeof value === "string") ?? null;
298
+ const census = collectParagraphCensus(newState.doc, paragraphSourceSeed);
299
+ const updates = collectParaIdUpdatesFromCensus(census, keeperPositions.paraIds, deterministicSeed);
300
+ const paragraphSourceUpdates = collectParagraphPropertySourceUpdates(census, keeperPositions.sourceTokens, transactions);
301
+ if (updates.length === 0 && paragraphSourceUpdates.length === 0) return null;
224
302
  const tr = newState.tr;
225
303
  for (const u of updates) setProseParagraphMarkupWithPropertySource({
226
304
  attrs: u.attrs,
@@ -228,6 +306,19 @@ const createParaIdAllocatorPlugin = () => new Plugin({
228
306
  pos: u.pos,
229
307
  transaction: tr
230
308
  });
309
+ for (const { pos, token } of paragraphSourceUpdates) {
310
+ const paragraph = tr.doc.nodeAt(pos);
311
+ if (!paragraph || paragraph.type.name !== "paragraph") panic("Paragraph-property token update lost its paragraph");
312
+ setProseParagraphMarkupWithPropertySource({
313
+ attrs: {
314
+ ...paragraph.attrs,
315
+ [PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR]: token
316
+ },
317
+ ownership: "preserve",
318
+ pos,
319
+ transaction: tr
320
+ });
321
+ }
231
322
  ignoreTrackedChanges(tr);
232
323
  tr.setMeta(paraIdAllocatorKey, "allocated");
233
324
  tr.setMeta("addToHistory", false);
@@ -1,66 +1,7 @@
1
- import { document_d_exports } from "../../../types/document.js";
2
- import { OutlineStyleAttr } from "../../../types/documentEnumValues.js";
3
- import { ImagePositionAttrs, TextBoxAttrs as TextBoxAttrs$1 } from "../../schema/nodes.js";
1
+ import { TextBoxAttrs as TextBoxAttrs$1 } from "../../schema/nodes.js";
4
2
  import { NodeExtension } from "../types.js";
5
3
  //#region src/prosemirror/extensions/nodes/TextBoxExtension.d.ts
6
- type TextBoxAttrs = {
7
- /** Width in pixels */
8
- width?: number;
9
- /** Height in pixels */
10
- height?: number;
11
- /** Text fitting behavior */
12
- autoFit?: document_d_exports.ShapeTextBody["autoFit"];
13
- /** Horizontal text wrapping inside the box */
14
- textWrap?: document_d_exports.ShapeTextBody["textWrap"];
15
- /** Unique identifier */
16
- textBoxId?: string;
17
- /** Fill color as CSS color */
18
- fillColor?: string;
19
- /** Outline width in pixels */
20
- outlineWidth?: number;
21
- /** Outline color as CSS color */
22
- outlineColor?: string;
23
- /** Outline dash style, or `"none"` for an explicit no-outline. */
24
- outlineStyle?: OutlineStyleAttr;
25
- /** Internal margin top in pixels */
26
- marginTop?: number;
27
- /** Internal margin bottom in pixels */
28
- marginBottom?: number;
29
- /** Internal margin left in pixels */
30
- marginLeft?: number;
31
- /** Internal margin right in pixels */
32
- marginRight?: number;
33
- /** Vertical text alignment */
34
- verticalAlign?: string;
35
- /** Display mode */
36
- displayMode?: "inline" | "float" | "block";
37
- /** CSS float direction */
38
- cssFloat?: "left" | "right" | "none";
39
- /** Wrap type */
40
- wrapType?: document_d_exports.ImageWrap["type"];
41
- /** OOXML wrapText direction for anchored text boxes (eigenpal #474). */
42
- wrapText?: "bothSides" | "left" | "right" | "largest";
43
- /** Wrap distance from top edge, in pixels (OOXML distT, EMU-converted). */
44
- distTop?: number;
45
- /** Wrap distance from bottom edge, in pixels. */
46
- distBottom?: number;
47
- /** Wrap distance from left edge, in pixels. */
48
- distLeft?: number;
49
- /** Wrap distance from right edge, in pixels. */
50
- distRight?: number;
51
- /** Position for floating/anchored text boxes. */
52
- position?: ImagePositionAttrs;
53
- /** Original DOCX placement hint for save-path reconstruction. */
54
- _docxPlacement?: "standalone" | "inlineWithPrevious";
55
- /** Original DOCX paragraph group for standalone text-box reconstruction. */
56
- _docxGroupId?: string;
57
- /** Inline anchor linking this block node to its source run position. */
58
- _docxAnchorId?: string;
59
- /** Original run-level revision wrapper for save-path reconstruction. */
60
- _docxTrackedChange?: TextBoxAttrs$1["_docxTrackedChange"];
61
- /** Original inline content-control ancestry for save-path reconstruction. */
62
- _docxInlineSdts?: TextBoxAttrs$1["_docxInlineSdts"];
63
- };
4
+ type TextBoxAttrs = TextBoxAttrs$1;
64
5
  declare const TextBoxExtension: (options?: Partial<Record<string, unknown>> | undefined) => NodeExtension;
65
6
  //#endregion
66
7
  export { TextBoxAttrs, TextBoxExtension };
@@ -2,6 +2,13 @@ import { IMAGE_WRAP_TYPE_VALUES, normalizeShapeTextAnchor } from "../../../types
2
2
  import { expectTextBoxAttrs } from "../../attrs/index.js";
3
3
  import { createNodeExtension } from "../create.js";
4
4
  //#region src/prosemirror/extensions/nodes/TextBoxExtension.ts
5
+ /**
6
+ * TextBox Extension — editable text box node
7
+ *
8
+ * An isolating block node that contains paragraphs (and tables).
9
+ * Rendered as a positioned container with optional fill, outline, and margins.
10
+ * Supports inline and floating positioning.
11
+ */
5
12
  function parseTextBoxPosition(raw) {
6
13
  if (!raw) return;
7
14
  try {
@@ -42,6 +49,7 @@ const TextBoxExtension = createNodeExtension({
42
49
  outlineWidth: { default: null },
43
50
  outlineColor: { default: null },
44
51
  outlineStyle: { default: null },
52
+ transform: { default: null },
45
53
  marginTop: { default: 4 },
46
54
  marginBottom: { default: 4 },
47
55
  marginLeft: { default: 7 },
@@ -59,6 +67,7 @@ const TextBoxExtension = createNodeExtension({
59
67
  _docxPlacement: { default: null },
60
68
  _docxGroupId: { default: null },
61
69
  _docxAnchorId: { default: null },
70
+ _docxTextBodyContentState: { default: { type: "authored" } },
62
71
  _docxTrackedChange: { default: null },
63
72
  _docxInlineSdts: { default: null }
64
73
  },
@@ -73,6 +82,7 @@ const TextBoxExtension = createNodeExtension({
73
82
  const textWrap = parseTextBoxTextWrap(d["textWrap"]);
74
83
  const verticalAlign = parseTextBoxVerticalAlign(d["verticalAlign"]);
75
84
  return {
85
+ _docxTextBodyContentState: { type: "authored" },
76
86
  ...d["width"] ? { width: Number(d["width"]) } : {},
77
87
  ...d["height"] ? { height: Number(d["height"]) } : {},
78
88
  ...autoFit ? { autoFit } : {},
@@ -82,6 +92,7 @@ const TextBoxExtension = createNodeExtension({
82
92
  ...d["outlineWidth"] ? { outlineWidth: Number(d["outlineWidth"]) } : {},
83
93
  ...d["outlineColor"] ? { outlineColor: d["outlineColor"] } : {},
84
94
  ...d["outlineStyle"] ? { outlineStyle: d["outlineStyle"] } : {},
95
+ ...d["transform"] ? { transform: d["transform"] } : {},
85
96
  ...d["marginTop"] ? { marginTop: Number(d["marginTop"]) } : {},
86
97
  ...d["marginBottom"] ? { marginBottom: Number(d["marginBottom"]) } : {},
87
98
  ...d["marginLeft"] ? { marginLeft: Number(d["marginLeft"]) } : {},
@@ -111,6 +122,7 @@ const TextBoxExtension = createNodeExtension({
111
122
  if (attrs.outlineWidth) domAttrs["data-outline-width"] = String(attrs.outlineWidth);
112
123
  if (attrs.outlineColor) domAttrs["data-outline-color"] = attrs.outlineColor;
113
124
  if (attrs.outlineStyle) domAttrs["data-outline-style"] = attrs.outlineStyle;
125
+ if (attrs.transform) domAttrs["data-transform"] = attrs.transform;
114
126
  if (typeof attrs.marginTop === "number") domAttrs["data-margin-top"] = String(attrs.marginTop);
115
127
  if (typeof attrs.marginBottom === "number") domAttrs["data-margin-bottom"] = String(attrs.marginBottom);
116
128
  if (typeof attrs.marginLeft === "number") domAttrs["data-margin-left"] = String(attrs.marginLeft);
@@ -158,6 +170,10 @@ const TextBoxExtension = createNodeExtension({
158
170
  styles.push("box-sizing: border-box");
159
171
  styles.push("overflow: hidden");
160
172
  styles.push("position: relative");
173
+ if (attrs.transform) {
174
+ styles.push(`transform: ${attrs.transform}`);
175
+ styles.push("transform-origin: center center");
176
+ }
161
177
  domAttrs["style"] = styles.join("; ");
162
178
  return [
163
179
  "div",
@@ -488,6 +488,10 @@ type ShapeAttrs = {
488
488
  /**
489
489
  * Text box node attributes
490
490
  */
491
+ declare const TEXT_BOX_TEXT_BODY_CONTENT_STATE_TYPES: readonly ["source-empty", "authored"];
492
+ type TextBoxTextBodyContentState = { [Type in (typeof TEXT_BOX_TEXT_BODY_CONTENT_STATE_TYPES)[number]]: {
493
+ readonly type: Type;
494
+ }; }[(typeof TEXT_BOX_TEXT_BODY_CONTENT_STATE_TYPES)[number]];
491
495
  type TextBoxAttrs = {
492
496
  /** Width in pixels */
493
497
  width?: number;
@@ -507,6 +511,8 @@ type TextBoxAttrs = {
507
511
  outlineColor?: string;
508
512
  /** Outline dash style, or `"none"` for an explicit no-outline. */
509
513
  outlineStyle?: OutlineStyleAttr;
514
+ /** DrawingML rotation and/or flips, serialized as CSS transform functions. */
515
+ transform?: string;
510
516
  /** Internal margin top in pixels */
511
517
  marginTop?: number;
512
518
  /** Internal margin bottom in pixels */
@@ -541,6 +547,12 @@ type TextBoxAttrs = {
541
547
  _docxGroupId?: string;
542
548
  /** Inline anchor linking this block node to its source run position. */
543
549
  _docxAnchorId?: string;
550
+ /**
551
+ * Ownership of the schema-required placeholder paragraph. A source text
552
+ * body with no children needs one paragraph while it is editable, but that
553
+ * paragraph is not authored document content.
554
+ */
555
+ _docxTextBodyContentState: TextBoxTextBodyContentState;
544
556
  /** Original run-level revision wrapper for save-path reconstruction. */
545
557
  _docxTrackedChange?: {
546
558
  type: "insertion";
@@ -765,7 +777,7 @@ type TableCellAttrs = {
765
777
  /** Preserve a DOCX vMerge restart even when PM cannot model it as a rowspan. */
766
778
  _preserveVMergeRestart?: boolean;
767
779
  /** Original DOCX vMerge continuation cells skipped into this PM rowspan. */
768
- _docxVMergeContinuationCells?: document_d_exports.TableCell[];
780
+ _docxVMergeContinuationCells?: unknown;
769
781
  };
770
782
  //#endregion
771
- export { BlockSdtAttrs, BookmarkBoundaryAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, ImagePositionAttrs, MathAttrs, PageBreakRunAttrs, ParagraphAttrs, ParagraphPropertyChangeAttrs, SdtAttrs, ShapeAttrs, SuggestedStructuralMarker, SymbolAttrs, TabAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAnchorAttrs, TextBoxAttrs };
783
+ export { BlockSdtAttrs, BookmarkBoundaryAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, ImagePositionAttrs, MathAttrs, PageBreakRunAttrs, ParagraphAttrs, ParagraphPropertyChangeAttrs, SdtAttrs, ShapeAttrs, SuggestedStructuralMarker, SymbolAttrs, TEXT_BOX_TEXT_BODY_CONTENT_STATE_TYPES, TabAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAnchorAttrs, TextBoxAttrs, TextBoxTextBodyContentState };
@@ -0,0 +1,7 @@
1
+ //#region src/prosemirror/schema/nodes.ts
2
+ /**
3
+ * Text box node attributes
4
+ */
5
+ const TEXT_BOX_TEXT_BODY_CONTENT_STATE_TYPES = Object.freeze(["source-empty", "authored"]);
6
+ //#endregion
7
+ export { TEXT_BOX_TEXT_BODY_CONTENT_STATE_TYPES };
@@ -0,0 +1,9 @@
1
+ import { Node } from "prosemirror-model";
2
+ import * as Y from "yjs";
3
+ //#region src/prosemirror/yjsParagraphSourceContract.d.ts
4
+ declare const proseDocumentParagraphSourceContract: (document: Node) => string | null;
5
+ declare const writeYjsParagraphSourceContract: (ydoc: Y.Doc, document: Node) => void;
6
+ declare const readYjsParagraphSourceContract: (ydoc: Y.Doc) => string | null;
7
+ declare const withParagraphSourceContract: (document: Node, contract: string) => Node;
8
+ //#endregion
9
+ export { proseDocumentParagraphSourceContract, readYjsParagraphSourceContract, withParagraphSourceContract, writeYjsParagraphSourceContract };
@@ -0,0 +1,26 @@
1
+ import { PROSE_PARAGRAPH_SOURCE_CONTRACT_ATTR, getProseDocumentParagraphPropertySourceContract } from "../docx/paragraphPropertySource.js";
2
+ import { panic } from "better-result";
3
+ //#region src/prosemirror/yjsParagraphSourceContract.ts
4
+ const FOLIO_YJS_METADATA_MAP_NAME = "folio:document-metadata";
5
+ const PARAGRAPH_SOURCE_CONTRACT_KEY = "paragraphSourceContract";
6
+ const proseDocumentParagraphSourceContract = (document) => {
7
+ return getProseDocumentParagraphPropertySourceContract(document);
8
+ };
9
+ const writeYjsParagraphSourceContract = (ydoc, document) => {
10
+ const contract = proseDocumentParagraphSourceContract(document);
11
+ if (!contract) panic("Cannot seed collaboration without a paragraph-property source contract");
12
+ ydoc.getMap(FOLIO_YJS_METADATA_MAP_NAME).set(PARAGRAPH_SOURCE_CONTRACT_KEY, contract);
13
+ };
14
+ const readYjsParagraphSourceContract = (ydoc) => {
15
+ const contract = ydoc.getMap(FOLIO_YJS_METADATA_MAP_NAME).get(PARAGRAPH_SOURCE_CONTRACT_KEY);
16
+ return typeof contract === "string" ? contract : null;
17
+ };
18
+ const withParagraphSourceContract = (document, contract) => {
19
+ if (document.type.name !== "doc") panic("A paragraph-property source contract can only attach to a document node");
20
+ return document.type.create({
21
+ ...document.attrs,
22
+ [PROSE_PARAGRAPH_SOURCE_CONTRACT_ATTR]: contract
23
+ }, document.content, document.marks);
24
+ };
25
+ //#endregion
26
+ export { proseDocumentParagraphSourceContract, readYjsParagraphSourceContract, withParagraphSourceContract, writeYjsParagraphSourceContract };
@@ -5,7 +5,11 @@ type BoundingBox = {
5
5
  height: number;
6
6
  };
7
7
  declare function parseRotationDegrees(transform: string | undefined): number;
8
+ /** Whether a DrawingML/CSS transform reflects around the vertical centre line. */
9
+ declare function hasHorizontalFlip(transform: string | undefined): boolean;
10
+ /** Whether a DrawingML/CSS transform reflects around the horizontal centre line. */
11
+ declare function hasVerticalFlip(transform: string | undefined): boolean;
8
12
  declare function rotatedBoundingBox(w: number, h: number, deg: number): BoundingBox;
9
13
  declare function inlineImageBoundingBox(run: ImageRun): BoundingBox;
10
14
  //#endregion
11
- export { BoundingBox, inlineImageBoundingBox, parseRotationDegrees, rotatedBoundingBox };
15
+ export { BoundingBox, hasHorizontalFlip, hasVerticalFlip, inlineImageBoundingBox, parseRotationDegrees, rotatedBoundingBox };
@@ -7,6 +7,14 @@ function parseRotationDegrees(transform) {
7
7
  if (!Number.isFinite(raw)) return 0;
8
8
  return (raw % 360 + 360) % 360;
9
9
  }
10
+ /** Whether a DrawingML/CSS transform reflects around the vertical centre line. */
11
+ function hasHorizontalFlip(transform) {
12
+ return /scaleX\(\s*-1\s*\)/iu.test(transform ?? "");
13
+ }
14
+ /** Whether a DrawingML/CSS transform reflects around the horizontal centre line. */
15
+ function hasVerticalFlip(transform) {
16
+ return /scaleY\(\s*-1\s*\)/iu.test(transform ?? "");
17
+ }
10
18
  function rotatedBoundingBox(w, h, deg) {
11
19
  if (deg === 0 || deg === 180) return {
12
20
  width: w,
@@ -33,4 +41,4 @@ function inlineImageBoundingBox(run) {
33
41
  return rotatedBoundingBox(run.width, run.height, rotation);
34
42
  }
35
43
  //#endregion
36
- export { inlineImageBoundingBox, parseRotationDegrees, rotatedBoundingBox };
44
+ export { hasHorizontalFlip, hasVerticalFlip, inlineImageBoundingBox, parseRotationDegrees, rotatedBoundingBox };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.37.2",
3
+ "version": "0.37.4",
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",
@@ -121,7 +121,7 @@
121
121
  "perf": "bun scripts/profile-editor.ts"
122
122
  },
123
123
  "dependencies": {
124
- "@stll/docx-core": "^0.19.3",
124
+ "@stll/docx-core": "^0.19.4",
125
125
  "@stll/docx-utils": "^0.1.0",
126
126
  "@stll/template-conditions": ">=0.4.0 <1.0.0",
127
127
  "better-result": "3.0.1",