@stll/folio-core 0.28.0 → 0.29.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.
Files changed (56) hide show
  1. package/dist/docx/styleParser.js +15 -14
  2. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +1 -2
  3. package/dist/layout-bridge/convert/toFlowBlocks.js +135 -118
  4. package/dist/layout-engine/footnoteColumnReflow.d.ts +15 -0
  5. package/dist/layout-engine/footnoteColumnReflow.js +74 -0
  6. package/dist/layout-engine/index.d.ts +3 -5
  7. package/dist/layout-engine/index.js +36 -15
  8. package/dist/layout-engine/measure/cache.d.ts +1 -1
  9. package/dist/layout-engine/measure/cache.js +11 -2
  10. package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
  11. package/dist/layout-engine/measure/listMarkerWidth.js +6 -4
  12. package/dist/layout-engine/paginator.d.ts +6 -0
  13. package/dist/layout-engine/paginator.js +30 -11
  14. package/dist/layout-engine/types.d.ts +11 -6
  15. package/dist/markdown/renderParagraph.js +7 -2
  16. package/dist/prosemirror/attrs/index.js +5 -3
  17. package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +8 -0
  18. package/dist/prosemirror/bookmarkBoundaryAttrs.js +66 -0
  19. package/dist/prosemirror/commands/formatPainter.js +45 -1
  20. package/dist/prosemirror/conversion/fromProseDoc.d.ts +4 -1
  21. package/dist/prosemirror/conversion/fromProseDoc.js +224 -91
  22. package/dist/prosemirror/conversion/toProseDoc.d.ts +4 -1
  23. package/dist/prosemirror/conversion/toProseDoc.js +325 -94
  24. package/dist/prosemirror/extensions/StarterKit.js +11 -3
  25. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +8 -4
  26. package/dist/prosemirror/extensions/features/PasteCleanupExtension.d.ts +4 -1
  27. package/dist/prosemirror/extensions/features/PasteCleanupExtension.js +9 -5
  28. package/dist/prosemirror/extensions/features/pasteCleanup.d.ts +10 -23
  29. package/dist/prosemirror/extensions/features/pasteCleanup.js +77 -22
  30. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +22 -22
  31. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +9 -0
  32. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +67 -0
  33. package/dist/prosemirror/extensions/nodes/FieldExtension.d.ts +10 -4
  34. package/dist/prosemirror/extensions/nodes/FieldExtension.js +77 -59
  35. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +4 -1
  36. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -3
  37. package/dist/prosemirror/listMarker.d.ts +58 -0
  38. package/dist/prosemirror/listMarker.js +185 -0
  39. package/dist/prosemirror/numberedRefFields.d.ts +24 -0
  40. package/dist/prosemirror/numberedRefFields.js +276 -0
  41. package/dist/prosemirror/paraText.js +56 -17
  42. package/dist/prosemirror/plugins/anonymizationDecorations.js +1 -1
  43. package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -1
  44. package/dist/prosemirror/plugins/pmTextScan.js +28 -7
  45. package/dist/prosemirror/plugins/templateDirectives.js +2 -2
  46. package/dist/prosemirror/schema/index.d.ts +2 -2
  47. package/dist/prosemirror/schema/marks.d.ts +3 -1
  48. package/dist/prosemirror/schema/nodes.d.ts +13 -1
  49. package/dist/prosemirror/styles/styleResolver.d.ts +0 -1
  50. package/dist/prosemirror/styles/styleResolver.js +22 -29
  51. package/dist/prosemirror/styles/styleToggleCascade.d.ts +37 -0
  52. package/dist/prosemirror/styles/styleToggleCascade.js +52 -0
  53. package/dist/prosemirror/validation.js +93 -0
  54. package/dist/utils/textFormattingMerge.d.ts +9 -1
  55. package/dist/utils/textFormattingMerge.js +32 -1
  56. package/package.json +1 -1
@@ -10,10 +10,24 @@ const collectBlockChunks = (doc) => {
10
10
  if (node.isTextblock) {
11
11
  const chunks = [];
12
12
  node.descendants((child, offset) => {
13
- if (child.isText && child.text !== void 0) chunks.push({
14
- text: child.text,
15
- start: pos + 1 + offset
16
- });
13
+ if (child.isText && child.text !== void 0) {
14
+ const start = pos + 1 + offset;
15
+ chunks.push({
16
+ text: child.text,
17
+ start,
18
+ end: start + child.text.length
19
+ });
20
+ return false;
21
+ }
22
+ if (child.isLeaf && child.textContent) {
23
+ const start = pos + 1 + offset;
24
+ chunks.push({
25
+ text: child.textContent,
26
+ start,
27
+ end: start + child.nodeSize
28
+ });
29
+ return false;
30
+ }
17
31
  return true;
18
32
  });
19
33
  if (chunks.length > 0) blocks.push(chunks);
@@ -24,14 +38,21 @@ const collectBlockChunks = (doc) => {
24
38
  return blocks;
25
39
  };
26
40
  /** Map a joined-string offset back to its PM doc position. */
27
- const offsetToDocPos = (chunks, offset) => {
41
+ const offsetToDocPos = (chunks, offset, bias = "start") => {
28
42
  let consumed = 0;
29
43
  for (const chunk of chunks) {
30
- if (offset <= consumed + chunk.text.length) return chunk.start + (offset - consumed);
44
+ if (offset <= consumed + chunk.text.length) {
45
+ const localOffset = offset - consumed;
46
+ const end = chunk.end ?? chunk.start + chunk.text.length;
47
+ if (end - chunk.start === chunk.text.length) return chunk.start + localOffset;
48
+ if (localOffset === 0) return chunk.start;
49
+ if (localOffset === chunk.text.length || bias === "end") return end;
50
+ return chunk.start;
51
+ }
31
52
  consumed += chunk.text.length;
32
53
  }
33
54
  const last = chunks.at(-1);
34
- return last ? last.start + last.text.length : 0;
55
+ return last ? last.end ?? last.start + last.text.length : 0;
35
56
  };
36
57
  /** Join a block's chunks into the single string callers scan. */
37
58
  const joinChunks = (chunks) => chunks.map((c) => c.text).join("");
@@ -71,7 +71,7 @@ const scanDirectives = (doc) => {
71
71
  const last = chunks.at(-1);
72
72
  ranges.push({
73
73
  from: chunks[0]?.start ?? 0,
74
- to: last ? last.start + last.text.length : 0,
74
+ to: last ? last.end ?? last.start + last.text.length : 0,
75
75
  kind: sole.meta.kind,
76
76
  expr: directiveExpr(sole.meta),
77
77
  block: true
@@ -82,7 +82,7 @@ const scanDirectives = (doc) => {
82
82
  const clauseVersion = marker.meta.kind === "clause" ? marker.meta.version : void 0;
83
83
  ranges.push({
84
84
  from: offsetToDocPos(chunks, marker.start),
85
- to: offsetToDocPos(chunks, marker.end),
85
+ to: offsetToDocPos(chunks, marker.end, "end"),
86
86
  kind: marker.meta.kind,
87
87
  expr: directiveExpr(marker.meta),
88
88
  block: false,
@@ -1,5 +1,5 @@
1
1
  import { CharacterSpacingAttrs, CharacterStyleAttrs, CommentAttrs, EmphasisMarkAttrs, FontFamilyAttrs, FontSizeAttrs, FootnoteRefAttrs, HighlightAttrs, HyperlinkAttrs, LanguageAttrs, RunFormattingOverrideAttrs, RunPropertyChangeMarkAttrs, RunShadingAttrs, StrikeAttrs, TextColorAttrs, TextEffectAttrs, TrackedChangeMarkAttrs, UnderlineAttrs } from "./marks.js";
2
- import { BlockSdtAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, ImagePositionAttrs, MathAttrs, ParagraphAttrs, ParagraphPropertyChangeAttrs, SdtAttrs, ShapeAttrs, SymbolAttrs, TabAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAttrs } from "./nodes.js";
2
+ import { BlockSdtAttrs, BookmarkBoundaryAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, ImagePositionAttrs, MathAttrs, ParagraphAttrs, ParagraphPropertyChangeAttrs, SdtAttrs, ShapeAttrs, SymbolAttrs, TabAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAttrs } from "./nodes.js";
3
3
  import { ExtensionManager } from "../extensions/ExtensionManager.js";
4
4
  //#region src/prosemirror/schema/index.d.ts
5
5
  declare const singletonManager: ExtensionManager;
@@ -11,4 +11,4 @@ type DocxSchema = typeof schema;
11
11
  type DocxNode = ReturnType<typeof schema.node>;
12
12
  type DocxMark = ReturnType<typeof schema.mark>;
13
13
  //#endregion
14
- export { type BlockSdtAttrs, type CharacterSpacingAttrs, type CharacterStyleAttrs, type CommentAttrs, DocxMark, DocxNode, DocxSchema, type EmphasisMarkAttrs, type FieldAttrs, type FontFamilyAttrs, type FontSizeAttrs, type FootnoteRefAttrs, type HardBreakAttrs, type HighlightAttrs, type HyperlinkAttrs, type ImageAttrs, type ImagePositionAttrs, type LanguageAttrs, type MathAttrs, type ParagraphAttrs, type ParagraphPropertyChangeAttrs, type RunFormattingOverrideAttrs, type RunPropertyChangeMarkAttrs, type RunShadingAttrs, type SdtAttrs, type ShapeAttrs, type StrikeAttrs, type SymbolAttrs, type TabAttrs, type TableAttrs, type TableCellAttrs, type TableRowAttrs, type TextBoxAttrs, type TextColorAttrs, type TextEffectAttrs, type TrackedChangeMarkAttrs, type UnderlineAttrs, schema, singletonManager };
14
+ export { type BlockSdtAttrs, type BookmarkBoundaryAttrs, type CharacterSpacingAttrs, type CharacterStyleAttrs, type CommentAttrs, DocxMark, DocxNode, DocxSchema, type EmphasisMarkAttrs, type FieldAttrs, type FontFamilyAttrs, type FontSizeAttrs, type FootnoteRefAttrs, type HardBreakAttrs, type HighlightAttrs, type HyperlinkAttrs, type ImageAttrs, type ImagePositionAttrs, type LanguageAttrs, type MathAttrs, type ParagraphAttrs, type ParagraphPropertyChangeAttrs, type RunFormattingOverrideAttrs, type RunPropertyChangeMarkAttrs, type RunShadingAttrs, type SdtAttrs, type ShapeAttrs, type StrikeAttrs, type SymbolAttrs, type TabAttrs, type TableAttrs, type TableCellAttrs, type TableRowAttrs, type TextBoxAttrs, type TextColorAttrs, type TextEffectAttrs, type TrackedChangeMarkAttrs, type UnderlineAttrs, schema, singletonManager };
@@ -121,7 +121,9 @@ type RunPropertyChangeMarkAttrs = {
121
121
  provenance: TrackedChangeProvenance;
122
122
  suggestionId?: string;
123
123
  };
124
- type RunFormattingOverrideAttrs = { [K in keyof Pick<document_d_exports.TextFormatting, "bold" | "italic" | "strike" | "doubleStrike" | "allCaps" | "smallCaps" | "hidden" | "emboss" | "imprint" | "shadow" | "outline" | "rtl">]?: false; } & {
124
+ type RunFormattingOverrideAttrs = { [K in keyof Pick<document_d_exports.TextFormatting, "bold" | "italic" | "strike" | "allCaps" | "smallCaps" | "hidden" | "emboss" | "imprint" | "shadow" | "outline">]?: boolean; } & {
125
+ doubleStrike?: false;
126
+ rtl?: false;
125
127
  /** Independent complex-script weight (`w:bCs`). */
126
128
  boldCs?: boolean;
127
129
  /** Force complex-script formatting for the full run (`w:cs`). */
@@ -15,6 +15,16 @@ type SymbolAttrs = {
15
15
  font: string;
16
16
  char: string;
17
17
  };
18
+ type BookmarkBoundaryAttrs = {
19
+ type: "start";
20
+ id: number;
21
+ name: string;
22
+ colFirst?: number;
23
+ colLast?: number;
24
+ } | {
25
+ type: "end";
26
+ id: number;
27
+ };
18
28
  /**
19
29
  * Paragraph node attributes - maps to ParagraphFormatting
20
30
  */
@@ -292,6 +302,8 @@ type FieldAttrs = {
292
302
  instruction: string;
293
303
  /** Current/cached display text */
294
304
  displayText: string;
305
+ /** Imported cache that proved numbered REF resolution for this field. */
306
+ _numberedRefBaseline?: string;
295
307
  /** Whether the field came from w:fldSimple or a complex fldChar range */
296
308
  fieldKind: "simple" | "complex";
297
309
  /** Field is locked */
@@ -706,4 +718,4 @@ type TableCellAttrs = {
706
718
  _docxVMergeContinuationCells?: document_d_exports.TableCell[];
707
719
  };
708
720
  //#endregion
709
- export { BlockSdtAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, ImagePositionAttrs, MathAttrs, ParagraphAttrs, ParagraphPropertyChangeAttrs, SdtAttrs, ShapeAttrs, SuggestedStructuralMarker, SymbolAttrs, TabAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAnchorAttrs, TextBoxAttrs };
721
+ export { BlockSdtAttrs, BookmarkBoundaryAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, ImagePositionAttrs, MathAttrs, ParagraphAttrs, ParagraphPropertyChangeAttrs, SdtAttrs, ShapeAttrs, SuggestedStructuralMarker, SymbolAttrs, TabAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAnchorAttrs, TextBoxAttrs };
@@ -127,7 +127,6 @@ declare class StyleResolver {
127
127
  */
128
128
  hasStyle(styleId: string): boolean;
129
129
  private findDefaultStyle;
130
- private mergeStyleIntoResult;
131
130
  }
132
131
  /**
133
132
  * Create a style resolver from document's style definitions
@@ -1,5 +1,5 @@
1
1
  import { mergeParagraphFormatting } from "../../utils/paragraphFormattingMerge.js";
2
- import { mergeTextFormatting } from "../../utils/textFormattingMerge.js";
2
+ import { cascadeStyleTextFormatting } from "./styleToggleCascade.js";
3
3
  //#region src/prosemirror/styles/styleResolver.ts
4
4
  /**
5
5
  * Word's default-template Normal style, used as a last-resort fallback for a
@@ -102,21 +102,23 @@ var StyleResolver = class {
102
102
  resolveParagraphStyleCascade(styleId, tableParagraphOverlay) {
103
103
  const result = {};
104
104
  if (this.docDefaults?.pPr) result.paragraphFormatting = { ...this.docDefaults.pPr };
105
- if (this.docDefaults?.rPr) result.runFormatting = { ...this.docDefaults.rPr };
106
105
  if (tableParagraphOverlay) {
107
106
  const merged = mergeParagraphFormatting(result.paragraphFormatting, tableParagraphOverlay);
108
107
  if (merged !== void 0) result.paragraphFormatting = merged;
109
108
  }
110
- if (!styleId) {
111
- if (this.defaultParagraphStyle) this.mergeStyleIntoResult(result, this.defaultParagraphStyle);
112
- return result;
113
- }
114
- const style = this.stylesById.get(styleId);
115
- if (!style) {
116
- if (this.defaultParagraphStyle) this.mergeStyleIntoResult(result, this.defaultParagraphStyle);
117
- return result;
109
+ const style = styleId ? this.stylesById.get(styleId) ?? this.defaultParagraphStyle : this.defaultParagraphStyle;
110
+ if (style?.pPr) {
111
+ const merged = mergeParagraphFormatting(result.paragraphFormatting, style.pPr);
112
+ if (merged) result.paragraphFormatting = merged;
118
113
  }
119
- this.mergeStyleIntoResult(result, style);
114
+ const runFormatting = cascadeStyleTextFormatting([{
115
+ formatting: this.docDefaults?.rPr,
116
+ type: "defaults"
117
+ }, {
118
+ formatting: style?.rPr,
119
+ type: "style"
120
+ }]).formatting;
121
+ if (runFormatting) result.runFormatting = runFormatting;
120
122
  return result;
121
123
  }
122
124
  /**
@@ -159,14 +161,15 @@ var StyleResolver = class {
159
161
  * @returns Resolved text formatting
160
162
  */
161
163
  resolveRunStyle(styleId) {
162
- let result = {};
163
- if (this.docDefaults?.rPr) result = { ...this.docDefaults.rPr };
164
- const defaultCharacterRpr = this.defaultCharacterStyle?.rPr;
165
- if (defaultCharacterRpr) result = mergeTextFormatting(result, defaultCharacterRpr) ?? {};
166
- const style = styleId ? this.stylesById.get(styleId) : void 0;
167
- if (!style?.rPr) return Object.keys(result).length > 0 ? result : void 0;
168
- const merged = mergeTextFormatting(result, style.rPr);
169
- return merged && Object.keys(merged).length > 0 ? merged : void 0;
164
+ const style = styleId ? this.stylesById.get(styleId) : this.defaultCharacterStyle;
165
+ const result = cascadeStyleTextFormatting([{
166
+ formatting: this.docDefaults?.rPr,
167
+ type: "defaults"
168
+ }, {
169
+ formatting: style?.rPr,
170
+ type: "style"
171
+ }]).formatting;
172
+ return result && Object.keys(result).length > 0 ? result : void 0;
170
173
  }
171
174
  /**
172
175
  * Get a character style's own properties WITHOUT docDefaults.
@@ -213,16 +216,6 @@ var StyleResolver = class {
213
216
  for (const style of this.stylesById.values()) if (style.type === type && style.default) return style;
214
217
  if (type === "paragraph") return this.stylesById.get("Normal") ?? (this.docDefaults ? void 0 : BUILTIN_NORMAL_STYLE);
215
218
  }
216
- mergeStyleIntoResult(result, style) {
217
- if (style.pPr) {
218
- const merged = mergeParagraphFormatting(result.paragraphFormatting, style.pPr);
219
- if (merged !== void 0) result.paragraphFormatting = merged;
220
- }
221
- if (style.rPr) {
222
- const merged = mergeTextFormatting(result.runFormatting, style.rPr);
223
- if (merged !== void 0) result.runFormatting = merged;
224
- }
225
- }
226
219
  };
227
220
  /**
228
221
  * Create a style resolver from document's style definitions
@@ -0,0 +1,37 @@
1
+ import { document_d_exports } from "../../types/document.js";
2
+ import { STYLE_TOGGLE_KEYS } from "../../utils/textFormattingMerge.js";
3
+ //#region src/prosemirror/styles/styleToggleCascade.d.ts
4
+ type StyleToggleKey = (typeof STYLE_TOGGLE_KEYS)[number];
5
+ type ToggleState = {
6
+ value: boolean;
7
+ defaultsActive: boolean;
8
+ };
9
+ /** Internal formatting value whose toggle history survives copying and separate cascade passes. */
10
+ type StyleTextFormattingCascade = {
11
+ formatting: document_d_exports.TextFormatting | undefined;
12
+ toggleStates: ReadonlyMap<StyleToggleKey, ToggleState>;
13
+ type: "styleToggleCascade";
14
+ };
15
+ type StyleToggleLevel = {
16
+ formatting: document_d_exports.TextFormatting | undefined;
17
+ type: "defaults" | "style" | "direct";
18
+ } | {
19
+ cascade: StyleTextFormattingCascade;
20
+ type: "carried";
21
+ };
22
+ type StyleToggleCascadeOptions = {
23
+ /** Existing ordinary-property merge whose toggle fields this cascade replaces. */
24
+ ordinaryFormatting: document_d_exports.TextFormatting | undefined;
25
+ };
26
+ /**
27
+ * Resolve run formatting across OOXML style hierarchy levels.
28
+ *
29
+ * Ordinary properties retain Folio's existing low-to-high, last-defined merge. Toggle
30
+ * properties reverse inherited state when a style level states `true`; `false` leaves inherited
31
+ * style state unchanged, while direct formatting remains an absolute value. The returned value
32
+ * carries its toggle history explicitly, so paragraph and character style resolution may happen
33
+ * in separate passes without an object-identity contract.
34
+ */
35
+ declare function cascadeStyleTextFormatting(levels: readonly StyleToggleLevel[], options?: StyleToggleCascadeOptions): StyleTextFormattingCascade;
36
+ //#endregion
37
+ export { cascadeStyleTextFormatting };
@@ -0,0 +1,52 @@
1
+ import { STYLE_TOGGLE_KEYS, mergeTextFormatting } from "../../utils/textFormattingMerge.js";
2
+ //#region src/prosemirror/styles/styleToggleCascade.ts
3
+ /**
4
+ * Resolve run formatting across OOXML style hierarchy levels.
5
+ *
6
+ * Ordinary properties retain Folio's existing low-to-high, last-defined merge. Toggle
7
+ * properties reverse inherited state when a style level states `true`; `false` leaves inherited
8
+ * style state unchanged, while direct formatting remains an absolute value. The returned value
9
+ * carries its toggle history explicitly, so paragraph and character style resolution may happen
10
+ * in separate passes without an object-identity contract.
11
+ */
12
+ function cascadeStyleTextFormatting(levels, options) {
13
+ let formatting = options ? mergeTextFormatting(void 0, options.ordinaryFormatting) : void 0;
14
+ const mergeOrdinaryFormatting = options === void 0;
15
+ const toggleStates = /* @__PURE__ */ new Map();
16
+ for (const level of levels) {
17
+ if (level.type === "carried") {
18
+ if (mergeOrdinaryFormatting) formatting = mergeTextFormatting(formatting, level.cascade.formatting);
19
+ for (const [key, state] of level.cascade.toggleStates) toggleStates.set(key, state);
20
+ continue;
21
+ }
22
+ if (!level.formatting) continue;
23
+ if (mergeOrdinaryFormatting) formatting = mergeTextFormatting(formatting, level.formatting);
24
+ for (const key of STYLE_TOGGLE_KEYS) {
25
+ const value = level.formatting[key];
26
+ if (value === void 0) continue;
27
+ const inherited = toggleStates.get(key);
28
+ if (level.type === "direct") toggleStates.set(key, {
29
+ value,
30
+ defaultsActive: false
31
+ });
32
+ else if (!value) continue;
33
+ else if (level.type === "defaults") toggleStates.set(key, {
34
+ value: true,
35
+ defaultsActive: true
36
+ });
37
+ else toggleStates.set(key, {
38
+ value: inherited?.defaultsActive === true ? true : !(inherited?.value ?? false),
39
+ defaultsActive: inherited?.defaultsActive === true
40
+ });
41
+ }
42
+ }
43
+ if (!formatting && toggleStates.size > 0) formatting = {};
44
+ for (const [key, state] of toggleStates) if (formatting) formatting[key] = state.value;
45
+ return {
46
+ formatting,
47
+ toggleStates,
48
+ type: "styleToggleCascade"
49
+ };
50
+ }
51
+ //#endregion
52
+ export { cascadeStyleTextFormatting };
@@ -1,4 +1,5 @@
1
1
  import { readBlockSdtAttrs, readCharacterSpacingMarkAttrs, readCharacterStyleMarkAttrs, readCommentMarkAttrs, readEmphasisMarkAttrs, readFieldAttrs, readFontFamilyMarkAttrs, readFontSizeMarkAttrs, readFootnoteRefMarkAttrs, readHardBreakAttrs, readHighlightMarkAttrs, readHyperlinkMarkAttrs, readImageAttrs, readLanguageMarkAttrs, readMathAttrs, readParagraphAttrs, readRunFormattingOverrideMarkAttrs, readRunPropertyChangeMarkAttrs, readRunShadingMarkAttrs, readSdtAttrs, readShapeAttrs, readStrikeMarkAttrs, readSymbolAttrs, readTabAttrs, readTableAttrs, readTableCellAttrs, readTableRowAttrs, readTextBoxAttrs, readTextColorMarkAttrs, readTextEffectMarkAttrs, readTrackedChangeMarkAttrs, readUnderlineMarkAttrs } from "./attrs/index.js";
2
+ import { readBookmarkBoundaryAttrs } from "./bookmarkBoundaryAttrs.js";
2
3
  import { readTextBoxAnchorAttrs } from "./textBoxAnchorAttrs.js";
3
4
  //#region src/prosemirror/validation.ts
4
5
  var ProseMirrorDocumentValidationError = class extends Error {
@@ -18,11 +19,71 @@ const validateProseMirrorDocument = (doc) => {
18
19
  message: `Expected doc, got ${doc.type.name}.`
19
20
  });
20
21
  validateNode(doc, "doc", issues);
22
+ validateBookmarkBoundaryStructure(doc, issues);
21
23
  return {
22
24
  valid: issues.length === 0,
23
25
  issues
24
26
  };
25
27
  };
28
+ const validateBookmarkBoundaryStructure = (doc, issues) => {
29
+ const open = /* @__PURE__ */ new Map();
30
+ const startedIds = /* @__PURE__ */ new Set();
31
+ const registerStart = (id, path) => {
32
+ if (startedIds.has(id)) {
33
+ issues.push({
34
+ path,
35
+ message: `Bookmark id ${id} has more than one start boundary.`
36
+ });
37
+ return;
38
+ }
39
+ startedIds.add(id);
40
+ open.set(id, {
41
+ id,
42
+ path
43
+ });
44
+ };
45
+ const registerEnd = (id, path) => {
46
+ if (!open.get(id)) {
47
+ issues.push({
48
+ path,
49
+ message: `Bookmark id ${id} has no open start boundary.`
50
+ });
51
+ return;
52
+ }
53
+ open.delete(id);
54
+ };
55
+ const visit = (node, path) => {
56
+ const paragraphAttrs = node.type.name === "paragraph" ? readParagraphAttrs(node) : null;
57
+ if (paragraphAttrs?.ok) for (const [index, bookmark] of (paragraphAttrs.value.bookmarks ?? []).entries()) registerStart(bookmark.id, `${path}.paragraph.attrs.bookmarks[${index}]`);
58
+ if (node.type.name === "bookmarkBoundary") {
59
+ const result = readBookmarkBoundaryAttrs(node);
60
+ if (result.ok) {
61
+ const attrs = result.value;
62
+ const hasHyperlink = node.marks.some((mark) => mark.type.name === "hyperlink");
63
+ const trackedChanges = node.marks.filter((mark) => mark.type.name === "insertion" || mark.type.name === "deletion");
64
+ if (trackedChanges.length > 1) issues.push({
65
+ path,
66
+ message: "Bookmark boundaries cannot carry multiple tracked-change parents."
67
+ });
68
+ else if (trackedChanges.length === 1 && !hasHyperlink) issues.push({
69
+ path,
70
+ message: "Bookmark boundaries inside tracked changes require a hyperlink serialization parent."
71
+ });
72
+ if (attrs.type === "start") registerStart(attrs.id, path);
73
+ else registerEnd(attrs.id, path);
74
+ }
75
+ }
76
+ node.forEach((child, _offset, index) => {
77
+ visit(child, `${path}.content[${index}]`);
78
+ });
79
+ if (paragraphAttrs?.ok) for (const [index, bookmark] of (paragraphAttrs.value.bookmarks ?? []).entries()) registerEnd(bookmark.id, `${path}.paragraph.attrs.bookmarks[${index}]`);
80
+ };
81
+ visit(doc, "doc");
82
+ for (const boundary of open.values()) issues.push({
83
+ path: boundary.path,
84
+ message: `Bookmark id ${boundary.id} has no matching end boundary.`
85
+ });
86
+ };
26
87
  const assertValidProseMirrorDocument = (doc, context) => {
27
88
  if (validDocumentCache.has(doc)) return;
28
89
  const validation = validateProseMirrorDocument(doc);
@@ -50,6 +111,9 @@ const validateNodeAttrs = (node, path, issues) => {
50
111
  case "horizontalRule":
51
112
  case "pageBreak":
52
113
  case "renderedPageBreak": return;
114
+ case "bookmarkBoundary":
115
+ appendAttrIssues(path, readBookmarkBoundaryAttrs(node), issues);
116
+ return;
53
117
  case "tab":
54
118
  appendAttrIssues(path, readTabAttrs(node), issues);
55
119
  return;
@@ -77,6 +141,35 @@ const validateNodeAttrs = (node, path, issues) => {
77
141
  return;
78
142
  case "field":
79
143
  appendAttrIssues(path, readFieldAttrs(node), issues);
144
+ if (node.childCount > 0) issues.push({
145
+ path: `${path}.content`,
146
+ message: "Ordinary fields cannot contain structured result children."
147
+ });
148
+ return;
149
+ case "structuredField":
150
+ {
151
+ const fieldAttrs = readFieldAttrs(node);
152
+ appendAttrIssues(path, fieldAttrs, issues);
153
+ if (fieldAttrs.ok) {
154
+ const hasStructuredHyperlink = node.content.content.some((child) => child.marks.some((mark) => mark.type.name === "hyperlink"));
155
+ if (fieldAttrs.value.fieldKind === "complex") issues.push({
156
+ path: `${path}.content`,
157
+ message: "Complex fields cannot contain structured result children."
158
+ });
159
+ else if (!hasStructuredHyperlink) issues.push({
160
+ path: `${path}.content`,
161
+ message: "Structured simple fields require hyperlink content."
162
+ });
163
+ node.forEach((child, _offset, index) => {
164
+ const childPath = `${path}.content[${index}]`;
165
+ const hasHyperlink = child.marks.some((mark) => mark.type.name === "hyperlink");
166
+ if (child.type.name === "bookmarkBoundary" && !hasHyperlink) issues.push({
167
+ path: childPath,
168
+ message: "Bookmark boundaries inside fields require a hyperlink parent."
169
+ });
170
+ });
171
+ }
172
+ }
80
173
  return;
81
174
  case "math":
82
175
  appendAttrIssues(path, readMathAttrs(node), issues);
@@ -1,5 +1,13 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
  //#region src/utils/textFormattingMerge.d.ts
3
+ declare const STYLE_TOGGLE_KEYS: readonly ["bold", "boldCs", "italic", "italicCs", "allCaps", "emboss", "imprint", "outline", "shadow", "smallCaps", "strike", "hidden"];
4
+ /**
5
+ * Merge the properties exposed by one resolved style definition.
6
+ *
7
+ * A false toggle at a child style level does not cancel the inherited state. The
8
+ * ordinary merge remains unchanged for direct formatting and non-toggle properties.
9
+ */
10
+ declare function mergeStyleTextFormatting(target: document_d_exports.TextFormatting | undefined, source: document_d_exports.TextFormatting | undefined): document_d_exports.TextFormatting | undefined;
3
11
  declare function mergeTextFormatting(target: document_d_exports.TextFormatting | undefined, source: document_d_exports.TextFormatting | undefined): document_d_exports.TextFormatting | undefined;
4
12
  //#endregion
5
- export { mergeTextFormatting };
13
+ export { STYLE_TOGGLE_KEYS, mergeStyleTextFormatting, mergeTextFormatting };
@@ -1,5 +1,36 @@
1
1
  import { mergeFontFamily } from "./fontFamilyMerge.js";
2
2
  //#region src/utils/textFormattingMerge.ts
3
+ const STYLE_TOGGLE_KEYS = [
4
+ "bold",
5
+ "boldCs",
6
+ "italic",
7
+ "italicCs",
8
+ "allCaps",
9
+ "emboss",
10
+ "imprint",
11
+ "outline",
12
+ "shadow",
13
+ "smallCaps",
14
+ "strike",
15
+ "hidden"
16
+ ];
17
+ /**
18
+ * Merge the properties exposed by one resolved style definition.
19
+ *
20
+ * A false toggle at a child style level does not cancel the inherited state. The
21
+ * ordinary merge remains unchanged for direct formatting and non-toggle properties.
22
+ */
23
+ function mergeStyleTextFormatting(target, source) {
24
+ const result = mergeTextFormatting(target, source);
25
+ if (!result || !source) return result;
26
+ for (const key of STYLE_TOGGLE_KEYS) {
27
+ if (source[key] !== false) continue;
28
+ const inherited = target?.[key];
29
+ if (inherited === void 0) Reflect.deleteProperty(result, key);
30
+ else result[key] = inherited;
31
+ }
32
+ return result;
33
+ }
3
34
  function mergeTextFormatting(target, source) {
4
35
  if (!source && !target) return;
5
36
  if (!source) return target;
@@ -28,4 +59,4 @@ function mergeTextFormatting(target, source) {
28
59
  return result;
29
60
  }
30
61
  //#endregion
31
- export { mergeTextFormatting };
62
+ export { STYLE_TOGGLE_KEYS, mergeStyleTextFormatting, mergeTextFormatting };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.28.0",
3
+ "version": "0.29.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",