@stll/folio-core 0.29.0 → 0.30.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/folioEditor.d.ts +28 -1
- package/dist/controller/folioEditor.js +37 -26
- package/dist/controller/fontReadiness.js +28 -24
- package/dist/controller/hiddenEditorManager.d.ts +2 -7
- package/dist/controller/layoutPipeline.js +33 -3
- package/dist/controller/noteEditorManager.d.ts +3 -6
- package/dist/docx/metafileSvg.d.ts +12 -0
- package/dist/docx/metafileSvg.js +462 -0
- package/dist/docx/paragraphTextBoxEnrichment.js +55 -4
- package/dist/docx/parser.d.ts +2 -2
- package/dist/docx/parser.js +26 -0
- package/dist/docx/runParser.js +2 -1
- package/dist/docx/serializer/runSerializer.js +14 -2
- package/dist/docx/styleParser.js +2 -1
- package/dist/docx/textBoxParser.js +14 -0
- package/dist/docx/vmlImageParser.js +72 -152
- package/dist/docx/vmlPreview.d.ts +31 -0
- package/dist/docx/vmlPreview.js +535 -0
- package/dist/docx/wrapTypes.d.ts +2 -36
- package/dist/docx/wrapTypes.js +1 -39
- package/dist/fonts/fontAlternates.d.ts +8 -0
- package/dist/fonts/fontAlternates.js +16 -0
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/footnoteLayout.js +34 -3
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.js +5 -0
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -0
- package/dist/layout-bridge/convert/toFlowBlocks.js +78 -28
- package/dist/layout-bridge/dom/noteStoryDom.d.ts +1 -1
- package/dist/layout-bridge/engine/selectionRects.js +3 -1
- package/dist/layout-engine/index.js +65 -32
- package/dist/layout-engine/justifiedLineFit.d.ts +7 -0
- package/dist/layout-engine/justifiedLineFit.js +6 -0
- package/dist/layout-engine/keep-together.js +15 -10
- package/dist/layout-engine/measure/complexScriptFormatting.d.ts +6 -1
- package/dist/layout-engine/measure/complexScriptFormatting.js +11 -2
- package/dist/layout-engine/measure/floatingTablePosition.d.ts +16 -1
- package/dist/layout-engine/measure/floatingTablePosition.js +29 -6
- package/dist/layout-engine/measure/lineBreakProvider.js +67 -8
- package/dist/layout-engine/measure/listMarkerWidth.d.ts +1 -0
- package/dist/layout-engine/measure/listMarkerWidth.js +23 -10
- package/dist/layout-engine/measure/measureBlocks.d.ts +15 -0
- package/dist/layout-engine/measure/measureBlocks.js +197 -30
- package/dist/layout-engine/measure/measureContainer.js +20 -24
- package/dist/layout-engine/measure/measureHelpers.d.ts +1 -1
- package/dist/layout-engine/measure/measureHelpers.js +14 -8
- package/dist/layout-engine/measure/measureParagraph.js +111 -37
- package/dist/layout-engine/measure/measureTypes.d.ts +3 -0
- package/dist/layout-engine/measure/tabCalculator.d.ts +123 -0
- package/dist/layout-engine/measure/tabCalculator.js +144 -0
- package/dist/layout-engine/measure/tableCellFlow.d.ts +21 -1
- package/dist/layout-engine/measure/tableCellFlow.js +45 -8
- package/dist/layout-engine/measure/tableInlinePlacement.d.ts +10 -2
- package/dist/layout-engine/measure/tableInlinePlacement.js +17 -15
- package/dist/layout-engine/paginator.d.ts +26 -2
- package/dist/layout-engine/paginator.js +76 -14
- package/dist/layout-engine/paragraphSpacing.d.ts +3 -1
- package/dist/layout-engine/paragraphSpacing.js +4 -3
- package/dist/layout-engine/renderedBreakReconciliation.d.ts +14 -3
- package/dist/layout-engine/renderedBreakReconciliation.js +45 -14
- package/dist/layout-engine/section-breaks.d.ts +3 -1
- package/dist/layout-engine/section-breaks.js +6 -2
- package/dist/layout-engine/types.d.ts +18 -1
- package/dist/layout-engine/types.js +1 -1
- package/dist/layout-painter/imageLayout.d.ts +1 -1
- package/dist/layout-painter/renderPage.js +6 -1
- package/dist/layout-painter/renderParagraph.js +169 -82
- package/dist/layout-painter/renderTable.d.ts +1 -1
- package/dist/layout-painter/renderTable.js +18 -7
- package/dist/layout-painter/renderTextBox.d.ts +1 -1
- package/dist/layout-painter/renderTextBox.js +6 -1
- package/dist/model.d.ts +3 -3
- package/dist/model.js +2 -2
- package/dist/paged-layout/sectionBlockWidths.d.ts +14 -0
- package/dist/paged-layout/sectionBlockWidths.js +103 -14
- package/dist/prosemirror/attrs/index.js +12 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +6 -2
- package/dist/prosemirror/conversion/toProseDoc.js +10 -3
- package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +9 -5
- package/dist/prosemirror/extensions/nodes/TableExtension.js +2 -0
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +6 -2
- package/dist/prosemirror/schema/nodes.d.ts +4 -0
- package/dist/prosemirror/utils/tabCalculator.d.ts +2 -123
- package/dist/prosemirror/utils/tabCalculator.js +1 -140
- package/dist/render-dom/RemoteSelectionOverlay.d.ts +4 -4
- package/dist/types/documentEnumValues.d.ts +4 -1
- package/dist/types/documentEnumValues.js +12 -1
- package/dist/types/editor-story.d.ts +8 -0
- package/dist/types/editor-story.js +0 -0
- package/dist/types/remote-selection.d.ts +11 -0
- package/dist/types/remote-selection.js +0 -0
- package/dist/types/wrap.d.ts +36 -0
- package/dist/types/wrap.js +40 -0
- package/dist/utils/fontResolver.d.ts +1 -1
- package/dist/utils/fontResolver.js +42 -2
- package/dist/utils/formatToStyle.js +4 -2
- package/dist/utils/horizontalScale.d.ts +28 -0
- package/dist/utils/horizontalScale.js +42 -0
- package/package.json +2 -2
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { findChild, findDeep, getAttribute, getChildElements, getLocalName } from "./xmlParser.js";
|
|
2
|
+
//#region src/docx/vmlPreview.ts
|
|
3
|
+
const MAX_VML_PREVIEW_DEPTH = 16;
|
|
4
|
+
const MAX_VML_PREVIEW_ELEMENTS = 256;
|
|
5
|
+
const MAX_VML_PREVIEW_PATH_POINTS = 2e4;
|
|
6
|
+
const MAX_VML_PREVIEW_COORDINATE = 1e6;
|
|
7
|
+
const MAX_VML_PREVIEW_DIMENSION_PX = 2e4;
|
|
8
|
+
const MAX_VML_SVG_CHARACTERS = 1e6;
|
|
9
|
+
const MAX_PACKAGE_VML_PREVIEW_CHARACTERS = 8 * 1024 * 1024;
|
|
10
|
+
const VML_PREVIEW_DATA_URL_PREFIX = "data:image/svg+xml;charset=utf-8,";
|
|
11
|
+
const VML_PREVIEW_FILENAME = "vml-shape-preview.svg";
|
|
12
|
+
const VML_PREVIEW_MIME_TYPE = "image/svg+xml";
|
|
13
|
+
const SAFE_VML_COLORS = /* @__PURE__ */ new Set([
|
|
14
|
+
"black",
|
|
15
|
+
"white",
|
|
16
|
+
"red",
|
|
17
|
+
"green",
|
|
18
|
+
"blue",
|
|
19
|
+
"yellow",
|
|
20
|
+
"gray",
|
|
21
|
+
"grey",
|
|
22
|
+
"silver",
|
|
23
|
+
"maroon",
|
|
24
|
+
"purple",
|
|
25
|
+
"fuchsia",
|
|
26
|
+
"lime",
|
|
27
|
+
"olive",
|
|
28
|
+
"navy",
|
|
29
|
+
"teal",
|
|
30
|
+
"aqua"
|
|
31
|
+
]);
|
|
32
|
+
const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
33
|
+
"__proto__",
|
|
34
|
+
"constructor",
|
|
35
|
+
"prototype"
|
|
36
|
+
]);
|
|
37
|
+
const EMPTY_RESULT = { type: "empty" };
|
|
38
|
+
const INVALID_RESULT = { type: "invalid" };
|
|
39
|
+
const isVmlPathCommand = (value) => {
|
|
40
|
+
switch (value) {
|
|
41
|
+
case "e":
|
|
42
|
+
case "l":
|
|
43
|
+
case "m":
|
|
44
|
+
case "r": return true;
|
|
45
|
+
default: return false;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
/** Parse a VML/CSS length into CSS pixels without accepting partial numbers. */
|
|
49
|
+
const vmlCssLengthToPx = (raw) => {
|
|
50
|
+
if (!raw) return;
|
|
51
|
+
const match = /^(?<amount>-?(?:\d+(?:\.\d+)?|\.\d+))\s*(?<unit>pt|in|px|cm|mm|pc)?$/iu.exec(raw.trim());
|
|
52
|
+
const amountText = match?.groups?.["amount"];
|
|
53
|
+
if (amountText === void 0) return;
|
|
54
|
+
const value = Number.parseFloat(amountText);
|
|
55
|
+
if (!Number.isFinite(value)) return;
|
|
56
|
+
switch (match?.groups?.["unit"]?.toLowerCase()) {
|
|
57
|
+
case "pt": return value / 72 * 96;
|
|
58
|
+
case "in": return value * 96;
|
|
59
|
+
case "cm": return value / 2.54 * 96;
|
|
60
|
+
case "mm": return value / 25.4 * 96;
|
|
61
|
+
case "pc": return value / 6 * 96;
|
|
62
|
+
case "px":
|
|
63
|
+
case void 0: return value;
|
|
64
|
+
default: return;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
/** Read a VML `style` attribute into a prototype-free lowercased record. */
|
|
68
|
+
const parseVmlStyle = (style) => {
|
|
69
|
+
const result = Object.create(null);
|
|
70
|
+
if (!style) return result;
|
|
71
|
+
for (const declaration of style.split(";")) {
|
|
72
|
+
const colon = declaration.indexOf(":");
|
|
73
|
+
if (colon < 0) continue;
|
|
74
|
+
const key = declaration.slice(0, colon).trim().toLowerCase();
|
|
75
|
+
const value = declaration.slice(colon + 1).trim();
|
|
76
|
+
if (!key || PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
77
|
+
result[key] = value;
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
};
|
|
81
|
+
/** Parse a finite VML number without exponent or partial-number coercion. */
|
|
82
|
+
const parseVmlNumber = (raw) => {
|
|
83
|
+
if (!raw || !/^-?(?:\d+(?:\.\d+)?|\.\d+)$/u.test(raw.trim())) return;
|
|
84
|
+
const value = Number.parseFloat(raw);
|
|
85
|
+
return Number.isFinite(value) ? value : void 0;
|
|
86
|
+
};
|
|
87
|
+
/** Check a physical preview dimension against the retained SVG layout bound. */
|
|
88
|
+
const isValidVmlPreviewDimension = (value) => value !== void 0 && value > 0 && value <= MAX_VML_PREVIEW_DIMENSION_PX;
|
|
89
|
+
/** Encode a generated, already-sanitized SVG without exceeding its output cap. */
|
|
90
|
+
const vmlSvgDataUrl = (svg) => svg.length <= MAX_VML_SVG_CHARACTERS ? `${VML_PREVIEW_DATA_URL_PREFIX}${encodeURIComponent(svg)}` : void 0;
|
|
91
|
+
const validCoordinate = (value) => value !== void 0 && Math.abs(value) <= MAX_VML_PREVIEW_COORDINATE;
|
|
92
|
+
const validExtent = (value) => value !== void 0 && value > 0 && value <= MAX_VML_PREVIEW_COORDINATE;
|
|
93
|
+
const coordinatePair = (raw) => {
|
|
94
|
+
const [xRaw, yRaw, extra] = raw?.split(",").map((part) => part.trim()) ?? [];
|
|
95
|
+
if (extra !== void 0) return;
|
|
96
|
+
const x = parseVmlNumber(xRaw);
|
|
97
|
+
const y = parseVmlNumber(yRaw);
|
|
98
|
+
return validCoordinate(x) && validCoordinate(y) ? {
|
|
99
|
+
x,
|
|
100
|
+
y
|
|
101
|
+
} : void 0;
|
|
102
|
+
};
|
|
103
|
+
const localCoordinateSpace = (element, fallbackWidth, fallbackHeight) => {
|
|
104
|
+
const originRaw = getAttribute(element, null, "coordorigin");
|
|
105
|
+
const sizeRaw = getAttribute(element, null, "coordsize");
|
|
106
|
+
const origin = originRaw ? coordinatePair(originRaw) : {
|
|
107
|
+
x: 0,
|
|
108
|
+
y: 0
|
|
109
|
+
};
|
|
110
|
+
const size = sizeRaw ? coordinatePair(sizeRaw) : {
|
|
111
|
+
x: fallbackWidth,
|
|
112
|
+
y: fallbackHeight
|
|
113
|
+
};
|
|
114
|
+
if (!origin || !size) return;
|
|
115
|
+
return validExtent(size.x) && validExtent(size.y) ? {
|
|
116
|
+
origin,
|
|
117
|
+
size
|
|
118
|
+
} : void 0;
|
|
119
|
+
};
|
|
120
|
+
const coordinateViewport = (style) => {
|
|
121
|
+
const x = parseVmlNumber(style["left"]) ?? 0;
|
|
122
|
+
const y = parseVmlNumber(style["top"]) ?? 0;
|
|
123
|
+
const width = parseVmlNumber(style["width"]);
|
|
124
|
+
const height = parseVmlNumber(style["height"]);
|
|
125
|
+
return validCoordinate(x) && validCoordinate(y) && validExtent(width) && validExtent(height) ? {
|
|
126
|
+
x,
|
|
127
|
+
y,
|
|
128
|
+
width,
|
|
129
|
+
height
|
|
130
|
+
} : void 0;
|
|
131
|
+
};
|
|
132
|
+
const transformMatrix = (viewport, space) => {
|
|
133
|
+
const scaleX = viewport.width / space.size.x;
|
|
134
|
+
const scaleY = viewport.height / space.size.y;
|
|
135
|
+
const translateX = viewport.x - space.origin.x * scaleX;
|
|
136
|
+
const translateY = viewport.y - space.origin.y * scaleY;
|
|
137
|
+
if ([
|
|
138
|
+
scaleX,
|
|
139
|
+
scaleY,
|
|
140
|
+
translateX,
|
|
141
|
+
translateY
|
|
142
|
+
].some((value) => !Number.isFinite(value) || Math.abs(value) > MAX_VML_PREVIEW_COORDINATE)) return;
|
|
143
|
+
return `matrix(${scaleX} 0 0 ${scaleY} ${translateX} ${translateY})`;
|
|
144
|
+
};
|
|
145
|
+
const safeColor = (raw, fallback) => {
|
|
146
|
+
const normalized = raw?.trim().toLowerCase();
|
|
147
|
+
if (!normalized) return fallback;
|
|
148
|
+
if (normalized === "none") return "none";
|
|
149
|
+
if (SAFE_VML_COLORS.has(normalized)) return normalized;
|
|
150
|
+
if (/^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/u.test(normalized)) return normalized;
|
|
151
|
+
return /^[0-9a-f]{6}$/u.test(normalized) ? `#${normalized}` : fallback;
|
|
152
|
+
};
|
|
153
|
+
const isVmlFalse = (raw) => {
|
|
154
|
+
switch (raw?.trim().toLowerCase()) {
|
|
155
|
+
case "0":
|
|
156
|
+
case "f":
|
|
157
|
+
case "false": return true;
|
|
158
|
+
default: return false;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const shapePaint = (shape) => {
|
|
162
|
+
const fill = isVmlFalse(getAttribute(shape, null, "filled")) ? "none" : safeColor(getAttribute(shape, null, "fillcolor"), "white");
|
|
163
|
+
const stroke = isVmlFalse(getAttribute(shape, null, "stroked")) ? "none" : safeColor(getAttribute(shape, null, "strokecolor"), "black");
|
|
164
|
+
return fill === "none" && stroke === "none" ? EMPTY_RESULT : {
|
|
165
|
+
type: "painted",
|
|
166
|
+
fill,
|
|
167
|
+
stroke
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
const isHidden = (style) => style["visibility"]?.toLowerCase() === "hidden" || style["display"]?.toLowerCase() === "none";
|
|
171
|
+
const consumeSvgCharacters = (budget, count) => {
|
|
172
|
+
if (budget.svgCharacters + count > MAX_VML_SVG_CHARACTERS) return false;
|
|
173
|
+
budget.svgCharacters += count;
|
|
174
|
+
return true;
|
|
175
|
+
};
|
|
176
|
+
const consumePathPoint = (budget) => {
|
|
177
|
+
if (budget.pathPoints >= MAX_VML_PREVIEW_PATH_POINTS) return false;
|
|
178
|
+
budget.pathPoints += 1;
|
|
179
|
+
return true;
|
|
180
|
+
};
|
|
181
|
+
const skipPathWhitespace = (cursor) => {
|
|
182
|
+
while (/\s/u.test(cursor.raw.charAt(cursor.offset))) cursor.offset += 1;
|
|
183
|
+
};
|
|
184
|
+
const nextPathNumber = (cursor) => {
|
|
185
|
+
skipPathWhitespace(cursor);
|
|
186
|
+
if (cursor.offset >= cursor.raw.length) {
|
|
187
|
+
if (!cursor.afterComma) return { type: "end" };
|
|
188
|
+
cursor.afterComma = false;
|
|
189
|
+
return {
|
|
190
|
+
type: "value",
|
|
191
|
+
value: 0
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (cursor.raw.charAt(cursor.offset) === ",") {
|
|
195
|
+
cursor.offset += 1;
|
|
196
|
+
cursor.afterComma = true;
|
|
197
|
+
return {
|
|
198
|
+
type: "value",
|
|
199
|
+
value: 0
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
cursor.afterComma = false;
|
|
203
|
+
const start = cursor.offset;
|
|
204
|
+
while (cursor.offset < cursor.raw.length && !/[,\s]/u.test(cursor.raw.charAt(cursor.offset))) cursor.offset += 1;
|
|
205
|
+
const value = parseVmlNumber(cursor.raw.slice(start, cursor.offset));
|
|
206
|
+
if (!validCoordinate(value)) return INVALID_RESULT;
|
|
207
|
+
skipPathWhitespace(cursor);
|
|
208
|
+
if (cursor.raw.charAt(cursor.offset) === ",") {
|
|
209
|
+
cursor.offset += 1;
|
|
210
|
+
cursor.afterComma = true;
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
type: "value",
|
|
214
|
+
value
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
const pathNumberCursor = (raw) => ({
|
|
218
|
+
afterComma: false,
|
|
219
|
+
offset: 0,
|
|
220
|
+
raw
|
|
221
|
+
});
|
|
222
|
+
const parseMovePair = (raw, budget) => {
|
|
223
|
+
const cursor = pathNumberCursor(raw);
|
|
224
|
+
const x = nextPathNumber(cursor);
|
|
225
|
+
const y = nextPathNumber(cursor);
|
|
226
|
+
const end = nextPathNumber(cursor);
|
|
227
|
+
return x.type === "value" && y.type === "value" && end.type === "end" && consumePathPoint(budget) ? {
|
|
228
|
+
x: x.value,
|
|
229
|
+
y: y.value
|
|
230
|
+
} : void 0;
|
|
231
|
+
};
|
|
232
|
+
const appendLinePairs = ({ raw, budget, commands, relative, startX, startY }) => {
|
|
233
|
+
const cursor = pathNumberCursor(raw);
|
|
234
|
+
let x = startX;
|
|
235
|
+
let y = startY;
|
|
236
|
+
let pairCount = 0;
|
|
237
|
+
while (true) {
|
|
238
|
+
const nextX = nextPathNumber(cursor);
|
|
239
|
+
if (nextX.type === "end") return pairCount > 0 ? {
|
|
240
|
+
x,
|
|
241
|
+
y
|
|
242
|
+
} : void 0;
|
|
243
|
+
const nextY = nextPathNumber(cursor);
|
|
244
|
+
if (nextX.type !== "value" || nextY.type !== "value") return;
|
|
245
|
+
x = relative ? x + nextX.value : nextX.value;
|
|
246
|
+
y = relative ? y + nextY.value : nextY.value;
|
|
247
|
+
if (!validCoordinate(x) || !validCoordinate(y) || !consumePathPoint(budget) || !appendPathCommand(commands, `L ${x} ${y}`, budget)) return;
|
|
248
|
+
pairCount += 1;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
const appendPathCommand = (commands, command, budget) => {
|
|
252
|
+
if (!consumeSvgCharacters(budget, command.length + 1)) return false;
|
|
253
|
+
commands.push(command);
|
|
254
|
+
return true;
|
|
255
|
+
};
|
|
256
|
+
const startPathSubpath = (state, point, budget) => {
|
|
257
|
+
const commands = state.type === "building" ? state.commands : [];
|
|
258
|
+
const hasLine = state.type === "building" && state.hasLine;
|
|
259
|
+
if (!appendPathCommand(commands, `M ${point.x} ${point.y}`, budget)) return;
|
|
260
|
+
return {
|
|
261
|
+
type: "building",
|
|
262
|
+
x: point.x,
|
|
263
|
+
y: point.y,
|
|
264
|
+
commands,
|
|
265
|
+
hasLine,
|
|
266
|
+
pathSets: state.pathSets
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
const renderVmlPath = (raw, budget) => {
|
|
270
|
+
if (raw.length > MAX_VML_SVG_CHARACTERS) return INVALID_RESULT;
|
|
271
|
+
let state = {
|
|
272
|
+
type: "between-sets",
|
|
273
|
+
pathSets: []
|
|
274
|
+
};
|
|
275
|
+
let offset = 0;
|
|
276
|
+
while (offset < raw.length) {
|
|
277
|
+
while (/\s/u.test(raw.charAt(offset))) offset += 1;
|
|
278
|
+
if (offset >= raw.length) break;
|
|
279
|
+
const command = raw.charAt(offset).toLowerCase();
|
|
280
|
+
if (!isVmlPathCommand(command)) return INVALID_RESULT;
|
|
281
|
+
offset += 1;
|
|
282
|
+
const argumentStart = offset;
|
|
283
|
+
while (offset < raw.length && !/[a-z]/iu.test(raw.charAt(offset))) offset += 1;
|
|
284
|
+
const rawArguments = raw.slice(argumentStart, offset).trim();
|
|
285
|
+
switch (command) {
|
|
286
|
+
case "m": {
|
|
287
|
+
const point = parseMovePair(rawArguments, budget);
|
|
288
|
+
if (!point) return INVALID_RESULT;
|
|
289
|
+
const nextState = startPathSubpath(state, point, budget);
|
|
290
|
+
if (!nextState) return INVALID_RESULT;
|
|
291
|
+
state = nextState;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case "l":
|
|
295
|
+
case "r": {
|
|
296
|
+
if (state.type !== "building") return INVALID_RESULT;
|
|
297
|
+
const endPoint = appendLinePairs({
|
|
298
|
+
raw: rawArguments,
|
|
299
|
+
budget,
|
|
300
|
+
commands: state.commands,
|
|
301
|
+
relative: command === "r",
|
|
302
|
+
startX: state.x,
|
|
303
|
+
startY: state.y
|
|
304
|
+
});
|
|
305
|
+
if (!endPoint) return INVALID_RESULT;
|
|
306
|
+
state = {
|
|
307
|
+
type: "building",
|
|
308
|
+
x: endPoint.x,
|
|
309
|
+
y: endPoint.y,
|
|
310
|
+
commands: state.commands,
|
|
311
|
+
hasLine: true,
|
|
312
|
+
pathSets: state.pathSets
|
|
313
|
+
};
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case "e":
|
|
317
|
+
if (rawArguments || state.type !== "building") return INVALID_RESULT;
|
|
318
|
+
if (state.hasLine) state.pathSets.push(state.commands.join(" "));
|
|
319
|
+
state = {
|
|
320
|
+
type: "between-sets",
|
|
321
|
+
pathSets: state.pathSets
|
|
322
|
+
};
|
|
323
|
+
break;
|
|
324
|
+
default:
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
switch (state.type) {
|
|
328
|
+
case "between-sets": return state.pathSets.length > 0 ? {
|
|
329
|
+
type: "painted",
|
|
330
|
+
pathSets: state.pathSets
|
|
331
|
+
} : EMPTY_RESULT;
|
|
332
|
+
case "building": return INVALID_RESULT;
|
|
333
|
+
default: return state;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const primitiveShapeFragment = (shape, viewport, budget) => {
|
|
337
|
+
const paint = shapePaint(shape);
|
|
338
|
+
if (paint.type === "empty") return EMPTY_RESULT;
|
|
339
|
+
const localName = getLocalName(shape.name ?? "");
|
|
340
|
+
let svg;
|
|
341
|
+
if (localName === "oval") svg = `<ellipse cx="${viewport.x + viewport.width / 2}" cy="${viewport.y + viewport.height / 2}" rx="${viewport.width / 2}" ry="${viewport.height / 2}" fill="${paint.fill}" stroke="${paint.stroke}"/>`;
|
|
342
|
+
else {
|
|
343
|
+
const radius = localName === "roundrect" ? Math.min(viewport.width, viewport.height) * .1 : 0;
|
|
344
|
+
svg = `<rect x="${viewport.x}" y="${viewport.y}" width="${viewport.width}" height="${viewport.height}" rx="${radius}" fill="${paint.fill}" stroke="${paint.stroke}"/>`;
|
|
345
|
+
}
|
|
346
|
+
return consumeSvgCharacters(budget, svg.length) ? {
|
|
347
|
+
type: "painted",
|
|
348
|
+
svg
|
|
349
|
+
} : INVALID_RESULT;
|
|
350
|
+
};
|
|
351
|
+
const lineFragment = (line, budget) => {
|
|
352
|
+
if (isVmlFalse(getAttribute(line, null, "stroked"))) return EMPTY_RESULT;
|
|
353
|
+
const from = coordinatePair(getAttribute(line, null, "from"));
|
|
354
|
+
const to = coordinatePair(getAttribute(line, null, "to"));
|
|
355
|
+
if (!from || !to) return INVALID_RESULT;
|
|
356
|
+
const stroke = safeColor(getAttribute(line, null, "strokecolor"), "black");
|
|
357
|
+
const svg = `<line x1="${from.x}" y1="${from.y}" x2="${to.x}" y2="${to.y}" stroke="${stroke}"/>`;
|
|
358
|
+
return consumeSvgCharacters(budget, svg.length) ? {
|
|
359
|
+
type: "painted",
|
|
360
|
+
svg
|
|
361
|
+
} : INVALID_RESULT;
|
|
362
|
+
};
|
|
363
|
+
const pathShapeFragment = (shape, budget) => {
|
|
364
|
+
if (findChild(shape, "v", "textbox") || findChild(shape, "v", "imagedata")) return EMPTY_RESULT;
|
|
365
|
+
const style = parseVmlStyle(getAttribute(shape, null, "style"));
|
|
366
|
+
if (isHidden(style)) return EMPTY_RESULT;
|
|
367
|
+
const paint = shapePaint(shape);
|
|
368
|
+
if (paint.type === "empty") return EMPTY_RESULT;
|
|
369
|
+
const rawPath = getAttribute(shape, null, "path");
|
|
370
|
+
if (!rawPath) return EMPTY_RESULT;
|
|
371
|
+
const viewport = coordinateViewport(style);
|
|
372
|
+
if (!viewport) return INVALID_RESULT;
|
|
373
|
+
const space = localCoordinateSpace(shape, viewport.width, viewport.height);
|
|
374
|
+
const transform = space ? transformMatrix(viewport, space) : void 0;
|
|
375
|
+
if (!transform) return INVALID_RESULT;
|
|
376
|
+
const path = renderVmlPath(rawPath, budget);
|
|
377
|
+
if (path.type !== "painted") return path;
|
|
378
|
+
const fragments = [];
|
|
379
|
+
for (const pathData of path.pathSets) {
|
|
380
|
+
const svg = `<path transform="${transform}" d="${pathData}" fill="${paint.fill}" fill-rule="evenodd" stroke="${paint.stroke}"/>`;
|
|
381
|
+
if (!consumeSvgCharacters(budget, svg.length - pathData.length)) return INVALID_RESULT;
|
|
382
|
+
fragments.push(svg);
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
type: "painted",
|
|
386
|
+
svg: fragments.join("")
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
const renderGroupChildren = (group, budget, depth) => {
|
|
390
|
+
const fragments = [];
|
|
391
|
+
for (const child of getChildElements(group)) {
|
|
392
|
+
if (budget.elements >= MAX_VML_PREVIEW_ELEMENTS) return INVALID_RESULT;
|
|
393
|
+
budget.elements += 1;
|
|
394
|
+
const localName = getLocalName(child.name ?? "");
|
|
395
|
+
let result;
|
|
396
|
+
switch (localName) {
|
|
397
|
+
case "group": {
|
|
398
|
+
if (depth >= MAX_VML_PREVIEW_DEPTH) return INVALID_RESULT;
|
|
399
|
+
const style = parseVmlStyle(getAttribute(child, null, "style"));
|
|
400
|
+
if (isHidden(style)) {
|
|
401
|
+
result = EMPTY_RESULT;
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
const viewport = coordinateViewport(style);
|
|
405
|
+
const space = viewport ? localCoordinateSpace(child, viewport.width, viewport.height) : void 0;
|
|
406
|
+
const transform = viewport && space ? transformMatrix(viewport, space) : void 0;
|
|
407
|
+
if (!transform) return INVALID_RESULT;
|
|
408
|
+
const content = renderGroupChildren(child, budget, depth + 1);
|
|
409
|
+
if (content.type !== "painted") {
|
|
410
|
+
result = content;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
const svg = `<g transform="${transform}">${content.svg}</g>`;
|
|
414
|
+
const wrapperCharacters = svg.length - content.svg.length;
|
|
415
|
+
result = consumeSvgCharacters(budget, wrapperCharacters) ? {
|
|
416
|
+
type: "painted",
|
|
417
|
+
svg
|
|
418
|
+
} : INVALID_RESULT;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
case "shape":
|
|
422
|
+
result = pathShapeFragment(child, budget);
|
|
423
|
+
break;
|
|
424
|
+
case "line":
|
|
425
|
+
result = lineFragment(child, budget);
|
|
426
|
+
break;
|
|
427
|
+
case "oval":
|
|
428
|
+
case "rect":
|
|
429
|
+
case "roundrect": {
|
|
430
|
+
const style = parseVmlStyle(getAttribute(child, null, "style"));
|
|
431
|
+
if (isHidden(style)) {
|
|
432
|
+
result = EMPTY_RESULT;
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
const viewport = coordinateViewport(style);
|
|
436
|
+
result = viewport ? primitiveShapeFragment(child, viewport, budget) : INVALID_RESULT;
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
default: result = EMPTY_RESULT;
|
|
440
|
+
}
|
|
441
|
+
switch (result.type) {
|
|
442
|
+
case "painted":
|
|
443
|
+
fragments.push(result.svg);
|
|
444
|
+
break;
|
|
445
|
+
case "empty": break;
|
|
446
|
+
case "invalid": return INVALID_RESULT;
|
|
447
|
+
default:
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return fragments.length > 0 ? {
|
|
451
|
+
type: "painted",
|
|
452
|
+
svg: fragments.join("")
|
|
453
|
+
} : EMPTY_RESULT;
|
|
454
|
+
};
|
|
455
|
+
/** Render a bounded standalone VML rectangle or oval preview. */
|
|
456
|
+
const renderStandaloneVmlPreview = (shape) => {
|
|
457
|
+
if (findDeep(shape, "v", "textbox")) return EMPTY_RESULT;
|
|
458
|
+
const style = parseVmlStyle(getAttribute(shape, null, "style"));
|
|
459
|
+
const widthPx = vmlCssLengthToPx(style["width"]);
|
|
460
|
+
const heightPx = vmlCssLengthToPx(style["height"]);
|
|
461
|
+
if (!isValidVmlPreviewDimension(widthPx) || !isValidVmlPreviewDimension(heightPx)) return INVALID_RESULT;
|
|
462
|
+
const budget = {
|
|
463
|
+
elements: 1,
|
|
464
|
+
pathPoints: 0,
|
|
465
|
+
svgCharacters: 0
|
|
466
|
+
};
|
|
467
|
+
const content = primitiveShapeFragment(shape, {
|
|
468
|
+
x: 0,
|
|
469
|
+
y: 0,
|
|
470
|
+
width: widthPx,
|
|
471
|
+
height: heightPx
|
|
472
|
+
}, budget);
|
|
473
|
+
if (content.type !== "painted") return content;
|
|
474
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${widthPx} ${heightPx}" width="${widthPx}" height="${heightPx}">${content.svg}</svg>`;
|
|
475
|
+
if (!consumeSvgCharacters(budget, svg.length - content.svg.length)) return INVALID_RESULT;
|
|
476
|
+
return {
|
|
477
|
+
type: "rendered",
|
|
478
|
+
svg,
|
|
479
|
+
widthPx,
|
|
480
|
+
heightPx,
|
|
481
|
+
style
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
/** Render a VML group through bounded, non-clipping local-coordinate transforms. */
|
|
485
|
+
const renderVmlGroupPreview = (group) => {
|
|
486
|
+
const style = parseVmlStyle(getAttribute(group, null, "style"));
|
|
487
|
+
const widthPx = vmlCssLengthToPx(style["width"]);
|
|
488
|
+
const heightPx = vmlCssLengthToPx(style["height"]);
|
|
489
|
+
if (!isValidVmlPreviewDimension(widthPx) || !isValidVmlPreviewDimension(heightPx)) return INVALID_RESULT;
|
|
490
|
+
const space = localCoordinateSpace(group, widthPx, heightPx);
|
|
491
|
+
if (!space) return INVALID_RESULT;
|
|
492
|
+
const budget = {
|
|
493
|
+
elements: 1,
|
|
494
|
+
pathPoints: 0,
|
|
495
|
+
svgCharacters: 0
|
|
496
|
+
};
|
|
497
|
+
const content = renderGroupChildren(group, budget, 0);
|
|
498
|
+
if (content.type !== "painted") return content;
|
|
499
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${space.origin.x} ${space.origin.y} ${space.size.x} ${space.size.y}" width="${widthPx}" height="${heightPx}">${content.svg}</svg>`;
|
|
500
|
+
if (!consumeSvgCharacters(budget, svg.length - content.svg.length)) return INVALID_RESULT;
|
|
501
|
+
return {
|
|
502
|
+
type: "rendered",
|
|
503
|
+
svg,
|
|
504
|
+
widthPx,
|
|
505
|
+
heightPx,
|
|
506
|
+
style
|
|
507
|
+
};
|
|
508
|
+
};
|
|
509
|
+
/** Bound retained synthetic VML previews while preserving their raw replay nodes. */
|
|
510
|
+
const enforcePackageVmlPreviewBudget = (root, maxCharacters = MAX_PACKAGE_VML_PREVIEW_CHARACTERS) => {
|
|
511
|
+
let remainingCharacters = Math.max(0, maxCharacters);
|
|
512
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
513
|
+
const visit = (value) => {
|
|
514
|
+
if (value === null || typeof value !== "object" || visited.has(value)) return;
|
|
515
|
+
visited.add(value);
|
|
516
|
+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return;
|
|
517
|
+
if (value instanceof Map) {
|
|
518
|
+
for (const child of value.values()) visit(child);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (Array.isArray(value)) {
|
|
522
|
+
for (const child of value) visit(child);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if ("type" in value && value.type === "image" && "rId" in value && value.rId === "" && "mimeType" in value && value.mimeType === VML_PREVIEW_MIME_TYPE && "filename" in value && value.filename === VML_PREVIEW_FILENAME && "src" in value && typeof value.src === "string" && value.src.startsWith(VML_PREVIEW_DATA_URL_PREFIX)) if (value.src.length <= remainingCharacters) remainingCharacters -= value.src.length;
|
|
526
|
+
else {
|
|
527
|
+
remainingCharacters = 0;
|
|
528
|
+
delete value.src;
|
|
529
|
+
}
|
|
530
|
+
for (const child of Object.values(value)) visit(child);
|
|
531
|
+
};
|
|
532
|
+
visit(root);
|
|
533
|
+
};
|
|
534
|
+
//#endregion
|
|
535
|
+
export { enforcePackageVmlPreviewBudget, isValidVmlPreviewDimension, parseVmlNumber, parseVmlStyle, renderStandaloneVmlPreview, renderVmlGroupPreview, vmlCssLengthToPx, vmlSvgDataUrl };
|
package/dist/docx/wrapTypes.d.ts
CHANGED
|
@@ -1,36 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Wrap-type predicates shared across the layout pipeline.
|
|
4
|
-
*
|
|
5
|
-
* Mirrors eigenpal docx-editor `wrapTypes` so that the new textBoxFlow
|
|
6
|
-
* predicates (eigenpal #474) and the existing image classifiers agree on
|
|
7
|
-
* which OOXML wrap variants are floating, which paint over body text, and
|
|
8
|
-
* which wrap around it.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* OOXML text-wrap variants for a drawing anchor (`wp:inline` vs the
|
|
12
|
-
* `wp:wrap*` elements). `inline` keeps the object in the text run; every other
|
|
13
|
-
* variant takes it out of inline flow. Mirrors the eigenpal docx-editor union
|
|
14
|
-
* so adapters share one wrap vocabulary.
|
|
15
|
-
*/
|
|
16
|
-
type WrapType = "inline" | "square" | "tight" | "through" | "topAndBottom" | "behind" | "inFront";
|
|
17
|
-
/**
|
|
18
|
-
* `true` for any wrap type that takes the object out of inline flow.
|
|
19
|
-
* Includes `topAndBottom` and the wrapNone variants (`behind`/`inFront`):
|
|
20
|
-
* those are anchored at absolute coordinates even though they don't shrink
|
|
21
|
-
* line widths.
|
|
22
|
-
*/
|
|
23
|
-
declare function isFloatingWrapType(wrapType: string | undefined): boolean;
|
|
24
|
-
/**
|
|
25
|
-
* `true` for `behind` and `inFront` — wrapNone variants that don't shrink
|
|
26
|
-
* line widths; text paints over or under the object.
|
|
27
|
-
*/
|
|
28
|
-
declare function isWrapNone(wrapType: string | undefined): boolean;
|
|
29
|
-
/**
|
|
30
|
-
* `true` for wrap types that actually divert lines around the object
|
|
31
|
-
* (`square` / `tight` / `through`). Excludes `topAndBottom` (block flow)
|
|
32
|
-
* and the wrapNone variants.
|
|
33
|
-
*/
|
|
34
|
-
declare function wrapsAroundText(wrapType: string | undefined): boolean;
|
|
35
|
-
//#endregion
|
|
36
|
-
export { WrapType, isFloatingWrapType, isWrapNone, wrapsAroundText };
|
|
1
|
+
import { WrapType, isFloatingWrapType, isWrapNone, wrapsAroundText } from "../types/wrap.js";
|
|
2
|
+
export { type WrapType, isFloatingWrapType, isWrapNone, wrapsAroundText };
|
package/dist/docx/wrapTypes.js
CHANGED
|
@@ -1,40 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
const FLOATING_WRAP_TYPES = /* @__PURE__ */ new Set([
|
|
3
|
-
"square",
|
|
4
|
-
"tight",
|
|
5
|
-
"through",
|
|
6
|
-
"topAndBottom",
|
|
7
|
-
"behind",
|
|
8
|
-
"inFront"
|
|
9
|
-
]);
|
|
10
|
-
const TEXT_WRAPPING_TYPES = /* @__PURE__ */ new Set([
|
|
11
|
-
"square",
|
|
12
|
-
"tight",
|
|
13
|
-
"through"
|
|
14
|
-
]);
|
|
15
|
-
/**
|
|
16
|
-
* `true` for any wrap type that takes the object out of inline flow.
|
|
17
|
-
* Includes `topAndBottom` and the wrapNone variants (`behind`/`inFront`):
|
|
18
|
-
* those are anchored at absolute coordinates even though they don't shrink
|
|
19
|
-
* line widths.
|
|
20
|
-
*/
|
|
21
|
-
function isFloatingWrapType(wrapType) {
|
|
22
|
-
return wrapType !== void 0 && FLOATING_WRAP_TYPES.has(wrapType);
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* `true` for `behind` and `inFront` — wrapNone variants that don't shrink
|
|
26
|
-
* line widths; text paints over or under the object.
|
|
27
|
-
*/
|
|
28
|
-
function isWrapNone(wrapType) {
|
|
29
|
-
return wrapType === "behind" || wrapType === "inFront";
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* `true` for wrap types that actually divert lines around the object
|
|
33
|
-
* (`square` / `tight` / `through`). Excludes `topAndBottom` (block flow)
|
|
34
|
-
* and the wrapNone variants.
|
|
35
|
-
*/
|
|
36
|
-
function wrapsAroundText(wrapType) {
|
|
37
|
-
return wrapType !== void 0 && TEXT_WRAPPING_TYPES.has(wrapType);
|
|
38
|
-
}
|
|
39
|
-
//#endregion
|
|
1
|
+
import { isFloatingWrapType, isWrapNone, wrapsAroundText } from "../types/wrap.js";
|
|
40
2
|
export { isFloatingWrapType, isWrapNone, wrapsAroundText };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
//#region src/fonts/fontAlternates.d.ts
|
|
3
|
+
type FontAlternates = ReadonlyMap<string, string>;
|
|
4
|
+
/** Build a document-scoped primary-name → OOXML alternate-name lookup. */
|
|
5
|
+
declare const buildFontAlternates: (fontTable: document_d_exports.FontTable | null | undefined) => FontAlternates;
|
|
6
|
+
declare const getFontAlternate: (family: string | undefined, alternates: FontAlternates | undefined) => string | undefined;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { FontAlternates, buildFontAlternates, getFontAlternate };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/fonts/fontAlternates.ts
|
|
2
|
+
const normalizeFontName = (name) => name.trim().toLowerCase();
|
|
3
|
+
/** Build a document-scoped primary-name → OOXML alternate-name lookup. */
|
|
4
|
+
const buildFontAlternates = (fontTable) => {
|
|
5
|
+
const alternates = /* @__PURE__ */ new Map();
|
|
6
|
+
for (const font of fontTable?.fonts ?? []) {
|
|
7
|
+
const primary = normalizeFontName(font.name);
|
|
8
|
+
const alternate = font.altName?.trim();
|
|
9
|
+
if (!primary || !alternate || normalizeFontName(alternate) === primary) continue;
|
|
10
|
+
alternates.set(primary, alternate);
|
|
11
|
+
}
|
|
12
|
+
return alternates;
|
|
13
|
+
};
|
|
14
|
+
const getFontAlternate = (family, alternates) => family === void 0 ? void 0 : alternates?.get(normalizeFontName(family));
|
|
15
|
+
//#endregion
|
|
16
|
+
export { buildFontAlternates, getFontAlternate };
|
|
@@ -6,6 +6,7 @@ type MeasureBlocksFn = (blocks: FlowBlock[], contentWidth: number) => Measure[];
|
|
|
6
6
|
type ConvertFootnoteOptions = {
|
|
7
7
|
styles?: document_d_exports.StyleDefinitions | null;
|
|
8
8
|
theme?: document_d_exports.Theme | null;
|
|
9
|
+
fontAlternates?: ToFlowBlocksOptions["fontAlternates"];
|
|
9
10
|
measureBlocks?: MeasureBlocksFn;
|
|
10
11
|
/** Document-wide `w:defaultTabStop` in twips — forwarded to toFlowBlocks. */
|
|
11
12
|
defaultTabStopTwips?: number;
|