@stll/folio-core 0.42.0 → 0.43.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/compat/eigenpal.d.ts +2 -2
- package/dist/controller/layoutPipeline.d.ts +2 -1
- package/dist/controller/layoutPipeline.js +5 -2
- package/dist/controller/layoutSession.d.ts +2 -1
- package/dist/controller/layoutSession.js +1 -0
- package/dist/docx/blockContentParser.js +2 -1
- package/dist/docx/commentIdNormalization.d.ts +8 -0
- package/dist/docx/commentIdNormalization.js +16 -0
- package/dist/docx/defaultParagraphStyle.d.ts +22 -0
- package/dist/docx/defaultParagraphStyle.js +32 -0
- package/dist/docx/footnoteParser.js +2 -0
- package/dist/docx/headerFooterRefParser.d.ts +12 -1
- package/dist/docx/headerFooterRefParser.js +9 -2
- package/dist/docx/numberingParser.js +2 -1
- package/dist/docx/numberingReference.d.ts +21 -0
- package/dist/docx/numberingReference.js +21 -0
- package/dist/docx/numberingReferenceNormalization.d.ts +11 -2
- package/dist/docx/numberingReferenceNormalization.js +32 -9
- package/dist/docx/paragraphParser.js +3 -2
- package/dist/docx/parser.js +10 -2
- package/dist/docx/rezip.js +22 -5
- package/dist/docx/server/createBilingualDocument.js +5 -4
- package/dist/docx/styleParser.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/internal/pageBreakRunSourceDescendantIndex.d.ts +2 -0
- package/dist/internal/pageBreakRunSourceDescendantIndex.js +7 -5
- package/dist/layout-bridge/convert/templatePreviewFlow.d.ts +19 -11
- package/dist/layout-bridge/convert/templatePreviewFlow.js +103 -38
- package/dist/layout-bridge/convert/toFlowBlocks.js +12 -3
- package/dist/prosemirror/commands/pageBreak.js +12 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +2 -1
- package/dist/prosemirror/conversion/toProseDoc.js +27 -12
- package/dist/prosemirror/extensions/features/ListExtension.js +42 -4
- package/dist/prosemirror/listMarker.js +2 -1
- package/dist/prosemirror/numberedRefFields.js +2 -1
- package/dist/prosemirror/pageBreakRunProjection.d.ts +11 -3
- package/dist/prosemirror/pageBreakRunProjection.js +16 -8
- package/dist/prosemirror/plugins/templatePreviewValues.d.ts +42 -1
- package/dist/prosemirror/plugins/templatePreviewValues.js +217 -14
- package/dist/prosemirror/styles/resolvedStyleAttrs.js +2 -1
- package/dist/prosemirror/styles/styleResolver.js +3 -6
- package/dist/prosemirror/utils/visualLineNavigation.d.ts +22 -2
- package/dist/prosemirror/utils/visualLineNavigation.js +80 -63
- package/dist/style-engine/styleEngine.d.ts +1 -1
- package/dist/style-sets/extract.js +57 -11
- package/package.json +2 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { findCollapsedLineEdgeCaretTarget } from "../../layout-bridge/dom/clickToPositionDom.js";
|
|
1
2
|
import { findBodyEmptyRuns, findBodyPmSpans } from "../../layout-bridge/dom/findBodyPmSpans.js";
|
|
2
3
|
import { createTextStreamRange, descendantTextNodes, totalTextLength } from "../../layout-bridge/dom/textStreamDom.js";
|
|
3
4
|
import { findVerticalScrollParent } from "../../utils/findVerticalScrollParent.js";
|
|
4
|
-
import { TextSelection } from "prosemirror-state";
|
|
5
|
+
import { Selection, TextSelection } from "prosemirror-state";
|
|
5
6
|
//#region src/prosemirror/utils/visualLineNavigation.ts
|
|
6
7
|
/**
|
|
7
8
|
* Visual line navigation helpers — implements Word/Google-Docs-style
|
|
@@ -17,12 +18,10 @@ import { TextSelection } from "prosemirror-state";
|
|
|
17
18
|
* @internal
|
|
18
19
|
*/
|
|
19
20
|
const CONTENT_LINE_SELECTOR = ".layout-page-content .layout-line";
|
|
21
|
+
const PM_SPAN_SELECTOR = "span[data-pm-start][data-pm-end]";
|
|
20
22
|
/** @internal */
|
|
21
23
|
function createVisualLineState() {
|
|
22
|
-
return {
|
|
23
|
-
stickyX: null,
|
|
24
|
-
lastVisualLineIndex: -1
|
|
25
|
-
};
|
|
24
|
+
return { step: null };
|
|
26
25
|
}
|
|
27
26
|
function scrollIntoViewIfNeeded(el) {
|
|
28
27
|
const container = findVerticalScrollParent(el);
|
|
@@ -35,7 +34,10 @@ function scrollIntoViewIfNeeded(el) {
|
|
|
35
34
|
}
|
|
36
35
|
/** @internal */
|
|
37
36
|
function getCaretClientX(container, pmPos) {
|
|
38
|
-
|
|
37
|
+
const spans = findBodyPmSpans(container);
|
|
38
|
+
const collapsedTarget = findCollapsedLineEdgeCaretTarget(spans, pmPos);
|
|
39
|
+
if (collapsedTarget) return collapsedTarget.geometry.left;
|
|
40
|
+
for (const spanEl of spans) {
|
|
39
41
|
const pmStart = Number(spanEl.dataset["pmStart"]);
|
|
40
42
|
const pmEnd = Number(spanEl.dataset["pmEnd"]);
|
|
41
43
|
if (spanEl.classList.contains("layout-run-tab")) {
|
|
@@ -58,21 +60,24 @@ function getCaretClientX(container, pmPos) {
|
|
|
58
60
|
}
|
|
59
61
|
return null;
|
|
60
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Whether one painted line carries `pmPos`. Span endpoints are inclusive, so a
|
|
65
|
+
* soft-wrap boundary answers true for both the line it ends and the line it
|
|
66
|
+
* starts.
|
|
67
|
+
*/
|
|
68
|
+
const lineCarriesPosition = (lineEl, pmPos) => {
|
|
69
|
+
for (const span of Array.from(lineEl.querySelectorAll(PM_SPAN_SELECTOR))) {
|
|
70
|
+
const start = Number(span.dataset["pmStart"]);
|
|
71
|
+
const end = Number(span.dataset["pmEnd"]);
|
|
72
|
+
if (pmPos >= start && pmPos <= end) return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
};
|
|
61
76
|
/** @internal */
|
|
62
77
|
function findLineElementAtPosition(container, pmPos) {
|
|
63
78
|
const allLines = container.querySelectorAll(CONTENT_LINE_SELECTOR);
|
|
64
|
-
for (const
|
|
65
|
-
|
|
66
|
-
const spans = lineEl.querySelectorAll("span[data-pm-start][data-pm-end]");
|
|
67
|
-
for (const span of Array.from(spans)) {
|
|
68
|
-
const s = span;
|
|
69
|
-
const start = Number(s.dataset["pmStart"]);
|
|
70
|
-
const end = Number(s.dataset["pmEnd"]);
|
|
71
|
-
if (pmPos >= start && pmPos <= end) return lineEl;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
for (const line of Array.from(allLines)) {
|
|
75
|
-
const lineEl = line;
|
|
79
|
+
for (const lineEl of Array.from(allLines)) if (lineCarriesPosition(lineEl, pmPos)) return lineEl;
|
|
80
|
+
for (const lineEl of Array.from(allLines)) {
|
|
76
81
|
const paragraph = lineEl.closest(".layout-paragraph");
|
|
77
82
|
if (!paragraph) continue;
|
|
78
83
|
const pStart = Number(paragraph.dataset["pmStart"]);
|
|
@@ -145,6 +150,36 @@ function findPositionOnLineAtClientX(lineEl, clientX) {
|
|
|
145
150
|
return clientX < closestSpan.getBoundingClientRect().left ? Number(closestSpan.dataset["pmStart"]) : Number(closestSpan.dataset["pmEnd"]);
|
|
146
151
|
}
|
|
147
152
|
/**
|
|
153
|
+
* The step the next vertical move continues from: the remembered one while the
|
|
154
|
+
* caret is still where that move left it, the caret's own painted geometry
|
|
155
|
+
* otherwise.
|
|
156
|
+
*/
|
|
157
|
+
const resolveVisualLineStep = ({ state, container, allLines, pmPos }) => {
|
|
158
|
+
const remembered = state.step;
|
|
159
|
+
if (remembered?.pmPos === pmPos) {
|
|
160
|
+
const rememberedLine = allLines[remembered.lineIndex];
|
|
161
|
+
if (rememberedLine && lineCarriesPosition(rememberedLine, pmPos)) return remembered;
|
|
162
|
+
}
|
|
163
|
+
const currentLine = findLineElementAtPosition(container, pmPos);
|
|
164
|
+
if (!currentLine) return null;
|
|
165
|
+
const lineIndex = allLines.indexOf(currentLine);
|
|
166
|
+
if (lineIndex === -1) return null;
|
|
167
|
+
const stickyX = getCaretClientX(container, pmPos);
|
|
168
|
+
return stickyX === null ? null : {
|
|
169
|
+
stickyX,
|
|
170
|
+
lineIndex,
|
|
171
|
+
pmPos
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
const createVerticalSelection = ({ doc, anchor, head, extend }) => {
|
|
175
|
+
try {
|
|
176
|
+
return extend ? TextSelection.create(doc, anchor, head) : TextSelection.create(doc, head);
|
|
177
|
+
} catch {
|
|
178
|
+
const $head = doc.resolve(head);
|
|
179
|
+
return extend ? TextSelection.between(doc.resolve(anchor), $head) : Selection.near($head);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
148
183
|
* Handle PM ArrowUp / ArrowDown with visual-line awareness + sticky
|
|
149
184
|
* X. Returns true if the event was handled and PM should not run
|
|
150
185
|
* its default behaviour. Mutates `state` so consecutive presses
|
|
@@ -153,59 +188,41 @@ function findPositionOnLineAtClientX(lineEl, clientX) {
|
|
|
153
188
|
* @internal
|
|
154
189
|
*/
|
|
155
190
|
function handleVisualLineKeyDown(state, view, event, container) {
|
|
156
|
-
if (event.key !== "ArrowUp" && event.key !== "ArrowDown")
|
|
157
|
-
|
|
158
|
-
"ArrowLeft",
|
|
159
|
-
"ArrowRight",
|
|
160
|
-
"Home",
|
|
161
|
-
"End"
|
|
162
|
-
].includes(event.key) || event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
|
|
163
|
-
state.stickyX = null;
|
|
164
|
-
state.lastVisualLineIndex = -1;
|
|
165
|
-
}
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
if (event.ctrlKey || event.metaKey) {
|
|
169
|
-
state.stickyX = null;
|
|
170
|
-
state.lastVisualLineIndex = -1;
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
191
|
+
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return false;
|
|
192
|
+
if (event.ctrlKey || event.metaKey) return false;
|
|
173
193
|
if (!container) return false;
|
|
174
194
|
const allLines = Array.from(container.querySelectorAll(CONTENT_LINE_SELECTOR));
|
|
175
195
|
if (allLines.length === 0) return false;
|
|
176
|
-
const {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (currentIndex === -1) return false;
|
|
189
|
-
}
|
|
190
|
-
const targetIndex = event.key === "ArrowUp" ? currentIndex - 1 : currentIndex + 1;
|
|
191
|
-
if (targetIndex < 0 || targetIndex >= allLines.length) {
|
|
192
|
-
state.lastVisualLineIndex = -1;
|
|
196
|
+
const { head, anchor } = view.state.selection;
|
|
197
|
+
const step = resolveVisualLineStep({
|
|
198
|
+
state,
|
|
199
|
+
container,
|
|
200
|
+
allLines,
|
|
201
|
+
pmPos: head
|
|
202
|
+
});
|
|
203
|
+
if (!step) return false;
|
|
204
|
+
const targetIndex = event.key === "ArrowUp" ? step.lineIndex - 1 : step.lineIndex + 1;
|
|
205
|
+
const targetLine = allLines[targetIndex];
|
|
206
|
+
if (!targetLine) {
|
|
207
|
+
state.step = null;
|
|
193
208
|
return false;
|
|
194
209
|
}
|
|
195
|
-
const
|
|
196
|
-
const newPos = findPositionOnLineAtClientX(targetLine, state.stickyX);
|
|
210
|
+
const newPos = findPositionOnLineAtClientX(targetLine, step.stickyX);
|
|
197
211
|
if (newPos === null) return false;
|
|
198
|
-
state.lastVisualLineIndex = targetIndex;
|
|
199
212
|
const { state: pmState, dispatch } = view;
|
|
200
213
|
const clampedPos = Math.max(0, Math.min(newPos, pmState.doc.content.size));
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
214
|
+
const selection = createVerticalSelection({
|
|
215
|
+
doc: pmState.doc,
|
|
216
|
+
anchor,
|
|
217
|
+
head: clampedPos,
|
|
218
|
+
extend: event.shiftKey
|
|
219
|
+
});
|
|
220
|
+
dispatch(pmState.tr.setSelection(selection));
|
|
221
|
+
state.step = {
|
|
222
|
+
stickyX: step.stickyX,
|
|
223
|
+
lineIndex: targetIndex,
|
|
224
|
+
pmPos: selection.head
|
|
225
|
+
};
|
|
209
226
|
scrollIntoViewIfNeeded(targetLine);
|
|
210
227
|
return true;
|
|
211
228
|
}
|
|
@@ -34,7 +34,7 @@ type StyleEngine = {
|
|
|
34
34
|
hasStyle: (styleId: string) => boolean;
|
|
35
35
|
/** Return the document-wide defaults (`w:docDefaults`). */
|
|
36
36
|
getDocDefaults: () => document_d_exports.DocDefaults | undefined;
|
|
37
|
-
/** The
|
|
37
|
+
/** The paragraph default per ECMA-376 17.7.4.17, else the built-in Normal. */
|
|
38
38
|
getDefaultParagraphStyle: () => document_d_exports.Style | undefined;
|
|
39
39
|
/** The style flagged `w:default="1"` for character styles. */
|
|
40
40
|
getDefaultCharacterStyle: () => document_d_exports.Style | undefined;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { BUILT_IN_DEFAULT_PARAGRAPH_FORMATTING, BUILT_IN_DEFAULT_PARAGRAPH_STYLE_ID, BUILT_IN_DEFAULT_PARAGRAPH_STYLE_NAME, resolveDefaultParagraphStyle } from "../docx/defaultParagraphStyle.js";
|
|
2
|
+
import { getCachedNumberingMap } from "../docx/numberingParser.js";
|
|
3
|
+
import { isNumberingReference } from "../docx/numberingReference.js";
|
|
4
|
+
import { normalizeStyleNumberingReferences } from "../docx/numberingReferenceNormalization.js";
|
|
1
5
|
import { parseDocx } from "../docx/parser.js";
|
|
2
6
|
import "./types.js";
|
|
3
7
|
import { panic } from "better-result";
|
|
4
8
|
//#region src/style-sets/extract.ts
|
|
5
9
|
const inspectDocumentStyles = (document) => {
|
|
6
10
|
const styles = document.package.styles?.styles ?? [];
|
|
7
|
-
const defaultParagraphStyleId = styles
|
|
11
|
+
const defaultParagraphStyleId = resolveDefaultParagraphStyle(styles)?.styleId;
|
|
8
12
|
return {
|
|
9
13
|
...defaultParagraphStyleId ? { defaultParagraphStyleId } : {},
|
|
10
14
|
styles: styles.map(toCatalogEntry)
|
|
@@ -24,15 +28,21 @@ const inspectDocumentStylesFromDocx = async (input) => inspectDocumentStyles(awa
|
|
|
24
28
|
* are retained. No source content or package relationship can enter the result.
|
|
25
29
|
*/
|
|
26
30
|
const extractDocumentStyleSet = (document, options) => {
|
|
31
|
+
if (options.styleIds?.length === 0) return panic("Cannot extract an empty style set");
|
|
27
32
|
const definitions = document.package.styles;
|
|
28
|
-
|
|
29
|
-
const stylesById = new Map(
|
|
33
|
+
const sourceStyles = definitions?.styles ?? [];
|
|
34
|
+
const stylesById = new Map(sourceStyles.map((style) => [style.styleId, style]));
|
|
30
35
|
const selectedStyleIds = options.styleIds === void 0 ? new Set(stylesById.keys()) : collectStyleDependencyClosure(stylesById, options.styleIds);
|
|
31
|
-
const styles =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
const styles = structuredClone(sourceStyles.filter((style) => selectedStyleIds.has(style.styleId)));
|
|
37
|
+
normalizeStyleNumberingReferences({
|
|
38
|
+
styles,
|
|
39
|
+
numbering: document.package.numbering ? getCachedNumberingMap(document.package.numbering) : void 0
|
|
40
|
+
});
|
|
41
|
+
const initialParagraphStyleId = ensureInitialParagraphStyle({
|
|
42
|
+
styles,
|
|
43
|
+
requested: options.initialParagraphStyleId,
|
|
44
|
+
hasDocDefaults: definitions?.docDefaults !== void 0
|
|
45
|
+
});
|
|
36
46
|
const numbering = extractReferencedNumbering(styles, document);
|
|
37
47
|
const fontTable = sanitizeFontTable(document.package.fontTable);
|
|
38
48
|
return structuredClone({
|
|
@@ -40,8 +50,8 @@ const extractDocumentStyleSet = (document, options) => {
|
|
|
40
50
|
name: options.name,
|
|
41
51
|
initialParagraphStyleId,
|
|
42
52
|
styles: {
|
|
43
|
-
...definitions
|
|
44
|
-
...definitions
|
|
53
|
+
...definitions?.docDefaults ? { docDefaults: definitions.docDefaults } : {},
|
|
54
|
+
...definitions?.latentStyles ? { latentStyles: definitions.latentStyles } : {},
|
|
45
55
|
styles
|
|
46
56
|
},
|
|
47
57
|
...numbering ? { numbering } : {},
|
|
@@ -59,6 +69,42 @@ const extractDocumentStyleSetFromDocx = async (input, options) => {
|
|
|
59
69
|
});
|
|
60
70
|
return extractDocumentStyleSet(document, options);
|
|
61
71
|
};
|
|
72
|
+
const ensureInitialParagraphStyle = ({ styles, requested, hasDocDefaults }) => {
|
|
73
|
+
if (requested !== void 0) {
|
|
74
|
+
if (!styles.find((style) => style.styleId === requested && style.type === "paragraph")) return panic(`Initial paragraph style "${requested}" is not present in the extracted set`);
|
|
75
|
+
return requested;
|
|
76
|
+
}
|
|
77
|
+
const resolved = resolveDefaultParagraphStyle(styles);
|
|
78
|
+
if (resolved) return resolved.styleId;
|
|
79
|
+
const minted = mintDefaultParagraphStyle({
|
|
80
|
+
takenStyleIds: new Set(styles.map((style) => style.styleId)),
|
|
81
|
+
hasDocDefaults
|
|
82
|
+
});
|
|
83
|
+
styles.push(minted);
|
|
84
|
+
return minted.styleId;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* The default paragraph style a set needs when its source declared none.
|
|
88
|
+
*
|
|
89
|
+
* The id only has to be free, because the set is what defines it. The
|
|
90
|
+
* formatting has to be the built-in template's whenever the source had no
|
|
91
|
+
* `w:docDefaults`, because that is what the source itself rendered as: a
|
|
92
|
+
* consumer applies its built-in Normal only where no default paragraph style
|
|
93
|
+
* exists, and this minted style is one. Where the source did declare
|
|
94
|
+
* `w:docDefaults`, the set carries them and they remain authoritative, so the
|
|
95
|
+
* minted style states nothing.
|
|
96
|
+
*/
|
|
97
|
+
const mintDefaultParagraphStyle = ({ takenStyleIds, hasDocDefaults }) => {
|
|
98
|
+
let styleId = BUILT_IN_DEFAULT_PARAGRAPH_STYLE_ID;
|
|
99
|
+
for (let suffix = 1; takenStyleIds.has(styleId); suffix += 1) styleId = `${BUILT_IN_DEFAULT_PARAGRAPH_STYLE_ID}${suffix}`;
|
|
100
|
+
return {
|
|
101
|
+
styleId,
|
|
102
|
+
type: "paragraph",
|
|
103
|
+
name: BUILT_IN_DEFAULT_PARAGRAPH_STYLE_NAME,
|
|
104
|
+
default: true,
|
|
105
|
+
...hasDocDefaults ? {} : { pPr: { ...BUILT_IN_DEFAULT_PARAGRAPH_FORMATTING } }
|
|
106
|
+
};
|
|
107
|
+
};
|
|
62
108
|
const collectStyleDependencyClosure = (stylesById, requestedStyleIds) => {
|
|
63
109
|
const selected = /* @__PURE__ */ new Set();
|
|
64
110
|
const pending = [...requestedStyleIds];
|
|
@@ -82,7 +128,7 @@ const extractReferencedNumbering = (styles, document) => {
|
|
|
82
128
|
const referencedNumIds = /* @__PURE__ */ new Set();
|
|
83
129
|
for (const style of styles) {
|
|
84
130
|
const numId = style.pPr?.numPr?.numId;
|
|
85
|
-
if (numId
|
|
131
|
+
if (isNumberingReference(numId)) referencedNumIds.add(numId);
|
|
86
132
|
}
|
|
87
133
|
if (referencedNumIds.size === 0) return;
|
|
88
134
|
const nums = source.nums.filter((numbering) => referencedNumIds.has(numbering.numId));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.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",
|
|
@@ -134,6 +134,7 @@
|
|
|
134
134
|
"prosemirror-dropcursor": "^1.8.2",
|
|
135
135
|
"prosemirror-gapcursor": "^1.4.1",
|
|
136
136
|
"prosemirror-history": "^1.5.0",
|
|
137
|
+
"prosemirror-inputrules": "^1.5.0",
|
|
137
138
|
"prosemirror-keymap": "^1.2.3",
|
|
138
139
|
"prosemirror-model": "^1.25.9",
|
|
139
140
|
"prosemirror-state": "^1.4.4",
|