@stll/folio-core 0.6.0 → 0.7.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/controller/layoutPipeline.js +33 -14
- package/dist/docx/blockContentParser.js +2 -100
- package/dist/docx/groupDrawingParser.d.ts +1 -1
- package/dist/docx/groupDrawingParser.js +49 -8
- package/dist/docx/paragraphTextBoxEnrichment.d.ts +9 -0
- package/dist/docx/paragraphTextBoxEnrichment.js +104 -0
- package/dist/docx/runParser.js +11 -2
- package/dist/docx/server/boundedArchive.d.ts +24 -0
- package/dist/docx/server/boundedArchive.js +106 -0
- package/dist/docx/server/extractDocxText.d.ts +23 -0
- package/dist/docx/server/extractDocxText.js +154 -0
- package/dist/docx/tableParser.js +2 -0
- package/dist/layout-bridge/convert/toFlowBlocks.js +75 -19
- package/dist/layout-bridge/sectionColumns.js +6 -1
- package/dist/layout-engine/index.js +120 -19
- package/dist/layout-engine/keep-together.d.ts +7 -5
- package/dist/layout-engine/keep-together.js +20 -4
- package/dist/layout-engine/measure/cache.js +2 -0
- package/dist/layout-engine/measure/measureBlocks.js +3 -2
- package/dist/layout-engine/measure/measureParagraph.js +30 -12
- package/dist/layout-engine/paginator.d.ts +2 -0
- package/dist/layout-engine/paginator.js +27 -15
- package/dist/layout-engine/tableRowBreak.js +3 -0
- package/dist/layout-engine/types.d.ts +20 -5
- package/dist/layout-painter/index.js +1 -1
- package/dist/layout-painter/renderPage.js +7 -2
- package/dist/layout-painter/renderParagraph.js +93 -9
- package/dist/layout-painter/renderTable.js +88 -10
- package/dist/paged-layout/sectionBlockWidths.js +11 -3
- package/dist/prosemirror/conversion/fromProseDoc.js +11 -2
- package/dist/prosemirror/conversion/toProseDoc.js +28 -20
- package/dist/prosemirror/extensions/nodes/TableExtension.js +3 -2
- package/dist/prosemirror/schema/nodes.d.ts +2 -1
- package/dist/prosemirror/utils/tabCalculator.js +1 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +3 -1
- package/dist/utils/formatToStyle.js +3 -3
- package/dist/utils/units.d.ts +6 -6
- package/dist/utils/units.js +8 -8
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import "../layout-engine/types.js";
|
|
2
2
|
import { renderPages } from "../layout-painter/renderPage.js";
|
|
3
3
|
import { templatePreviewValuesKey } from "../prosemirror/plugins/templatePreviewValues.js";
|
|
4
|
+
import { getMargins, getPageSize, twipsToPixels } from "../paged-layout/sectionGeometry.js";
|
|
5
|
+
import { getColumns } from "../layout-bridge/sectionColumns.js";
|
|
4
6
|
import { toFlowBlocks } from "../layout-bridge/convert/toFlowBlocks.js";
|
|
5
7
|
import { recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordLayoutStart } from "../layout-engine/layoutInstrumentation.js";
|
|
6
8
|
import { buildBookmarkPageMap } from "../fields/bookmarkPages.js";
|
|
@@ -15,9 +17,17 @@ import { layoutDocument } from "../layout-engine/index.js";
|
|
|
15
17
|
import { measureBlocks, measureSingleBlockWithoutFloatingZones } from "../layout-engine/measure/measureBlocks.js";
|
|
16
18
|
import { tryBuildIncrementalMeasures } from "../paged-layout/incrementalMeasure.js";
|
|
17
19
|
import { computePerBlockMeasureInputs } from "../paged-layout/sectionBlockWidths.js";
|
|
18
|
-
import { getMargins, getPageSize, twipsToPixels } from "../paged-layout/sectionGeometry.js";
|
|
19
20
|
import { getDocumentWatermark } from "../watermark/index.js";
|
|
20
21
|
//#region src/controller/layoutPipeline.ts
|
|
22
|
+
function bodyMarginsBelowHeader(authoredMargins, preparedHeader) {
|
|
23
|
+
if (!preparedHeader) return authoredMargins;
|
|
24
|
+
const headerBottom = (authoredMargins.header ?? 0) + (preparedHeader.marginPushBottom ?? preparedHeader.height);
|
|
25
|
+
if (headerBottom <= authoredMargins.top) return authoredMargins;
|
|
26
|
+
return {
|
|
27
|
+
...authoredMargins,
|
|
28
|
+
top: headerBottom
|
|
29
|
+
};
|
|
30
|
+
}
|
|
21
31
|
function runLayoutPipeline(deps, state, options = {}) {
|
|
22
32
|
const { contentWidth, columns, pageSize, margins, pageGap, syncCoordinator, headerContent, footerContent, firstPageHeaderContent, firstPageFooterContent, headerContentRId, footerContentRId, firstPageHeaderContentRId, firstPageFooterContentRId, sectionHeaderFooterRefs, theme: _theme, sectionProperties, document, defaultTabStop, mirrorMargins, styles, layout, hfPMs, painter, pagesContainer, session, renderHfFromContentOrPm, renderHeaderFooterContentByRId, documentFontsAreLoaded, buildFootnoteRenderItems, describeInvalidHighlightMarks, emptyTemplatePreviewEntries: EMPTY_TEMPLATE_PREVIEW_ENTRIES } = deps;
|
|
23
33
|
let outcome = {};
|
|
@@ -91,17 +101,29 @@ function runLayoutPipeline(deps, state, options = {}) {
|
|
|
91
101
|
let firstPageFooterForRender = hasTitlePg ? renderHfFromContentOrPm(firstPageFooterContent, firstPageFooterContentRId, hfPMs, contentWidth, hfMetricsFooter, hfOptions) : void 0;
|
|
92
102
|
let headerContentByRId = renderHeaderFooterContentByRId(document?.package.headers, hfPMs, contentWidth, hfMetricsHeader, hfOptions);
|
|
93
103
|
let footerContentByRId = renderHeaderFooterContentByRId(document?.package.footers, hfPMs, contentWidth, hfMetricsFooter, hfOptions);
|
|
104
|
+
const initialBodyMargins = bodyMarginsBelowHeader(margins, headerContentForRender);
|
|
94
105
|
recordPhaseDuration("header-footer", phaseStartedAt);
|
|
95
106
|
phaseStartedAt = performance.now();
|
|
96
107
|
const bodyLayoutConfig = {
|
|
97
108
|
pageSize,
|
|
98
|
-
margins
|
|
109
|
+
margins: initialBodyMargins
|
|
99
110
|
};
|
|
100
111
|
if (columns !== void 0) bodyLayoutConfig.columns = columns;
|
|
112
|
+
const finalSectionProperties = document?.package.document.sections?.at(-1)?.properties;
|
|
113
|
+
const finalHeaderRId = sectionHeaderFooterRefs?.at(-1)?.headerDefault;
|
|
114
|
+
let finalHeaderForLayout = headerContentForRender;
|
|
115
|
+
if (finalHeaderRId) finalHeaderForLayout = headerContentByRId?.get(finalHeaderRId);
|
|
116
|
+
else if (sectionHeaderFooterRefs !== void 0) finalHeaderForLayout = void 0;
|
|
117
|
+
const finalLayoutConfig = finalSectionProperties ? {
|
|
118
|
+
pageSize: getPageSize(finalSectionProperties),
|
|
119
|
+
margins: bodyMarginsBelowHeader(getMargins(finalSectionProperties), finalHeaderForLayout)
|
|
120
|
+
} : bodyLayoutConfig;
|
|
121
|
+
const finalColumns = getColumns(finalSectionProperties);
|
|
122
|
+
if (finalColumns !== void 0) finalLayoutConfig.columns = finalColumns;
|
|
101
123
|
const blockMeasureInputs = computePerBlockMeasureInputs({
|
|
102
124
|
blocks: newBlocks,
|
|
103
125
|
bodyConfig: bodyLayoutConfig,
|
|
104
|
-
finalConfig:
|
|
126
|
+
finalConfig: finalLayoutConfig
|
|
105
127
|
});
|
|
106
128
|
const blockWidths = blockMeasureInputs.widths;
|
|
107
129
|
const previousArtifacts = session.artifacts;
|
|
@@ -133,22 +155,19 @@ function runLayoutPipeline(deps, state, options = {}) {
|
|
|
133
155
|
const buildLayoutOpts = () => {
|
|
134
156
|
const nextLayoutOpts = {
|
|
135
157
|
pageSize,
|
|
136
|
-
margins,
|
|
158
|
+
margins: initialBodyMargins,
|
|
137
159
|
pageGap,
|
|
138
160
|
mirrorMargins
|
|
139
161
|
};
|
|
140
|
-
if (hasTitlePg
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
if (hasTitlePg) nextLayoutOpts.firstPageMargins = bodyMarginsBelowHeader(margins, firstPageHeaderForRender);
|
|
163
|
+
if (finalSectionProperties) {
|
|
164
|
+
nextLayoutOpts.finalPageSize = finalLayoutConfig.pageSize;
|
|
165
|
+
nextLayoutOpts.finalMargins = finalLayoutConfig.margins;
|
|
166
|
+
nextLayoutOpts.finalColumns = finalLayoutConfig.columns ?? {
|
|
167
|
+
count: 1,
|
|
168
|
+
gap: 0
|
|
145
169
|
};
|
|
146
170
|
}
|
|
147
|
-
const finalSection = document?.package.document.sections?.at(-1);
|
|
148
|
-
if (finalSection) {
|
|
149
|
-
nextLayoutOpts.finalPageSize = getPageSize(finalSection.properties);
|
|
150
|
-
nextLayoutOpts.finalMargins = getMargins(finalSection.properties);
|
|
151
|
-
}
|
|
152
171
|
if (columns !== void 0) nextLayoutOpts.columns = columns;
|
|
153
172
|
if (bodyBreakType !== void 0) nextLayoutOpts.bodyBreakType = bodyBreakType;
|
|
154
173
|
if (sectionHeaderFooterRefs !== void 0) nextLayoutOpts.sectionHeaderFooterRefs = sectionHeaderFooterRefs;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { elementToXml, findChild,
|
|
1
|
+
import { elementToXml, findChild, getChildElements, getLocalName, mergeXmlnsDeclarations } from "./xmlParser.js";
|
|
2
2
|
import { parseBookmarkEnd, parseBookmarkStart } from "./bookmarkParser.js";
|
|
3
|
-
import { getTextBoxContentElement, isTextBoxDrawing, parseTextBox, parseTextBoxContent } from "./textBoxParser.js";
|
|
4
3
|
import { parseSdtProperties } from "./sdtProperties.js";
|
|
5
4
|
import { parseParagraph } from "./paragraphParser.js";
|
|
6
5
|
import { appendBookmarkMarkerToLastParagraphInBlocks, prependBookmarkMarkersToFirstParagraphInBlocks } from "./bookmarkPlacement.js";
|
|
6
|
+
import { enrichParagraphTextBoxes } from "./paragraphTextBoxEnrichment.js";
|
|
7
7
|
import { parseTable } from "./tableParser.js";
|
|
8
8
|
import { padDecimal } from "./numberingParser.js";
|
|
9
9
|
import { convertBulletToUnicode } from "./bulletMarkers.js";
|
|
@@ -100,104 +100,6 @@ const computeListMarker = (paragraph, numbering, listCounters, abstractCounters)
|
|
|
100
100
|
}
|
|
101
101
|
listRendering.marker = computedMarker;
|
|
102
102
|
};
|
|
103
|
-
const enrichParagraphTextBoxes = (paragraph, paraXml, styles, theme, numbering, rels, media) => {
|
|
104
|
-
const xmlChildren = getChildElements(paraXml);
|
|
105
|
-
let parsedIndex = 0;
|
|
106
|
-
let lastConsumedRun;
|
|
107
|
-
for (const xmlChild of xmlChildren) {
|
|
108
|
-
if (getLocalName(xmlChild.name ?? "") !== "r") {
|
|
109
|
-
if (parsedIndex < paragraph.content.length && paragraph.content[parsedIndex]?.type !== "run") parsedIndex += 1;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
const { textBoxDrawings, hasNonTextBoxContent } = scanRunForTextBoxDrawings(xmlChild);
|
|
113
|
-
const parsedContent = paragraph.content[parsedIndex];
|
|
114
|
-
const parsedRun = parsedContent?.type === "run" ? parsedContent : void 0;
|
|
115
|
-
const targetRun = parsedRun ?? (hasNonTextBoxContent ? lastConsumedRun : void 0);
|
|
116
|
-
for (const runEl of textBoxDrawings) {
|
|
117
|
-
const textBox = parseTextBox(runEl);
|
|
118
|
-
if (!textBox) continue;
|
|
119
|
-
const wsp = findDeep(runEl, "wps", "wsp");
|
|
120
|
-
if (wsp) {
|
|
121
|
-
const txbxContentEl = getTextBoxContentElement(wsp);
|
|
122
|
-
if (txbxContentEl) textBox.content = parseTextBoxContent(txbxContentEl, parseParagraph, null, styles, theme, numbering, rels ?? void 0, media ?? void 0);
|
|
123
|
-
}
|
|
124
|
-
const shape = {
|
|
125
|
-
type: "shape",
|
|
126
|
-
shapeType: "textBox",
|
|
127
|
-
size: textBox.size,
|
|
128
|
-
...textBox.position !== void 0 ? { position: textBox.position } : {},
|
|
129
|
-
...textBox.wrap !== void 0 ? { wrap: textBox.wrap } : {},
|
|
130
|
-
...textBox.fill !== void 0 ? { fill: textBox.fill } : {},
|
|
131
|
-
...textBox.outline !== void 0 ? { outline: textBox.outline } : {},
|
|
132
|
-
textBody: {
|
|
133
|
-
content: textBox.content,
|
|
134
|
-
...textBox.margins !== void 0 ? { margins: textBox.margins } : {}
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
if (textBox.id) shape.id = textBox.id;
|
|
138
|
-
const shapeContent = {
|
|
139
|
-
type: "shape",
|
|
140
|
-
shape
|
|
141
|
-
};
|
|
142
|
-
if (targetRun && hasNonTextBoxContent) targetRun.content.push(shapeContent);
|
|
143
|
-
else {
|
|
144
|
-
const newRun = {
|
|
145
|
-
type: "run",
|
|
146
|
-
content: [shapeContent]
|
|
147
|
-
};
|
|
148
|
-
paragraph.content.splice(parsedIndex, 0, newRun);
|
|
149
|
-
lastConsumedRun = newRun;
|
|
150
|
-
parsedIndex += 1;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (hasNonTextBoxContent && parsedRun) {
|
|
154
|
-
lastConsumedRun = parsedRun;
|
|
155
|
-
parsedIndex += 1;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
const scanRunForTextBoxDrawings = (xmlRun) => {
|
|
160
|
-
const textBoxDrawings = [];
|
|
161
|
-
let hasNonTextBoxContent = false;
|
|
162
|
-
const visitDrawing = (drawingEl) => {
|
|
163
|
-
if (isTextBoxDrawing(drawingEl)) {
|
|
164
|
-
textBoxDrawings.push(drawingEl);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
hasNonTextBoxContent = true;
|
|
168
|
-
};
|
|
169
|
-
for (const el of getChildElements(xmlRun)) {
|
|
170
|
-
const name = getLocalName(el.name ?? "");
|
|
171
|
-
if (name === "rPr") continue;
|
|
172
|
-
if (name === "drawing") {
|
|
173
|
-
visitDrawing(el);
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
if (name === "AlternateContent") {
|
|
177
|
-
const branches = getChildElements(el);
|
|
178
|
-
const choice = branches.find((branch) => getLocalName(branch.name ?? "") === "Choice");
|
|
179
|
-
const fallback = branches.find((branch) => getLocalName(branch.name ?? "") === "Fallback");
|
|
180
|
-
const tryBranch = (branch) => {
|
|
181
|
-
if (!branch) return false;
|
|
182
|
-
let found = false;
|
|
183
|
-
for (const innerEl of getChildElements(branch)) if (getLocalName(innerEl.name ?? "") === "drawing") {
|
|
184
|
-
visitDrawing(innerEl);
|
|
185
|
-
found = true;
|
|
186
|
-
}
|
|
187
|
-
return found;
|
|
188
|
-
};
|
|
189
|
-
let foundInBranch = tryBranch(choice);
|
|
190
|
-
if (!foundInBranch) foundInBranch = tryBranch(fallback);
|
|
191
|
-
if (!foundInBranch) hasNonTextBoxContent = true;
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
hasNonTextBoxContent = true;
|
|
195
|
-
}
|
|
196
|
-
return {
|
|
197
|
-
textBoxDrawings,
|
|
198
|
-
hasNonTextBoxContent
|
|
199
|
-
};
|
|
200
|
-
};
|
|
201
103
|
const parseBlockContent = (parent, styles, theme, numbering, rels, media, options) => parseBlockContentWithState(parent, styles, theme, numbering, rels, media, {
|
|
202
104
|
listCounters: /* @__PURE__ */ new Map(),
|
|
203
105
|
abstractCounters: /* @__PURE__ */ new Map(),
|
|
@@ -3,6 +3,6 @@ import { XmlElement } from "./xmlParser.js";
|
|
|
3
3
|
|
|
4
4
|
//#region src/docx/groupDrawingParser.d.ts
|
|
5
5
|
/** Parse a WordprocessingGroup drawing into a safe SVG-backed image preview. */
|
|
6
|
-
declare const parseGroupDrawing: (drawing: XmlElement) => document_d_exports.Image | null;
|
|
6
|
+
declare const parseGroupDrawing: (drawing: XmlElement, rels?: document_d_exports.RelationshipMap, media?: Map<string, document_d_exports.MediaFile>) => document_d_exports.Image | null;
|
|
7
7
|
//#endregion
|
|
8
8
|
export { parseGroupDrawing };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { findAllDeep, findChildByLocalName, findChildrenByLocalName, getAttribute, getLocalName, getTextContent, parseNumericAttribute } from "./xmlParser.js";
|
|
1
|
+
import { findAllDeep, findChildByLocalName, findChildrenByLocalName, getAttribute, getChildElements, getLocalName, getTextContent, parseNumericAttribute } from "./xmlParser.js";
|
|
2
2
|
import { emuToPixels } from "../utils/units.js";
|
|
3
|
-
import { parseImage } from "./imageParser.js";
|
|
3
|
+
import { parseImage, resolveImageData } from "./imageParser.js";
|
|
4
4
|
//#region src/docx/groupDrawingParser.ts
|
|
5
5
|
const HEX_COLOR = /^[0-9A-Fa-f]{6}$/u;
|
|
6
6
|
const DEFAULT_TEXT_COLOR = "000000";
|
|
@@ -8,6 +8,7 @@ const DEFAULT_FONT_HALF_POINTS = 22;
|
|
|
8
8
|
const DEFAULT_LINE_WIDTH_EMU = 9525;
|
|
9
9
|
const HALF_POINT_TO_EMU = 6350;
|
|
10
10
|
const MAX_GROUP_SHAPES = 256;
|
|
11
|
+
const CROP_SCALE = 1e5;
|
|
11
12
|
const MAX_PATH_COMMANDS = 1e4;
|
|
12
13
|
const MAX_TEXT_CHARACTERS = 2e4;
|
|
13
14
|
const MAX_SVG_CHARACTERS = 1e6;
|
|
@@ -110,18 +111,58 @@ const renderTextBox = (wsp) => {
|
|
|
110
111
|
const lineStep = lineHeight / fontSize * svgFontSize;
|
|
111
112
|
return `<text x="0" y="${svgFontSize}" transform="translate(${x} ${y}) scale(${scale})" font-family="Arial, sans-serif" font-size="${svgFontSize}" fill="#${color}">${lines.map((line, index) => `<tspan x="0" dy="${index === 0 ? 0 : lineStep}">${line}</tspan>`).join("")}</text>`;
|
|
112
113
|
};
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
114
|
+
const renderPicture = (picture, index, rels, media) => {
|
|
115
|
+
const { x, y, width, height } = childTransform(picture);
|
|
116
|
+
if (width <= 0 || height <= 0) return "";
|
|
117
|
+
const blipFill = findChildByLocalName(picture, "blipFill");
|
|
118
|
+
const blip = findChildByLocalName(blipFill, "blip");
|
|
119
|
+
const { src } = resolveImageData(getAttribute(blip, "r", "embed") ?? getAttribute(blip, "r", "link") ?? "", rels, media);
|
|
120
|
+
if (!src) return "";
|
|
121
|
+
const sourceRect = findChildByLocalName(blipFill, "srcRect");
|
|
122
|
+
const left = Math.max(0, numericAttr(sourceRect, "l")) / CROP_SCALE;
|
|
123
|
+
const top = Math.max(0, numericAttr(sourceRect, "t")) / CROP_SCALE;
|
|
124
|
+
const right = Math.max(0, numericAttr(sourceRect, "r")) / CROP_SCALE;
|
|
125
|
+
const bottom = Math.max(0, numericAttr(sourceRect, "b")) / CROP_SCALE;
|
|
126
|
+
const visibleWidth = 1 - left - right;
|
|
127
|
+
const visibleHeight = 1 - top - bottom;
|
|
128
|
+
if (visibleWidth <= 0 || visibleHeight <= 0) return "";
|
|
129
|
+
const image = `<image x="${x - width * left / visibleWidth}" y="${y - height * top / visibleHeight}" width="${width / visibleWidth}" height="${height / visibleHeight}" href="${escapeXml(src)}" preserveAspectRatio="none"/>`;
|
|
130
|
+
if (left === 0 && top === 0 && right === 0 && bottom === 0) return image;
|
|
131
|
+
const clipId = `group-picture-${index}`;
|
|
132
|
+
return `<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${width}" height="${height}"/></clipPath></defs><g clip-path="url(#${clipId})">${image}</g>`;
|
|
133
|
+
};
|
|
134
|
+
const groupViewBox = (group, width, height) => {
|
|
135
|
+
const transform = findChildByLocalName(findChildByLocalName(group, "grpSpPr"), "xfrm");
|
|
136
|
+
const childOffset = findChildByLocalName(transform, "chOff");
|
|
137
|
+
const childExtent = findChildByLocalName(transform, "chExt");
|
|
138
|
+
const childWidth = numericAttr(childExtent, "cx");
|
|
139
|
+
const childHeight = numericAttr(childExtent, "cy");
|
|
140
|
+
return {
|
|
141
|
+
x: numericAttr(childOffset, "x"),
|
|
142
|
+
y: numericAttr(childOffset, "y"),
|
|
143
|
+
width: childWidth > 0 ? childWidth : width,
|
|
144
|
+
height: childHeight > 0 ? childHeight : height
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
const createSvg = (group, width, height, rels, media) => {
|
|
148
|
+
const content = getChildElements(group).slice(0, MAX_GROUP_SHAPES).map((child, index) => {
|
|
149
|
+
const localName = getLocalName(child.name ?? "");
|
|
150
|
+
if (localName === "pic") return renderPicture(child, index, rels, media);
|
|
151
|
+
if (localName !== "wsp") return "";
|
|
152
|
+
return findChildByLocalName(child, "txbx") ? renderTextBox(child) : renderGeometry(child);
|
|
153
|
+
}).join("");
|
|
154
|
+
if (!content) return null;
|
|
155
|
+
const viewBox = groupViewBox(group, width, height);
|
|
156
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}" width="${emuToPixels(width)}" height="${emuToPixels(height)}">${content}</svg>`;
|
|
116
157
|
};
|
|
117
158
|
/** Parse a WordprocessingGroup drawing into a safe SVG-backed image preview. */
|
|
118
|
-
const parseGroupDrawing = (drawing) => {
|
|
159
|
+
const parseGroupDrawing = (drawing, rels, media) => {
|
|
119
160
|
const group = findChildByLocalName(findAllDeep(drawing, "a", "graphicData").at(0) ?? null, "wgp");
|
|
120
161
|
if (!group) return null;
|
|
121
162
|
const image = parseImage(drawing, void 0, void 0);
|
|
122
163
|
if (!image || image.size.width <= 0 || image.size.height <= 0) return null;
|
|
123
|
-
const svg = createSvg(group, image.size.width, image.size.height);
|
|
124
|
-
if (svg.length > MAX_SVG_CHARACTERS) return null;
|
|
164
|
+
const svg = createSvg(group, image.size.width, image.size.height, rels, media);
|
|
165
|
+
if (!svg || svg.length > MAX_SVG_CHARACTERS) return null;
|
|
125
166
|
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
126
167
|
image.mimeType = "image/svg+xml";
|
|
127
168
|
image.filename = "wordprocessing-group.svg";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
import { NumberingMap } from "./numberingParser.js";
|
|
3
|
+
import { StyleMap } from "./styleParser.js";
|
|
4
|
+
import { XmlElement } from "./xmlParser.js";
|
|
5
|
+
|
|
6
|
+
//#region src/docx/paragraphTextBoxEnrichment.d.ts
|
|
7
|
+
declare const enrichParagraphTextBoxes: (paragraph: document_d_exports.Paragraph, paraXml: XmlElement, styles: StyleMap | null, theme: document_d_exports.Theme | null, numbering: NumberingMap | null, rels: document_d_exports.RelationshipMap | null, media: Map<string, document_d_exports.MediaFile> | null) => void;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { enrichParagraphTextBoxes };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { findDeep, getChildElements, getLocalName } from "./xmlParser.js";
|
|
2
|
+
import { getTextBoxContentElement, isTextBoxDrawing, parseTextBox, parseTextBoxContent } from "./textBoxParser.js";
|
|
3
|
+
import { parseParagraph } from "./paragraphParser.js";
|
|
4
|
+
//#region src/docx/paragraphTextBoxEnrichment.ts
|
|
5
|
+
const enrichParagraphTextBoxes = (paragraph, paraXml, styles, theme, numbering, rels, media) => {
|
|
6
|
+
const xmlChildren = getChildElements(paraXml);
|
|
7
|
+
let parsedIndex = 0;
|
|
8
|
+
let lastConsumedRun;
|
|
9
|
+
for (const xmlChild of xmlChildren) {
|
|
10
|
+
if (getLocalName(xmlChild.name ?? "") !== "r") {
|
|
11
|
+
if (parsedIndex < paragraph.content.length && paragraph.content[parsedIndex]?.type !== "run") parsedIndex += 1;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const { textBoxDrawings, hasNonTextBoxContent } = scanRunForTextBoxDrawings(xmlChild);
|
|
15
|
+
const parsedContent = paragraph.content[parsedIndex];
|
|
16
|
+
const parsedRun = parsedContent?.type === "run" ? parsedContent : void 0;
|
|
17
|
+
const targetRun = parsedRun ?? (hasNonTextBoxContent ? lastConsumedRun : void 0);
|
|
18
|
+
for (const runEl of textBoxDrawings) {
|
|
19
|
+
const textBox = parseTextBox(runEl);
|
|
20
|
+
if (!textBox) continue;
|
|
21
|
+
const wsp = findDeep(runEl, "wps", "wsp");
|
|
22
|
+
if (wsp) {
|
|
23
|
+
const txbxContentEl = getTextBoxContentElement(wsp);
|
|
24
|
+
if (txbxContentEl) textBox.content = parseTextBoxContent(txbxContentEl, parseParagraph, null, styles, theme, numbering, rels ?? void 0, media ?? void 0);
|
|
25
|
+
}
|
|
26
|
+
const shape = {
|
|
27
|
+
type: "shape",
|
|
28
|
+
shapeType: "textBox",
|
|
29
|
+
size: textBox.size,
|
|
30
|
+
...textBox.position !== void 0 ? { position: textBox.position } : {},
|
|
31
|
+
...textBox.wrap !== void 0 ? { wrap: textBox.wrap } : {},
|
|
32
|
+
...textBox.fill !== void 0 ? { fill: textBox.fill } : {},
|
|
33
|
+
...textBox.outline !== void 0 ? { outline: textBox.outline } : {},
|
|
34
|
+
textBody: {
|
|
35
|
+
content: textBox.content,
|
|
36
|
+
...textBox.margins !== void 0 ? { margins: textBox.margins } : {}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
if (textBox.id) shape.id = textBox.id;
|
|
40
|
+
const shapeContent = {
|
|
41
|
+
type: "shape",
|
|
42
|
+
shape
|
|
43
|
+
};
|
|
44
|
+
if (targetRun && hasNonTextBoxContent) targetRun.content.push(shapeContent);
|
|
45
|
+
else {
|
|
46
|
+
const newRun = {
|
|
47
|
+
type: "run",
|
|
48
|
+
content: [shapeContent]
|
|
49
|
+
};
|
|
50
|
+
paragraph.content.splice(parsedIndex, 0, newRun);
|
|
51
|
+
lastConsumedRun = newRun;
|
|
52
|
+
parsedIndex += 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (hasNonTextBoxContent && parsedRun) {
|
|
56
|
+
lastConsumedRun = parsedRun;
|
|
57
|
+
parsedIndex += 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const scanRunForTextBoxDrawings = (xmlRun) => {
|
|
62
|
+
const textBoxDrawings = [];
|
|
63
|
+
let hasNonTextBoxContent = false;
|
|
64
|
+
const visitDrawing = (drawingEl) => {
|
|
65
|
+
if (isTextBoxDrawing(drawingEl)) {
|
|
66
|
+
textBoxDrawings.push(drawingEl);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
hasNonTextBoxContent = true;
|
|
70
|
+
};
|
|
71
|
+
for (const el of getChildElements(xmlRun)) {
|
|
72
|
+
const name = getLocalName(el.name ?? "");
|
|
73
|
+
if (name === "rPr") continue;
|
|
74
|
+
if (name === "drawing") {
|
|
75
|
+
visitDrawing(el);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (name === "AlternateContent") {
|
|
79
|
+
const branches = getChildElements(el);
|
|
80
|
+
const choice = branches.find((branch) => getLocalName(branch.name ?? "") === "Choice");
|
|
81
|
+
const fallback = branches.find((branch) => getLocalName(branch.name ?? "") === "Fallback");
|
|
82
|
+
const tryBranch = (branch) => {
|
|
83
|
+
if (!branch) return false;
|
|
84
|
+
let found = false;
|
|
85
|
+
for (const innerEl of getChildElements(branch)) if (getLocalName(innerEl.name ?? "") === "drawing") {
|
|
86
|
+
visitDrawing(innerEl);
|
|
87
|
+
found = true;
|
|
88
|
+
}
|
|
89
|
+
return found;
|
|
90
|
+
};
|
|
91
|
+
let foundInBranch = tryBranch(choice);
|
|
92
|
+
if (!foundInBranch) foundInBranch = tryBranch(fallback);
|
|
93
|
+
if (!foundInBranch) hasNonTextBoxContent = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
hasNonTextBoxContent = true;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
textBoxDrawings,
|
|
100
|
+
hasNonTextBoxContent
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
//#endregion
|
|
104
|
+
export { enrichParagraphTextBoxes };
|
package/dist/docx/runParser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cloneWithXmlnsDeclarations, elementToXml, findChild, findChildren, getAttribute, getChildElements, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js";
|
|
1
|
+
import { cloneWithXmlnsDeclarations, elementToXml, findAllDeep, findChild, findChildren, getAttribute, getChildElements, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js";
|
|
2
2
|
import { EmphasisMarkSchema, FontThemeSchema, HighlightColorSchema, ShadingPatternSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
|
|
3
3
|
import { parseImage } from "./imageParser.js";
|
|
4
4
|
import { parseGroupDrawing } from "./groupDrawingParser.js";
|
|
@@ -443,7 +443,7 @@ function parseInstrText(element) {
|
|
|
443
443
|
* `shapeParser.parseShapeFromDrawing` into a `ShapeContent`.
|
|
444
444
|
*/
|
|
445
445
|
function parseDrawingContent(element, rels, media) {
|
|
446
|
-
const groupImage = parseGroupDrawing(element);
|
|
446
|
+
const groupImage = parseGroupDrawing(element, rels ?? void 0, media ?? void 0);
|
|
447
447
|
if (groupImage) return {
|
|
448
448
|
type: "drawing",
|
|
449
449
|
image: groupImage,
|
|
@@ -544,6 +544,15 @@ function parseRunContents(runElement, rels, media, rootXmlns = {}) {
|
|
|
544
544
|
const alternateChildren = getChildElements(child);
|
|
545
545
|
const choiceEl = alternateChildren.find((el) => getLocalName(el.name) === "Choice");
|
|
546
546
|
const fallbackEl = alternateChildren.find((el) => getLocalName(el.name) === "Fallback");
|
|
547
|
+
const groupedChoiceDrawing = choiceEl ? getChildElements(choiceEl).find((element) => getLocalName(element.name) === "drawing" && findAllDeep(element, "wpg", "wgp").length > 0) : void 0;
|
|
548
|
+
if (groupedChoiceDrawing) {
|
|
549
|
+
const groupedDrawing = parseDrawingContent(groupedChoiceDrawing, rels, media);
|
|
550
|
+
if (groupedDrawing?.type === "drawing" && groupedDrawing.image.src) {
|
|
551
|
+
groupedDrawing.rawXml = elementToXml(child);
|
|
552
|
+
contents.push(groupedDrawing);
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
547
556
|
const fallbackPict = fallbackEl ? getChildElements(fallbackEl).find((el) => getLocalName(el.name) === "pict") : void 0;
|
|
548
557
|
const fallbackVml = fallbackPict ? parseVmlImageContent(fallbackPict, rels, media, rootXmlns) : null;
|
|
549
558
|
if (fallbackVml?.image.src) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/docx/server/boundedArchive.d.ts
|
|
2
|
+
declare const DOCX_MAX_ENTRY_BYTES: number;
|
|
3
|
+
declare const DOCX_MAX_TOTAL_BYTES: number;
|
|
4
|
+
declare const DOCX_MAX_ENTRIES = 4096;
|
|
5
|
+
declare const DocxArchiveError_base: import("better-result").TaggedErrorClass<"DocxArchiveError", {
|
|
6
|
+
message: string;
|
|
7
|
+
reason: "load-failed" | "too-many-entries" | "entry-too-large" | "total-too-large";
|
|
8
|
+
cause?: unknown;
|
|
9
|
+
}>;
|
|
10
|
+
/** Error raised when a DOCX archive cannot be loaded within configured limits. */
|
|
11
|
+
declare class DocxArchiveError extends DocxArchiveError_base {}
|
|
12
|
+
type DocxArchiveOptions = {
|
|
13
|
+
maxEntryBytes?: number;
|
|
14
|
+
maxTotalBytes?: number;
|
|
15
|
+
maxEntries?: number;
|
|
16
|
+
};
|
|
17
|
+
type DocxArchive = {
|
|
18
|
+
entries: readonly string[];
|
|
19
|
+
readEntryString: (path: string) => Promise<string | null>;
|
|
20
|
+
readEntryUint8: (path: string) => Promise<Uint8Array | null>;
|
|
21
|
+
};
|
|
22
|
+
declare const loadDocxArchive: (bytes: ArrayBuffer | Uint8Array, options?: DocxArchiveOptions) => Promise<DocxArchive>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { DOCX_MAX_ENTRIES, DOCX_MAX_ENTRY_BYTES, DOCX_MAX_TOTAL_BYTES, DocxArchive, DocxArchiveError, DocxArchiveOptions, loadDocxArchive };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { TaggedError } from "better-result";
|
|
2
|
+
import JSZip from "jszip";
|
|
3
|
+
//#region src/docx/server/boundedArchive.ts
|
|
4
|
+
const DOCX_MAX_ENTRY_BYTES = 128 * 1024 * 1024;
|
|
5
|
+
const DOCX_MAX_TOTAL_BYTES = 256 * 1024 * 1024;
|
|
6
|
+
const DOCX_MAX_ENTRIES = 4096;
|
|
7
|
+
/** Error raised when a DOCX archive cannot be loaded within configured limits. */
|
|
8
|
+
var DocxArchiveError = class extends TaggedError("DocxArchiveError")() {};
|
|
9
|
+
const collectStream = async ({ stream, maxEntryBytes, remainingBytes, maxTotalBytes, path }) => await new Promise((resolve, reject) => {
|
|
10
|
+
const chunks = [];
|
|
11
|
+
let entryBytes = 0;
|
|
12
|
+
const fail = (reason, message) => {
|
|
13
|
+
const destroy = Reflect.get(stream, "destroy");
|
|
14
|
+
if (typeof destroy === "function") Reflect.apply(destroy, stream, []);
|
|
15
|
+
reject(new DocxArchiveError({
|
|
16
|
+
message,
|
|
17
|
+
reason
|
|
18
|
+
}));
|
|
19
|
+
};
|
|
20
|
+
stream.on("data", (chunk) => {
|
|
21
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
22
|
+
entryBytes += bytes.length;
|
|
23
|
+
if (entryBytes > maxEntryBytes) {
|
|
24
|
+
fail("entry-too-large", `DOCX entry "${path}" exceeded the ${maxEntryBytes}-byte limit`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (entryBytes > remainingBytes) {
|
|
28
|
+
fail("total-too-large", `DOCX archive exceeded the ${maxTotalBytes}-byte cumulative limit while reading "${path}"`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
chunks.push(bytes);
|
|
32
|
+
});
|
|
33
|
+
stream.on("end", () => resolve(Buffer.concat(chunks)));
|
|
34
|
+
stream.on("error", reject);
|
|
35
|
+
});
|
|
36
|
+
const loadDocxArchive = async (bytes, options = {}) => {
|
|
37
|
+
const maxEntryBytes = options.maxEntryBytes ?? 134217728;
|
|
38
|
+
const maxTotalBytes = options.maxTotalBytes ?? 268435456;
|
|
39
|
+
const maxEntries = options.maxEntries ?? 4096;
|
|
40
|
+
let zip;
|
|
41
|
+
try {
|
|
42
|
+
zip = await JSZip.loadAsync(bytes);
|
|
43
|
+
} catch (cause) {
|
|
44
|
+
throw new DocxArchiveError({
|
|
45
|
+
message: "Failed to parse DOCX archive",
|
|
46
|
+
reason: "load-failed",
|
|
47
|
+
cause
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const archiveEntries = Object.values(zip.files);
|
|
51
|
+
if (archiveEntries.length > maxEntries) throw new DocxArchiveError({
|
|
52
|
+
message: `DOCX archive declares ${archiveEntries.length} entries (max ${maxEntries})`,
|
|
53
|
+
reason: "too-many-entries"
|
|
54
|
+
});
|
|
55
|
+
let declaredTotalBytes = 0;
|
|
56
|
+
for (const entry of archiveEntries) {
|
|
57
|
+
const data = "_data" in entry ? entry._data : void 0;
|
|
58
|
+
const declaredBytes = typeof data === "object" && data !== null && "uncompressedSize" in data ? data.uncompressedSize : void 0;
|
|
59
|
+
if (typeof declaredBytes !== "number" || !Number.isFinite(declaredBytes)) {
|
|
60
|
+
declaredTotalBytes = NaN;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
if (declaredBytes > maxEntryBytes) throw new DocxArchiveError({
|
|
64
|
+
message: `DOCX entry "${entry.name}" declares ${declaredBytes} bytes (max ${maxEntryBytes})`,
|
|
65
|
+
reason: "entry-too-large"
|
|
66
|
+
});
|
|
67
|
+
declaredTotalBytes += declaredBytes;
|
|
68
|
+
}
|
|
69
|
+
if (Number.isFinite(declaredTotalBytes) && declaredTotalBytes > maxTotalBytes) throw new DocxArchiveError({
|
|
70
|
+
message: `DOCX archive declares ${declaredTotalBytes} cumulative bytes (max ${maxTotalBytes})`,
|
|
71
|
+
reason: "total-too-large"
|
|
72
|
+
});
|
|
73
|
+
let totalBytesRead = 0;
|
|
74
|
+
let readChain = Promise.resolve();
|
|
75
|
+
const readEntry = async (path) => {
|
|
76
|
+
const work = async () => {
|
|
77
|
+
const entry = zip.file(path);
|
|
78
|
+
if (!entry) return null;
|
|
79
|
+
const buffer = await collectStream({
|
|
80
|
+
stream: entry.nodeStream("nodebuffer"),
|
|
81
|
+
maxEntryBytes,
|
|
82
|
+
remainingBytes: maxTotalBytes - totalBytesRead,
|
|
83
|
+
maxTotalBytes,
|
|
84
|
+
path
|
|
85
|
+
});
|
|
86
|
+
totalBytesRead += buffer.length;
|
|
87
|
+
return buffer;
|
|
88
|
+
};
|
|
89
|
+
const next = readChain.then(work, work);
|
|
90
|
+
readChain = next.then(() => void 0, () => void 0);
|
|
91
|
+
return await next;
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
entries: Object.freeze(archiveEntries.map(({ name }) => name)),
|
|
95
|
+
async readEntryString(path) {
|
|
96
|
+
const buffer = await readEntry(path);
|
|
97
|
+
return buffer === null ? null : buffer.toString("utf-8");
|
|
98
|
+
},
|
|
99
|
+
async readEntryUint8(path) {
|
|
100
|
+
const buffer = await readEntry(path);
|
|
101
|
+
return buffer === null ? null : new Uint8Array(buffer);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
//#endregion
|
|
106
|
+
export { DOCX_MAX_ENTRIES, DOCX_MAX_ENTRY_BYTES, DOCX_MAX_TOTAL_BYTES, DocxArchiveError, loadDocxArchive };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/docx/server/extractDocxText.d.ts
|
|
2
|
+
/** Document part containing an extracted paragraph. */
|
|
3
|
+
type DocxParagraphSource = "header" | "body" | "footer";
|
|
4
|
+
/** Paragraph text and lightweight formatting metadata from a DOCX archive. */
|
|
5
|
+
type ExtractedDocxParagraph = {
|
|
6
|
+
index: number;
|
|
7
|
+
text: string;
|
|
8
|
+
source: DocxParagraphSource;
|
|
9
|
+
style?: string;
|
|
10
|
+
bold?: boolean;
|
|
11
|
+
fontSize?: number;
|
|
12
|
+
alignment?: "left" | "center" | "right" | "both";
|
|
13
|
+
};
|
|
14
|
+
/** Accepted-revision paragraph text extracted in deterministic part order. */
|
|
15
|
+
type ExtractedDocxText = {
|
|
16
|
+
paragraphs: ExtractedDocxParagraph[];
|
|
17
|
+
charCount: number;
|
|
18
|
+
view: "accepted";
|
|
19
|
+
};
|
|
20
|
+
/** Extract paragraph text and formatting metadata from a DOCX archive. */
|
|
21
|
+
declare const extractDocxText: (bytes: ArrayBuffer | Uint8Array) => Promise<ExtractedDocxText>;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { DocxParagraphSource, ExtractedDocxParagraph, ExtractedDocxText, extractDocxText };
|