@stll/folio-core 0.38.0 → 0.39.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 +4 -0
- package/dist/display-list/build/buildContext.d.ts +3 -1
- package/dist/display-list/build/buildDisplayList.d.ts +4 -1
- package/dist/display-list/build/buildDisplayList.js +38 -4
- package/dist/display-list/build/commentAnnotations.d.ts +10 -0
- package/dist/display-list/build/commentAnnotations.js +17 -0
- package/dist/display-list/build/paragraphPrimitives.js +39 -1
- package/dist/display-list/primitives.d.ts +1 -1
- package/dist/display-list/types.d.ts +16 -1
- package/dist/docx/diagramPreview.d.ts +7 -0
- package/dist/docx/diagramPreview.js +191 -0
- package/dist/docx/paragraphTextBoxEnrichment.js +2 -1
- package/dist/docx/parser.js +13 -0
- package/dist/docx/runParser.js +8 -0
- package/dist/docx/serializer/runSerializer.js +3 -1
- package/dist/docx/textBoxParser.js +25 -5
- package/dist/export-pdf.js +2 -0
- package/dist/headless-layout.js +1 -0
- package/dist/layout-engine/index.js +5 -5
- package/dist/layout-engine/sectionVerticalAlignment.d.ts +7 -0
- package/dist/layout-engine/sectionVerticalAlignment.js +52 -0
- package/dist/layout-engine/types.d.ts +3 -1
- package/dist/pdf/writePdf.js +37 -2
- package/dist/prosemirror/attrs/index.js +35 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +1 -0
- package/dist/prosemirror/conversion/toProseDoc.js +2 -0
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -1
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +39 -1
- package/dist/prosemirror/schema/nodes.d.ts +2 -0
- package/package.json +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { displayCommentsFrom } from "../display-list/build/commentAnnotations.js";
|
|
1
2
|
import { createDisplayListPagePainter } from "../display-list/editor/displayListPagePainter.js";
|
|
2
3
|
import { PAGE_RENDERER, displayListFurnitureFrom } from "../display-list/editor/pageRenderer.js";
|
|
3
4
|
import { resolveDocumentGridLinePitch } from "../docx/documentGrid.js";
|
|
@@ -307,6 +308,8 @@ function runLayoutPipeline(deps, state, options = {}) {
|
|
|
307
308
|
pageGap,
|
|
308
309
|
mirrorMargins
|
|
309
310
|
};
|
|
311
|
+
if (document?.package.document.sections !== void 0) nextLayoutOpts.sectionVerticalAlignments = document.package.document.sections.map(({ properties }) => properties.verticalAlign);
|
|
312
|
+
else if (sectionProperties !== null && sectionProperties !== void 0) nextLayoutOpts.sectionVerticalAlignments = [sectionProperties.verticalAlign];
|
|
310
313
|
if (hasTitlePg) nextLayoutOpts.firstPageMargins = bodyMarginsClearHeaderFooter({
|
|
311
314
|
authoredMargins: margins,
|
|
312
315
|
preparedHeader: firstPageHeaderForRender,
|
|
@@ -513,6 +516,7 @@ function runLayoutPipeline(deps, state, options = {}) {
|
|
|
513
516
|
layout: newLayout,
|
|
514
517
|
blockLookup,
|
|
515
518
|
doc: pagesContainer.ownerDocument,
|
|
519
|
+
...document?.package.document.comments === void 0 ? {} : { comments: displayCommentsFrom(document.package.document.comments) },
|
|
516
520
|
...displayListFurnitureFrom(renderOpts),
|
|
517
521
|
...footnoteContentMap.size === 0 ? {} : { footnoteContentById: footnoteContentMap }
|
|
518
522
|
}).paintPage;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DisplayColor, DisplayLink, DisplayLinkTarget, DisplayStoryRef } from "../types.js";
|
|
1
|
+
import { DisplayColor, DisplayLink, DisplayLinkTarget, DisplayRect, DisplayStoryRef } from "../types.js";
|
|
2
2
|
import { FontTable } from "./fontTable.js";
|
|
3
3
|
import { ImageTable } from "./imagePrimitives.js";
|
|
4
4
|
import { UnsupportedCollector } from "./unsupported.js";
|
|
@@ -23,6 +23,8 @@ type BuildContext = {
|
|
|
23
23
|
readonly authorColors: AuthorColorTable;
|
|
24
24
|
/** Link sink for the page being built. */
|
|
25
25
|
readonly links: DisplayLink[];
|
|
26
|
+
/** Exact painted rectangles for each comment range on this page. */
|
|
27
|
+
readonly commentRects: Map<number, DisplayRect[]>;
|
|
26
28
|
/**
|
|
27
29
|
* Bookmark name → where it lands, collected in a first pass over the layout.
|
|
28
30
|
* A `#name` hyperlink can only become a `page` target once every page is
|
|
@@ -3,6 +3,7 @@ import { Layout } from "../../layout-engine/types.js";
|
|
|
3
3
|
import { BlockLookup } from "../../layout-painter/index.js";
|
|
4
4
|
import { DisplayColor, DisplayList, DisplayMetadata, DisplayPage } from "../types.js";
|
|
5
5
|
import { PageFurnitureInputs } from "./furniture.js";
|
|
6
|
+
import { DisplayCommentInput } from "./commentAnnotations.js";
|
|
6
7
|
//#region src/display-list/build/buildDisplayList.d.ts
|
|
7
8
|
/**
|
|
8
9
|
* Every feature a source document can have that a `Layout` does not record and
|
|
@@ -42,6 +43,8 @@ type BuildDisplayListOptions = PageFurnitureInputs & {
|
|
|
42
43
|
* that cannot find that name on the host paints the document in a substitute.
|
|
43
44
|
*/
|
|
44
45
|
readonly embeddedFonts?: readonly EmbeddedFont[];
|
|
46
|
+
/** Comment bodies keyed by the `commentIds` already carried on layout runs. */
|
|
47
|
+
readonly comments?: readonly DisplayCommentInput[];
|
|
45
48
|
};
|
|
46
49
|
/**
|
|
47
50
|
* A document's display list, one page at a time.
|
|
@@ -68,7 +71,7 @@ type DisplayListBuilder = {
|
|
|
68
71
|
*/
|
|
69
72
|
readonly snapshot: () => DisplayList;
|
|
70
73
|
};
|
|
71
|
-
declare const createDisplayListBuilder: ({ layout, blockLookup, metadata, pageBackground, documentFeatures, embeddedFonts, ...furniture }: BuildDisplayListOptions) => DisplayListBuilder;
|
|
74
|
+
declare const createDisplayListBuilder: ({ layout, blockLookup, metadata, pageBackground, documentFeatures, embeddedFonts, comments, ...furniture }: BuildDisplayListOptions) => DisplayListBuilder;
|
|
72
75
|
/**
|
|
73
76
|
* The whole document's display list. Every page is built, so the tables, the
|
|
74
77
|
* outline and the gap report are complete; an editor that paints a window of
|
|
@@ -27,6 +27,34 @@ const FIRST_PAGE_INDEX = 0;
|
|
|
27
27
|
* document-wide flag the caller would have to remember to set.
|
|
28
28
|
*/
|
|
29
29
|
const DOCUMENT_FEATURE_GAPS = [["pageBorders", "w:pgBorders reach the painter through render options, not through Layout, and none were supplied to the builder"], ["watermark", "watermarks reach the painter through render options, not through Layout, and none was supplied to the builder"]];
|
|
30
|
+
const rectKey = ({ xPx, yPx, widthPx, heightPx }) => `${String(xPx)}:${String(yPx)}:${String(widthPx)}:${String(heightPx)}`;
|
|
31
|
+
const dedupeRects = (rects) => {
|
|
32
|
+
const seen = /* @__PURE__ */ new Set();
|
|
33
|
+
const unique = [];
|
|
34
|
+
for (const rect of rects) {
|
|
35
|
+
const key = rectKey(rect);
|
|
36
|
+
if (seen.has(key)) continue;
|
|
37
|
+
seen.add(key);
|
|
38
|
+
unique.push(rect);
|
|
39
|
+
}
|
|
40
|
+
return unique;
|
|
41
|
+
};
|
|
42
|
+
const buildCommentAnnotations = (commentsById, commentRects) => {
|
|
43
|
+
const annotations = [];
|
|
44
|
+
for (const [commentId, rects] of commentRects) {
|
|
45
|
+
const comment = commentsById.get(commentId);
|
|
46
|
+
if (comment === void 0) continue;
|
|
47
|
+
const uniqueRects = dedupeRects(rects);
|
|
48
|
+
if (uniqueRects.length === 0) continue;
|
|
49
|
+
annotations.push({
|
|
50
|
+
commentId,
|
|
51
|
+
rects: uniqueRects,
|
|
52
|
+
contents: comment.contents,
|
|
53
|
+
author: comment.author
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return annotations;
|
|
57
|
+
};
|
|
30
58
|
const paragraphTextOf = (block) => block.runs.map((run) => run.kind === "text" ? run.text : "").join("").trim();
|
|
31
59
|
/**
|
|
32
60
|
* Resolve everything that needs the whole document before any page is painted:
|
|
@@ -126,14 +154,16 @@ const paintFragment = ({ fragment, entry, context, composer, prevEntry, nextEntr
|
|
|
126
154
|
}
|
|
127
155
|
}
|
|
128
156
|
};
|
|
129
|
-
const buildPage = ({ page, pageIndex, totalPages, blockLookup, pageBackground, bookmarkTargets, fonts, images, unsupported, authorColors, furniture }) => {
|
|
157
|
+
const buildPage = ({ page, pageIndex, totalPages, blockLookup, pageBackground, bookmarkTargets, fonts, images, unsupported, authorColors, furniture, commentsById }) => {
|
|
130
158
|
const links = [];
|
|
159
|
+
const commentRects = /* @__PURE__ */ new Map();
|
|
131
160
|
const context = {
|
|
132
161
|
fonts,
|
|
133
162
|
images,
|
|
134
163
|
unsupported,
|
|
135
164
|
authorColors,
|
|
136
165
|
links,
|
|
166
|
+
commentRects,
|
|
137
167
|
bookmarkTargets,
|
|
138
168
|
story: { kind: "body" },
|
|
139
169
|
pageIndex,
|
|
@@ -208,7 +238,8 @@ const buildPage = ({ page, pageIndex, totalPages, blockLookup, pageBackground, b
|
|
|
208
238
|
orientation: page.orientation ?? (page.size.w > page.size.h ? "landscape" : "portrait"),
|
|
209
239
|
primitives: composer.primitives(),
|
|
210
240
|
regions: composer.regions(),
|
|
211
|
-
links
|
|
241
|
+
links,
|
|
242
|
+
comments: buildCommentAnnotations(commentsById, commentRects)
|
|
212
243
|
};
|
|
213
244
|
};
|
|
214
245
|
/** Which kind of region a fragment paints into. */
|
|
@@ -223,12 +254,14 @@ const SUPPLIED_BY = {
|
|
|
223
254
|
pageBorders: (furniture) => furniture.pageBorders !== void 0,
|
|
224
255
|
watermark: (furniture) => furniture.watermark !== void 0 || furniture.watermarkByHeaderRId !== void 0
|
|
225
256
|
};
|
|
226
|
-
const createDisplayListBuilder = ({ layout, blockLookup, metadata, pageBackground, documentFeatures, embeddedFonts, ...furniture }) => {
|
|
257
|
+
const createDisplayListBuilder = ({ layout, blockLookup, metadata, pageBackground, documentFeatures, embeddedFonts, comments, ...furniture }) => {
|
|
227
258
|
const fonts = new FontTable(embeddedFonts ?? []);
|
|
228
259
|
const images = new ImageTable();
|
|
229
260
|
const unsupported = new UnsupportedCollector();
|
|
230
261
|
const authorColors = new AuthorColorTable();
|
|
231
262
|
const { bookmarkTargets, outline } = collectDocumentTargets(layout, blockLookup);
|
|
263
|
+
const commentsById = /* @__PURE__ */ new Map();
|
|
264
|
+
for (const comment of comments ?? []) commentsById.set(comment.id, comment);
|
|
232
265
|
if (layout.pages.length > 0) for (const [feature, detail] of DOCUMENT_FEATURE_GAPS) {
|
|
233
266
|
if (SUPPLIED_BY[feature](furniture)) continue;
|
|
234
267
|
if (documentFeatures !== void 0 && !documentFeatures[feature]) continue;
|
|
@@ -251,7 +284,8 @@ const createDisplayListBuilder = ({ layout, blockLookup, metadata, pageBackgroun
|
|
|
251
284
|
images,
|
|
252
285
|
unsupported,
|
|
253
286
|
authorColors,
|
|
254
|
-
furniture
|
|
287
|
+
furniture,
|
|
288
|
+
commentsById
|
|
255
289
|
});
|
|
256
290
|
built.set(pageIndex, display);
|
|
257
291
|
return display;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { document_d_exports } from "../../types/document.js";
|
|
2
|
+
//#region src/display-list/build/commentAnnotations.d.ts
|
|
3
|
+
type DisplayCommentInput = {
|
|
4
|
+
readonly id: number;
|
|
5
|
+
readonly author: string;
|
|
6
|
+
readonly contents: string;
|
|
7
|
+
};
|
|
8
|
+
declare const displayCommentsFrom: (comments: readonly document_d_exports.Comment[]) => readonly DisplayCommentInput[];
|
|
9
|
+
//#endregion
|
|
10
|
+
export { DisplayCommentInput, displayCommentsFrom };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getParagraphText } from "../../docx/paragraphParser.js";
|
|
2
|
+
//#region src/display-list/build/commentAnnotations.ts
|
|
3
|
+
const displayCommentsFrom = (comments) => {
|
|
4
|
+
const inputs = [];
|
|
5
|
+
for (const { id, author, content } of comments) {
|
|
6
|
+
const paragraphs = [];
|
|
7
|
+
for (const paragraph of content) paragraphs.push(getParagraphText(paragraph));
|
|
8
|
+
inputs.push({
|
|
9
|
+
id,
|
|
10
|
+
author,
|
|
11
|
+
contents: paragraphs.join("\n")
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return inputs;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { displayCommentsFrom };
|
|
@@ -45,6 +45,20 @@ const HYPERLINK_COLOR = {
|
|
|
45
45
|
b: 193,
|
|
46
46
|
a: 1
|
|
47
47
|
};
|
|
48
|
+
/** Matches the editable DOM painter's review-range treatment. */
|
|
49
|
+
const COMMENT_BACKGROUND = {
|
|
50
|
+
r: 255,
|
|
51
|
+
g: 212,
|
|
52
|
+
b: 0,
|
|
53
|
+
a: .08
|
|
54
|
+
};
|
|
55
|
+
const COMMENT_BORDER = {
|
|
56
|
+
r: 180,
|
|
57
|
+
g: 130,
|
|
58
|
+
b: 0,
|
|
59
|
+
a: .24
|
|
60
|
+
};
|
|
61
|
+
const COMMENT_BORDER_WIDTH_PX = 1;
|
|
48
62
|
/** `renderParagraph.ts:603-616`: a raised run paints 0.4em up, a lowered one 0.2em down. */
|
|
49
63
|
const SUPERSCRIPT_RISE_RATIO = .4;
|
|
50
64
|
const SUBSCRIPT_DROP_RATIO = .2;
|
|
@@ -275,8 +289,32 @@ const emitGlyphRun = ({ sink, context, run, glyphs, style, paintXPx, baselineYPx
|
|
|
275
289
|
let runBaselineYPx = baselineYPx - (run.positionPx ?? 0);
|
|
276
290
|
if (run.superscript) runBaselineYPx -= fontSizePx * SUPERSCRIPT_RISE_RATIO;
|
|
277
291
|
else if (run.subscript) runBaselineYPx += fontSizePx * SUBSCRIPT_DROP_RATIO;
|
|
292
|
+
const commentIds = run.commentIds ?? [];
|
|
278
293
|
const background = run.highlight ?? run.shading;
|
|
279
|
-
if (
|
|
294
|
+
if (commentIds.length > 0 && glyphs.widthPx > 0) {
|
|
295
|
+
const metrics = getFontMetrics(style);
|
|
296
|
+
const rect = {
|
|
297
|
+
xPx: paintXPx,
|
|
298
|
+
yPx: runBaselineYPx - metrics.fontBoxAscent,
|
|
299
|
+
widthPx: glyphs.widthPx,
|
|
300
|
+
heightPx: metrics.fontBoxAscent + metrics.fontBoxDescent
|
|
301
|
+
};
|
|
302
|
+
sink.backgrounds.push({
|
|
303
|
+
kind: "rect",
|
|
304
|
+
rect,
|
|
305
|
+
fill: COMMENT_BACKGROUND
|
|
306
|
+
});
|
|
307
|
+
sink.decorations.push(horizontalLine(paintXPx, glyphs.widthPx, rect.yPx + rect.heightPx - .5, {
|
|
308
|
+
color: COMMENT_BORDER,
|
|
309
|
+
thicknessPx: COMMENT_BORDER_WIDTH_PX,
|
|
310
|
+
pattern: "solid"
|
|
311
|
+
}));
|
|
312
|
+
for (const commentId of commentIds) {
|
|
313
|
+
const rects = context.commentRects.get(commentId);
|
|
314
|
+
if (rects === void 0) context.commentRects.set(commentId, [rect]);
|
|
315
|
+
else rects.push(rect);
|
|
316
|
+
}
|
|
317
|
+
} else if (background && glyphs.widthPx > 0) {
|
|
280
318
|
const fill = parseDisplayColor(background);
|
|
281
319
|
if (fill) {
|
|
282
320
|
const metrics = getFontMetrics(style);
|
|
@@ -15,7 +15,7 @@ import { hasComplexScript, isComplexScriptCodePoint } from "../utils/scriptSegme
|
|
|
15
15
|
* advances rightward from its origin whatever the paragraph does.
|
|
16
16
|
*/
|
|
17
17
|
declare const glyphCellOffsetsPx: (run: DisplayGlyphRun) => readonly number[];
|
|
18
|
-
declare const DISPLAY_PRIMITIVE_KINDS: ("
|
|
18
|
+
declare const DISPLAY_PRIMITIVE_KINDS: ("line" | "rect" | "image" | "glyphRun" | "clipGroup" | "rotateGroup" | "opacityGroup")[];
|
|
19
19
|
/**
|
|
20
20
|
* Dash geometry of each stroke pattern, as multiples of the stroke thickness.
|
|
21
21
|
*
|
|
@@ -357,6 +357,19 @@ type DisplayLinkTarget = {
|
|
|
357
357
|
readonly pageIndex: number;
|
|
358
358
|
readonly yPx: number;
|
|
359
359
|
};
|
|
360
|
+
/**
|
|
361
|
+
* One DOCX comment whose anchor reaches this page.
|
|
362
|
+
*
|
|
363
|
+
* A range may cross run and line boundaries, so every painted rectangle is
|
|
364
|
+
* retained. Raster backends use those rectangles for the highlight; PDF uses
|
|
365
|
+
* the final one as the location of its single text annotation.
|
|
366
|
+
*/
|
|
367
|
+
type DisplayCommentAnnotation = {
|
|
368
|
+
readonly commentId: number;
|
|
369
|
+
readonly rects: readonly DisplayRect[];
|
|
370
|
+
readonly contents: string;
|
|
371
|
+
readonly author: string;
|
|
372
|
+
};
|
|
360
373
|
/**
|
|
361
374
|
* What kind of thing a click landed in.
|
|
362
375
|
*
|
|
@@ -437,6 +450,8 @@ type DisplayPage = {
|
|
|
437
450
|
*/
|
|
438
451
|
readonly regions: readonly DisplayHitRegion[];
|
|
439
452
|
readonly links: readonly DisplayLink[];
|
|
453
|
+
/** Native review comments anchored on this page. */
|
|
454
|
+
readonly comments: readonly DisplayCommentAnnotation[];
|
|
440
455
|
};
|
|
441
456
|
/** One heading, for a document outline. `pageIndex` is 0-based. */
|
|
442
457
|
type DisplayOutlineEntry = {
|
|
@@ -473,4 +488,4 @@ type DisplayList = {
|
|
|
473
488
|
readonly unsupported: readonly DisplayUnsupported[];
|
|
474
489
|
};
|
|
475
490
|
//#endregion
|
|
476
|
-
export { DisplayClipGroup, DisplayColor, DisplayEmbeddedFont, DisplayFontFace, DisplayFontRef, DisplayGlyphRun, DisplayHitRegion, DisplayHitRegionKind, DisplayHitRegionModel, DisplayImagePrimitive, DisplayImageRef, DisplayImageSource, DisplayLine, DisplayLink, DisplayLinkTarget, DisplayList, DisplayMetadata, DisplayModelRange, DisplayOpacityGroup, DisplayOutlineEntry, DisplayPage, DisplayPrimitive, DisplayRect, DisplayRectPrimitive, DisplayRotateGroup, DisplayRunAdjustments, DisplayStoryRef, DisplayStroke, DisplayStrokePattern, DisplayUnsupported };
|
|
491
|
+
export { DisplayClipGroup, DisplayColor, DisplayCommentAnnotation, DisplayEmbeddedFont, DisplayFontFace, DisplayFontRef, DisplayGlyphRun, DisplayHitRegion, DisplayHitRegionKind, DisplayHitRegionModel, DisplayImagePrimitive, DisplayImageRef, DisplayImageSource, DisplayLine, DisplayLink, DisplayLinkTarget, DisplayList, DisplayMetadata, DisplayModelRange, DisplayOpacityGroup, DisplayOutlineEntry, DisplayPage, DisplayPrimitive, DisplayRect, DisplayRectPrimitive, DisplayRotateGroup, DisplayRunAdjustments, DisplayStoryRef, DisplayStroke, DisplayStrokePattern, DisplayUnsupported };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
import { XmlElement } from "./xmlParser.js";
|
|
3
|
+
//#region src/docx/diagramPreview.d.ts
|
|
4
|
+
/** Create a deliberately simple, bounded preview; it is never an editable diagram projection. */
|
|
5
|
+
declare const parseDiagramPreview: (drawing: XmlElement, rels: document_d_exports.RelationshipMap | undefined, media: Map<string, document_d_exports.MediaFile> | undefined) => document_d_exports.Image | null;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { parseDiagramPreview };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { resolveRelativePath } from "./relsParser.js";
|
|
2
|
+
import { findChildByNamespaceUri, getAttribute, getLocalName, parseNumericAttribute, parseXmlDocument } from "./xmlParser.js";
|
|
3
|
+
//#region src/docx/diagramPreview.ts
|
|
4
|
+
const MAX_PREVIEW_SHAPES = 128;
|
|
5
|
+
const MAX_PREVIEW_PIXELS = 144e4;
|
|
6
|
+
const MAX_PREVIEW_PAINT_PIXELS = MAX_PREVIEW_PIXELS * 4;
|
|
7
|
+
const DRAWINGML_NAMESPACE_URIS = /* @__PURE__ */ new Set(["http://schemas.openxmlformats.org/drawingml/2006/main", "http://purl.oclc.org/ooxml/drawingml/main"]);
|
|
8
|
+
const DIAGRAM_NAMESPACE_URIS = /* @__PURE__ */ new Set(["http://schemas.openxmlformats.org/drawingml/2006/diagram", "http://purl.oclc.org/ooxml/drawingml/diagram"]);
|
|
9
|
+
const DIAGRAM_DRAWING_NAMESPACE_URIS = /* @__PURE__ */ new Set(["http://schemas.microsoft.com/office/drawing/2008/diagram"]);
|
|
10
|
+
const WORD_DRAWING_NAMESPACE_URIS = /* @__PURE__ */ new Set(["http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", "http://purl.oclc.org/ooxml/drawingml/wordprocessingDrawing"]);
|
|
11
|
+
const crc32 = (bytes) => {
|
|
12
|
+
let crc = 4294967295;
|
|
13
|
+
for (const byte of bytes) {
|
|
14
|
+
crc ^= byte;
|
|
15
|
+
for (let bit = 0; bit < 8; bit += 1) crc = crc >>> 1 ^ (crc & 1 ? 3988292384 : 0);
|
|
16
|
+
}
|
|
17
|
+
return (crc ^ 4294967295) >>> 0;
|
|
18
|
+
};
|
|
19
|
+
const adler32 = (bytes) => {
|
|
20
|
+
let a = 1;
|
|
21
|
+
let b = 0;
|
|
22
|
+
for (const byte of bytes) {
|
|
23
|
+
a = (a + byte) % 65521;
|
|
24
|
+
b = (b + a) % 65521;
|
|
25
|
+
}
|
|
26
|
+
return b << 16 | a;
|
|
27
|
+
};
|
|
28
|
+
const pngChunk = (type, data) => {
|
|
29
|
+
const name = new TextEncoder().encode(type);
|
|
30
|
+
const output = new Uint8Array(12 + data.length);
|
|
31
|
+
const view = new DataView(output.buffer);
|
|
32
|
+
view.setUint32(0, data.length);
|
|
33
|
+
output.set(name, 4);
|
|
34
|
+
output.set(data, 8);
|
|
35
|
+
view.setUint32(8 + data.length, crc32(output.subarray(4, 8 + data.length)));
|
|
36
|
+
return output;
|
|
37
|
+
};
|
|
38
|
+
const previewPng = (width, height, shapes) => {
|
|
39
|
+
const scale = Math.min(1, Math.sqrt(MAX_PREVIEW_PIXELS / (width * height)));
|
|
40
|
+
const w = Math.max(1, Math.round(width * scale));
|
|
41
|
+
const h = Math.max(1, Math.round(height * scale));
|
|
42
|
+
const pixels = new Uint8Array((w * 4 + 1) * h);
|
|
43
|
+
for (let y = 0; y < h; y += 1) {
|
|
44
|
+
const row = y * (w * 4 + 1);
|
|
45
|
+
pixels[row] = 0;
|
|
46
|
+
for (let x = 0; x < w; x += 1) {
|
|
47
|
+
const index = row + 1 + x * 4;
|
|
48
|
+
pixels[index] = 238;
|
|
49
|
+
pixels[index + 1] = 242;
|
|
50
|
+
pixels[index + 2] = 247;
|
|
51
|
+
pixels[index + 3] = 255;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let paintedPixels = 0;
|
|
55
|
+
for (const shape of shapes) {
|
|
56
|
+
const sx = Math.max(0, Math.round(shape.x / width * w));
|
|
57
|
+
const sy = Math.max(0, Math.round(shape.y / height * h));
|
|
58
|
+
const ex = Math.min(w, Math.round((shape.x + shape.width) / width * w));
|
|
59
|
+
const ey = Math.min(h, Math.round((shape.y + shape.height) / height * h));
|
|
60
|
+
const red = Number.parseInt(shape.color.slice(0, 2), 16) || 232;
|
|
61
|
+
const green = Number.parseInt(shape.color.slice(2, 4), 16) || 238;
|
|
62
|
+
const blue = Number.parseInt(shape.color.slice(4, 6), 16) || 247;
|
|
63
|
+
const shapePixels = Math.max(0, ex - sx) * Math.max(0, ey - sy);
|
|
64
|
+
if (paintedPixels + shapePixels > MAX_PREVIEW_PAINT_PIXELS) break;
|
|
65
|
+
paintedPixels += shapePixels;
|
|
66
|
+
for (let y = sy; y < ey; y += 1) {
|
|
67
|
+
const row = y * (w * 4 + 1);
|
|
68
|
+
for (let x = sx; x < ex; x += 1) {
|
|
69
|
+
const index = row + 1 + x * 4;
|
|
70
|
+
pixels[index] = red;
|
|
71
|
+
pixels[index + 1] = green;
|
|
72
|
+
pixels[index + 2] = blue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const compressed = new Uint8Array(2 + pixels.length + Math.ceil(pixels.length / 65535) * 5 + 4);
|
|
77
|
+
compressed[0] = 120;
|
|
78
|
+
compressed[1] = 1;
|
|
79
|
+
let cursor = 2;
|
|
80
|
+
for (let offset = 0; offset < pixels.length;) {
|
|
81
|
+
const length = Math.min(65535, pixels.length - offset);
|
|
82
|
+
compressed[cursor++] = offset + length === pixels.length ? 1 : 0;
|
|
83
|
+
compressed[cursor++] = length & 255;
|
|
84
|
+
compressed[cursor++] = length >>> 8;
|
|
85
|
+
compressed[cursor++] = ~length & 255;
|
|
86
|
+
compressed[cursor++] = ~length >>> 8 & 255;
|
|
87
|
+
compressed.set(pixels.subarray(offset, offset + length), cursor);
|
|
88
|
+
cursor += length;
|
|
89
|
+
offset += length;
|
|
90
|
+
}
|
|
91
|
+
const output = new Uint8Array(cursor + 4);
|
|
92
|
+
output.set(compressed.subarray(0, cursor));
|
|
93
|
+
new DataView(output.buffer).setUint32(cursor, adler32(pixels));
|
|
94
|
+
const signature = new Uint8Array([
|
|
95
|
+
137,
|
|
96
|
+
80,
|
|
97
|
+
78,
|
|
98
|
+
71,
|
|
99
|
+
13,
|
|
100
|
+
10,
|
|
101
|
+
26,
|
|
102
|
+
10
|
|
103
|
+
]);
|
|
104
|
+
const header = /* @__PURE__ */ new Uint8Array(13);
|
|
105
|
+
new DataView(header.buffer).setUint32(0, w);
|
|
106
|
+
new DataView(header.buffer).setUint32(4, h);
|
|
107
|
+
header[8] = 8;
|
|
108
|
+
header[9] = 6;
|
|
109
|
+
const chunks = [
|
|
110
|
+
signature,
|
|
111
|
+
pngChunk("IHDR", header),
|
|
112
|
+
pngChunk("IDAT", output),
|
|
113
|
+
pngChunk("IEND", /* @__PURE__ */ new Uint8Array())
|
|
114
|
+
];
|
|
115
|
+
const png = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
|
|
116
|
+
let position = 0;
|
|
117
|
+
for (const chunk of chunks) {
|
|
118
|
+
png.set(chunk, position);
|
|
119
|
+
position += chunk.length;
|
|
120
|
+
}
|
|
121
|
+
return png;
|
|
122
|
+
};
|
|
123
|
+
const extent = (drawing) => {
|
|
124
|
+
const value = findChildByNamespaceUri(findChildByNamespaceUri(drawing, WORD_DRAWING_NAMESPACE_URIS, "inline") ?? findChildByNamespaceUri(drawing, WORD_DRAWING_NAMESPACE_URIS, "anchor"), WORD_DRAWING_NAMESPACE_URIS, "extent");
|
|
125
|
+
return {
|
|
126
|
+
width: parseNumericAttribute(value, null, "cx") ?? 0,
|
|
127
|
+
height: parseNumericAttribute(value, null, "cy") ?? 0
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
const cachedDiagramShapes = (rels, media) => {
|
|
131
|
+
const matches = [...rels.values()].filter((relationship) => relationship.type === "http://schemas.microsoft.com/office/2007/relationships/diagramDrawing");
|
|
132
|
+
if (matches.length !== 1 || !matches[0]?.target) return [];
|
|
133
|
+
const path = resolveRelativePath("word/document.xml", matches[0].target);
|
|
134
|
+
const file = media.get(path);
|
|
135
|
+
if (!file?.data) return [];
|
|
136
|
+
const root = parseXmlDocument(new TextDecoder().decode(file.data));
|
|
137
|
+
if (!root) return [];
|
|
138
|
+
const shapes = [];
|
|
139
|
+
for (const shape of descendantsByNamespace(root, DIAGRAM_DRAWING_NAMESPACE_URIS, "sp").slice(0, MAX_PREVIEW_SHAPES)) {
|
|
140
|
+
const properties = findChildByNamespaceUri(shape, DIAGRAM_DRAWING_NAMESPACE_URIS, "spPr");
|
|
141
|
+
const transform = findChildByNamespaceUri(properties, DRAWINGML_NAMESPACE_URIS, "xfrm");
|
|
142
|
+
const offset = findChildByNamespaceUri(transform, DRAWINGML_NAMESPACE_URIS, "off");
|
|
143
|
+
const size = findChildByNamespaceUri(transform, DRAWINGML_NAMESPACE_URIS, "ext");
|
|
144
|
+
const width = parseNumericAttribute(size, null, "cx") ?? 0;
|
|
145
|
+
const height = parseNumericAttribute(size, null, "cy") ?? 0;
|
|
146
|
+
if (width <= 0 || height <= 0) continue;
|
|
147
|
+
const color = findChildByNamespaceUri(findChildByNamespaceUri(properties, DRAWINGML_NAMESPACE_URIS, "solidFill"), DRAWINGML_NAMESPACE_URIS, "srgbClr");
|
|
148
|
+
shapes.push({
|
|
149
|
+
x: parseNumericAttribute(offset, null, "x") ?? 0,
|
|
150
|
+
y: parseNumericAttribute(offset, null, "y") ?? 0,
|
|
151
|
+
width,
|
|
152
|
+
height,
|
|
153
|
+
color: getAttribute(color, null, "val") ?? "E8EEF7"
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return shapes;
|
|
157
|
+
};
|
|
158
|
+
const descendantsByNamespace = (root, namespaceUris, localName) => {
|
|
159
|
+
const result = [];
|
|
160
|
+
const visit = (element) => {
|
|
161
|
+
if (element.namespaceUri && namespaceUris.has(element.namespaceUri) && getLocalName(element.name ?? "") === localName) result.push(element);
|
|
162
|
+
for (const child of element.elements ?? []) if (child.type === "element") visit(child);
|
|
163
|
+
};
|
|
164
|
+
visit(root);
|
|
165
|
+
return result;
|
|
166
|
+
};
|
|
167
|
+
/** Create a deliberately simple, bounded preview; it is never an editable diagram projection. */
|
|
168
|
+
const parseDiagramPreview = (drawing, rels, media) => {
|
|
169
|
+
if (!rels || !media) return null;
|
|
170
|
+
const graphicData = descendantsByNamespace(drawing, DRAWINGML_NAMESPACE_URIS, "graphicData").at(0);
|
|
171
|
+
if (!graphicData || !DIAGRAM_NAMESPACE_URIS.has(getAttribute(graphicData, null, "uri") ?? "")) return null;
|
|
172
|
+
const { width, height } = extent(drawing);
|
|
173
|
+
if (width <= 0 || height <= 0) return null;
|
|
174
|
+
const png = previewPng(width, height, cachedDiagramShapes(rels, media));
|
|
175
|
+
let binary = "";
|
|
176
|
+
for (let offset = 0; offset < png.length; offset += 32768) binary += String.fromCodePoint(...png.subarray(offset, offset + 32768));
|
|
177
|
+
return {
|
|
178
|
+
type: "image",
|
|
179
|
+
rId: "",
|
|
180
|
+
src: `data:image/png;base64,${btoa(binary)}`,
|
|
181
|
+
mimeType: "image/png",
|
|
182
|
+
filename: "smartart-preview.png",
|
|
183
|
+
size: {
|
|
184
|
+
width,
|
|
185
|
+
height
|
|
186
|
+
},
|
|
187
|
+
wrap: { type: "inline" }
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
//#endregion
|
|
191
|
+
export { parseDiagramPreview };
|
|
@@ -207,7 +207,8 @@ const enrichTextBoxRuns = ({ content, xmlChildren, styles, theme, numbering, rel
|
|
|
207
207
|
...textBox.autoFit !== void 0 ? { autoFit: textBox.autoFit } : {},
|
|
208
208
|
...textBox.textWrap !== void 0 ? { textWrap: textBox.textWrap } : {},
|
|
209
209
|
...textBox.verticalAlign !== void 0 ? { anchor: textBox.verticalAlign } : {},
|
|
210
|
-
...textBox.margins !== void 0 ? { margins: textBox.margins } : {}
|
|
210
|
+
...textBox.margins !== void 0 ? { margins: textBox.margins } : {},
|
|
211
|
+
...textBox.wordArt !== void 0 ? { wordArt: textBox.wordArt } : {}
|
|
211
212
|
}
|
|
212
213
|
};
|
|
213
214
|
if (textBox.id) shape.id = textBox.id;
|
package/dist/docx/parser.js
CHANGED
|
@@ -27,6 +27,7 @@ import { applyThemeFontLang, parseTheme } from "./themeParser.js";
|
|
|
27
27
|
import { normalizeTrackedMoveRanges } from "./trackedMoveRangeNormalization.js";
|
|
28
28
|
import { getMediaMimeType, mediaToDataUrl, unzipDocx } from "./unzip.js";
|
|
29
29
|
import { enforcePackageVmlPreviewBudget } from "./vmlPreview.js";
|
|
30
|
+
import { FOLIO_XML_RESOURCE_LIMITS } from "./xmlResourceLimits.js";
|
|
30
31
|
import { TaggedError } from "better-result";
|
|
31
32
|
//#region src/docx/parser.ts
|
|
32
33
|
/**
|
|
@@ -341,6 +342,18 @@ async function buildMediaMap(raw, rels) {
|
|
|
341
342
|
const normalizedPath = path.replace(/^word\//u, "");
|
|
342
343
|
if (normalizedPath !== path) media.set(normalizedPath, mediaFile);
|
|
343
344
|
}
|
|
345
|
+
for (const [path, xml] of raw.allXml.entries()) {
|
|
346
|
+
if (!/^word\/diagrams\/[^/]+\.xml$/iu.test(path) || new TextEncoder().encode(xml).byteLength > FOLIO_XML_RESOURCE_LIMITS.maxBytes) continue;
|
|
347
|
+
const data = new TextEncoder().encode(xml).buffer;
|
|
348
|
+
const file = {
|
|
349
|
+
path,
|
|
350
|
+
filename: path.split("/").pop() ?? path,
|
|
351
|
+
mimeType: "application/xml",
|
|
352
|
+
data
|
|
353
|
+
};
|
|
354
|
+
media.set(path, file);
|
|
355
|
+
media.set(path.replace(/^word\//u, ""), file);
|
|
356
|
+
}
|
|
344
357
|
return media;
|
|
345
358
|
}
|
|
346
359
|
async function applyMediaResolver(media, resolver) {
|
package/dist/docx/runParser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isValidHexColor } from "../utils/colorResolver.js";
|
|
2
2
|
import { parseHorizontalScalePercent } from "../utils/horizontalScale.js";
|
|
3
|
+
import { parseDiagramPreview } from "./diagramPreview.js";
|
|
3
4
|
import { parseGroupDrawing } from "./groupDrawingParser.js";
|
|
4
5
|
import { parseImage } from "./imageParser.js";
|
|
5
6
|
import { imageRawXmlFingerprint } from "./imageRawXml.js";
|
|
@@ -488,6 +489,13 @@ function parseDrawingContent(element, rels, media) {
|
|
|
488
489
|
rawXml: captureVerbatimXml(element),
|
|
489
490
|
rawImageFingerprint: imageRawXmlFingerprint(groupImage)
|
|
490
491
|
};
|
|
492
|
+
const diagramImage = parseDiagramPreview(element, rels ?? void 0, media ?? void 0);
|
|
493
|
+
if (diagramImage) return {
|
|
494
|
+
type: "drawing",
|
|
495
|
+
image: diagramImage,
|
|
496
|
+
rawXml: captureVerbatimXml(element),
|
|
497
|
+
rawXmlMode: DRAWING_RAW_XML_MODES.PRESERVE_ONLY
|
|
498
|
+
};
|
|
491
499
|
if (shouldPreserveRawShapeDrawing(element)) return {
|
|
492
500
|
type: "drawing",
|
|
493
501
|
image: {
|
|
@@ -364,6 +364,7 @@ function serializeShapeContent(content) {
|
|
|
364
364
|
if (shape.textBody) {
|
|
365
365
|
const tb = shape.textBody;
|
|
366
366
|
const bpAttrs = ["rot=\"0\"", "vert=\"horz\""];
|
|
367
|
+
if (tb.wordArt?.fromWordArt !== void 0) bpAttrs.push(`fromWordArt="${tb.wordArt.fromWordArt ? "1" : "0"}"`);
|
|
367
368
|
if (tb.textWrap) bpAttrs.push(`wrap="${tb.textWrap}"`);
|
|
368
369
|
if (tb.anchor) bpAttrs.push(`anchor="${serializeTextBodyAnchor(tb.anchor)}"`);
|
|
369
370
|
if (tb.anchorCenter) bpAttrs.push("anchorCtr=\"1\"");
|
|
@@ -377,7 +378,8 @@ function serializeShapeContent(content) {
|
|
|
377
378
|
if (tb.autoFit === "shape") autoFitXml = "<a:spAutoFit/>";
|
|
378
379
|
else if (tb.autoFit === "normal") autoFitXml = "<a:normAutofit/>";
|
|
379
380
|
else if (tb.autoFit === "none") autoFitXml = "<a:noAutofit/>";
|
|
380
|
-
const
|
|
381
|
+
const bodyPrChildren = `${tb.wordArt?.preset ? `<a:prstTxWarp prst="${escapeXml(tb.wordArt.preset)}">${tb.wordArt.adjustments && tb.wordArt.adjustments.length > 0 ? `<a:avLst>${tb.wordArt.adjustments.map(({ name, formula }) => `<a:gd name="${escapeXml(name)}" fmla="${escapeXml(formula)}"/>`).join("")}</a:avLst>` : "<a:avLst/>"}</a:prstTxWarp>` : ""}${autoFitXml}`;
|
|
382
|
+
const bodyPrXml = bodyPrChildren ? `<wps:bodyPr ${bpAttrs.join(" ")}>${bodyPrChildren}</wps:bodyPr>` : `<wps:bodyPr ${bpAttrs.join(" ")}/>`;
|
|
381
383
|
if (isTextBox) textBody = [
|
|
382
384
|
"<wps:txbx><w:txbxContent>",
|
|
383
385
|
serializeShapeTextBody(tb.content),
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { emuToPixels } from "../utils/units.js";
|
|
2
2
|
import { parseAnchorPosition, parseAnchorWrap, parseFill, parseOutline, resolveColorValueToHex } from "./drawingUtils.js";
|
|
3
|
-
import { findByFullName, findChildByLocalName, findChildrenByLocalName, getAttribute, getChildElements, parseNumericAttribute } from "./xmlParser.js";
|
|
3
|
+
import { findByFullName, findChildByLocalName, findChildByNamespaceUri, findChildrenByLocalName, findChildrenByNamespaceUri, getAttribute, getChildElements, parseNumericAttribute } from "./xmlParser.js";
|
|
4
4
|
//#region src/docx/textBoxParser.ts
|
|
5
|
+
const DRAWINGML_MAIN_NAMESPACE_URIS = /* @__PURE__ */ new Set(["http://schemas.openxmlformats.org/drawingml/2006/main", "http://purl.oclc.org/ooxml/drawingml/main"]);
|
|
6
|
+
const WORDPROCESSING_SHAPE_NAMESPACE_URIS = /* @__PURE__ */ new Set(["http://schemas.microsoft.com/office/word/2010/wordprocessingShape"]);
|
|
5
7
|
/** Default text box margins in EMUs (0.1 inch) */
|
|
6
8
|
const DEFAULT_MARGIN_EMU = 91440;
|
|
7
9
|
const parseTextBoxVerticalAlign = (value) => {
|
|
@@ -28,6 +30,24 @@ function parseBodyProperties(bodyPr) {
|
|
|
28
30
|
else if (findChildByLocalName(bodyPr, "noAutofit")) result.autoFit = "none";
|
|
29
31
|
const verticalAlign = parseTextBoxVerticalAlign(getAttribute(bodyPr, null, "anchor"));
|
|
30
32
|
if (verticalAlign) result.verticalAlign = verticalAlign;
|
|
33
|
+
const fromWordArt = getAttribute(bodyPr, null, "fromWordArt");
|
|
34
|
+
const warp = findChildByNamespaceUri(bodyPr, DRAWINGML_MAIN_NAMESPACE_URIS, "prstTxWarp");
|
|
35
|
+
if (fromWordArt !== null || warp) {
|
|
36
|
+
const adjustments = warp ? findChildrenByNamespaceUri(findChildByNamespaceUri(warp, DRAWINGML_MAIN_NAMESPACE_URIS, "avLst"), DRAWINGML_MAIN_NAMESPACE_URIS, "gd").map((adjustment) => {
|
|
37
|
+
const name = getAttribute(adjustment, null, "name");
|
|
38
|
+
const formula = getAttribute(adjustment, null, "fmla");
|
|
39
|
+
return name !== null && formula !== null ? {
|
|
40
|
+
name,
|
|
41
|
+
formula
|
|
42
|
+
} : void 0;
|
|
43
|
+
}).filter((adjustment) => adjustment !== void 0) : [];
|
|
44
|
+
const preset = warp ? getAttribute(warp, null, "prst") : null;
|
|
45
|
+
result.wordArt = {
|
|
46
|
+
...fromWordArt !== null ? { fromWordArt: fromWordArt === "1" || fromWordArt === "true" || fromWordArt === "on" } : {},
|
|
47
|
+
...preset !== null ? { preset } : {},
|
|
48
|
+
...adjustments.length > 0 ? { adjustments } : {}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
31
51
|
const lIns = parseNumericAttribute(bodyPr, null, "lIns");
|
|
32
52
|
const rIns = parseNumericAttribute(bodyPr, null, "rIns");
|
|
33
53
|
const tIns = parseNumericAttribute(bodyPr, null, "tIns");
|
|
@@ -119,9 +139,8 @@ function parseTextBox(drawingEl) {
|
|
|
119
139
|
const wsp = findByFullName(graphicData, "wps:wsp");
|
|
120
140
|
if (!wsp) return null;
|
|
121
141
|
if (!findByFullName(wsp, "wps:txbx")) return null;
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const bodyPr = wspChildren.find((el) => el.name === "wps:bodyPr");
|
|
142
|
+
const spPr = getChildElements(wsp).find((el) => el.name === "wps:spPr");
|
|
143
|
+
const bodyPr = findChildByNamespaceUri(wsp, WORDPROCESSING_SHAPE_NAMESPACE_URIS, "bodyPr");
|
|
125
144
|
const extent = findByFullName(container, "wp:extent");
|
|
126
145
|
const size = {
|
|
127
146
|
width: parseNumericAttribute(extent, null, "cx") ?? 0,
|
|
@@ -144,6 +163,7 @@ function parseTextBox(drawingEl) {
|
|
|
144
163
|
if (bodyProps.autoFit) textBox.autoFit = bodyProps.autoFit;
|
|
145
164
|
if (bodyProps.textWrap) textBox.textWrap = bodyProps.textWrap;
|
|
146
165
|
if (bodyProps.verticalAlign) textBox.verticalAlign = bodyProps.verticalAlign;
|
|
166
|
+
if (bodyProps.wordArt) textBox.wordArt = bodyProps.wordArt;
|
|
147
167
|
if (isAnchor) {
|
|
148
168
|
const position = parseAnchorPosition(container);
|
|
149
169
|
if (position) textBox.position = position;
|
|
@@ -170,7 +190,7 @@ function parseTextBoxFromShape(wsp, size, position, wrap) {
|
|
|
170
190
|
if (!findByFullName(wsp, "wps:txbx")) return null;
|
|
171
191
|
const wspChildren = getChildElements(wsp);
|
|
172
192
|
const spPr = wspChildren.find((el) => el.name === "wps:spPr");
|
|
173
|
-
const bodyPr =
|
|
193
|
+
const bodyPr = findChildByNamespaceUri(wsp, WORDPROCESSING_SHAPE_NAMESPACE_URIS, "bodyPr");
|
|
174
194
|
const cNvPr = wspChildren.find((el) => el.name === "wps:cNvPr");
|
|
175
195
|
const id = cNvPr ? getAttribute(cNvPr, null, "id") ?? void 0 : void 0;
|
|
176
196
|
const fill = parseFill(spPr ?? null);
|
package/dist/export-pdf.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildDisplayList } from "./display-list/build/buildDisplayList.js";
|
|
2
|
+
import { displayCommentsFrom } from "./display-list/build/commentAnnotations.js";
|
|
2
3
|
import { installHeadlessMeasureProvider } from "./fonts/headlessMeasure.js";
|
|
3
4
|
import { layoutDocxHeadless } from "./headless-layout.js";
|
|
4
5
|
import { getMeasureProvider, setMeasureProvider } from "./layout-engine/measure/measureProvider.js";
|
|
@@ -73,6 +74,7 @@ const exportWithHeadlessProvider = async (input, options) => {
|
|
|
73
74
|
documentFeatures: laidOut.value.documentFeatures,
|
|
74
75
|
...laidOut.value.furniture,
|
|
75
76
|
embeddedFonts: laidOut.value.embeddedFonts,
|
|
77
|
+
...laidOut.value.document.package.document.comments === void 0 ? {} : { comments: displayCommentsFrom(laidOut.value.document.package.document.comments) },
|
|
76
78
|
...options.metadata === void 0 ? {} : { metadata: options.metadata }
|
|
77
79
|
});
|
|
78
80
|
const written = await writePdf(list, {
|
package/dist/headless-layout.js
CHANGED
|
@@ -272,6 +272,7 @@ const layoutDocxHeadless = async (input, options = {}) => {
|
|
|
272
272
|
finalPageSize,
|
|
273
273
|
finalMargins,
|
|
274
274
|
finalPageNumbering: getPageNumbering(finalSection),
|
|
275
|
+
sectionVerticalAlignments: sections.map(({ properties }) => properties.verticalAlign),
|
|
275
276
|
pageGap: options.pageGap ?? 0,
|
|
276
277
|
titlePage: firstSection?.titlePg === true,
|
|
277
278
|
evenAndOddHeaders: document.package.settings?.evenAndOddHeaders === true,
|
|
@@ -11,6 +11,7 @@ import { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacing
|
|
|
11
11
|
import { assertExhaustiveFlowBlock, findPageIndexContainingPmPos } from "./pmPageIndex.js";
|
|
12
12
|
import { INITIAL_RENDERED_BREAK_STATE, PAGE_ADVANCE, reconcileAfterBlock, reconcileBreakBeforeBlock, recordReflowBoundary } from "./renderedBreakReconciliation.js";
|
|
13
13
|
import { applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, normalizeSectionBreakType, scheduleSectionBreak } from "./section-breaks.js";
|
|
14
|
+
import { applySectionVerticalAlignment } from "./sectionVerticalAlignment.js";
|
|
14
15
|
import { buildTableRowBreakInfo, getRowContinuationSkip, snapRowBreak } from "./tableRowBreak.js";
|
|
15
16
|
import { bandFragmentX, bandTopContentY, isPageFrameRelativeAnchor } from "./textBoxFlow.js";
|
|
16
17
|
import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, resolveTableCellPadding, tableColumnsArePinned } from "./types.js";
|
|
@@ -145,16 +146,15 @@ function applyContextualSpacing(blocks) {
|
|
|
145
146
|
* reservations as page floors so every column participates in the same footnote geometry.
|
|
146
147
|
*/
|
|
147
148
|
function layoutDocument(blocks, measures, options) {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
initialLayout: layout,
|
|
149
|
+
const initialLayout = layoutDocumentPass(blocks, measures, options);
|
|
150
|
+
return applySectionVerticalAlignment(options.footnoteHeightById ? reflowFootnoteColumns({
|
|
151
|
+
initialLayout,
|
|
152
152
|
...options.footnoteReservedHeights ? { initialReserveFloors: options.footnoteReservedHeights } : {},
|
|
153
153
|
runLayout: (reserveFloors) => layoutDocumentPass(blocks, measures, {
|
|
154
154
|
...options,
|
|
155
155
|
footnoteReservedHeights: reserveFloors
|
|
156
156
|
})
|
|
157
|
-
});
|
|
157
|
+
}) : initialLayout, options.sectionVerticalAlignments);
|
|
158
158
|
}
|
|
159
159
|
function layoutDocumentPass(blocks, measures, options) {
|
|
160
160
|
if (blocks.length !== measures.length) panic(`layoutDocument: expected one measure per block (blocks=${blocks.length}, measures=${measures.length})`);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Layout } from "./types.js";
|
|
2
|
+
import { VerticalAlign } from "@stll/docx-core/model";
|
|
3
|
+
//#region src/layout-engine/sectionVerticalAlignment.d.ts
|
|
4
|
+
/** Align each page's body inside the authored section content frame. */
|
|
5
|
+
declare const applySectionVerticalAlignment: (layout: Layout, alignments: readonly (VerticalAlign | undefined)[] | undefined) => Layout;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { applySectionVerticalAlignment };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/layout-engine/sectionVerticalAlignment.ts
|
|
2
|
+
const isFlowFragment = (fragment) => {
|
|
3
|
+
switch (fragment.kind) {
|
|
4
|
+
case "paragraph": return true;
|
|
5
|
+
case "table": return !fragment.isFloating;
|
|
6
|
+
case "image": return !fragment.isAnchored;
|
|
7
|
+
case "textBox": return !fragment.isPositioned;
|
|
8
|
+
default: return false;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
const alignedOffset = (alignment, availableHeight) => {
|
|
12
|
+
switch (alignment) {
|
|
13
|
+
case "center": return availableHeight / 2;
|
|
14
|
+
case "bottom": return availableHeight;
|
|
15
|
+
case "top":
|
|
16
|
+
case "both": return 0;
|
|
17
|
+
default: return 0;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const alignPage = (page, alignment) => {
|
|
21
|
+
if (alignment !== "center" && alignment !== "bottom") return page;
|
|
22
|
+
const flow = page.fragments.filter(isFlowFragment);
|
|
23
|
+
if (flow.length === 0) return page;
|
|
24
|
+
const contentBottom = page.size.h - page.margins.bottom - (page.footnoteReservedHeight ?? 0);
|
|
25
|
+
const occupiedBottom = Math.max(...flow.map((fragment) => fragment.y + fragment.height));
|
|
26
|
+
const offset = alignedOffset(alignment, Math.max(0, contentBottom - occupiedBottom));
|
|
27
|
+
if (offset === 0) return page;
|
|
28
|
+
const flowSet = new Set(flow);
|
|
29
|
+
return {
|
|
30
|
+
...page,
|
|
31
|
+
fragments: page.fragments.map((fragment) => flowSet.has(fragment) ? {
|
|
32
|
+
...fragment,
|
|
33
|
+
y: fragment.y + offset
|
|
34
|
+
} : fragment)
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
/** Align each page's body inside the authored section content frame. */
|
|
38
|
+
const applySectionVerticalAlignment = (layout, alignments) => {
|
|
39
|
+
if (alignments === void 0) return layout;
|
|
40
|
+
let changed = false;
|
|
41
|
+
const pages = layout.pages.map((page) => {
|
|
42
|
+
const aligned = alignPage(page, alignments[page.sectionIndex ?? 0]);
|
|
43
|
+
changed ||= aligned !== page;
|
|
44
|
+
return aligned;
|
|
45
|
+
});
|
|
46
|
+
return changed ? {
|
|
47
|
+
...layout,
|
|
48
|
+
pages
|
|
49
|
+
} : layout;
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
export { applySectionVerticalAlignment };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OutlineStyleAttr } from "../types/documentEnumValues.js";
|
|
2
|
-
import { ImagePosition, ImageWrap, NumberFormat, SdtProperties, SdtType, ShapeTextBody, TableCellFormatting, TableWidthType } from "@stll/docx-core/model";
|
|
2
|
+
import { ImagePosition, ImageWrap, NumberFormat, SdtProperties, SdtType, ShapeTextBody, TableCellFormatting, TableWidthType, VerticalAlign } from "@stll/docx-core/model";
|
|
3
3
|
declare namespace types_d_exports {
|
|
4
4
|
export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListMarkerFormatting, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, RenderedPageBreakRun, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionPageNumbering, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getTableRowLeadingWidth, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, resolveTableCellPadding, tableColumnsArePinned };
|
|
5
5
|
}
|
|
@@ -1252,6 +1252,8 @@ type LayoutOptions = {
|
|
|
1252
1252
|
finalColumns?: ColumnLayout;
|
|
1253
1253
|
/** Body-level final section page-number policy. */
|
|
1254
1254
|
finalPageNumbering?: SectionPageNumbering;
|
|
1255
|
+
/** Vertical body alignment for each section, indexed by section ordinal. */
|
|
1256
|
+
sectionVerticalAlignments?: readonly (VerticalAlign | undefined)[];
|
|
1255
1257
|
/** Column configuration. */
|
|
1256
1258
|
columns?: ColumnLayout;
|
|
1257
1259
|
/** Gap between rendered pages (for UI). */
|
package/dist/pdf/writePdf.js
CHANGED
|
@@ -116,7 +116,7 @@ const buildDestination = ({ pageRefs, pages, pageIndex, yPx }) => {
|
|
|
116
116
|
pdfNull
|
|
117
117
|
]));
|
|
118
118
|
};
|
|
119
|
-
const
|
|
119
|
+
const buildLinkAnnotation = ({ document, link, page, pages, pageRefs }) => {
|
|
120
120
|
const topLeft = displayPointToPdf(page.heightPx, link.rect.xPx, link.rect.yPx);
|
|
121
121
|
const bottomRight = displayPointToPdf(page.heightPx, link.rect.xPx + link.rect.widthPx, link.rect.yPx + link.rect.heightPx);
|
|
122
122
|
let action;
|
|
@@ -160,6 +160,26 @@ const buildAnnotation = ({ document, link, page, pages, pageRefs }) => {
|
|
|
160
160
|
["Dest", destination]
|
|
161
161
|
])));
|
|
162
162
|
};
|
|
163
|
+
const buildCommentAnnotation = ({ document, comment, page }) => {
|
|
164
|
+
const rect = comment.rects.at(-1);
|
|
165
|
+
if (rect === void 0) return Result.err(new WritePdfError({ message: `comment ${String(comment.commentId)} has no anchor rectangle` }));
|
|
166
|
+
const topLeft = displayPointToPdf(page.heightPx, rect.xPx, rect.yPx);
|
|
167
|
+
const bottomRight = displayPointToPdf(page.heightPx, rect.xPx + rect.widthPx, rect.yPx + rect.heightPx);
|
|
168
|
+
return Result.ok(document.add(pdfDict([
|
|
169
|
+
["Type", pdfName("Annot")],
|
|
170
|
+
["Subtype", pdfName("Text")],
|
|
171
|
+
["Rect", pdfNumberArray([
|
|
172
|
+
Math.min(topLeft.x, bottomRight.x),
|
|
173
|
+
Math.min(topLeft.y, bottomRight.y),
|
|
174
|
+
Math.max(topLeft.x, bottomRight.x),
|
|
175
|
+
Math.max(topLeft.y, bottomRight.y)
|
|
176
|
+
])],
|
|
177
|
+
["Contents", pdfTextString(comment.contents)],
|
|
178
|
+
["T", pdfTextString(comment.author)],
|
|
179
|
+
["Name", pdfName("Comment")],
|
|
180
|
+
["NM", pdfAsciiString(`folio-comment-${String(comment.commentId)}`)]
|
|
181
|
+
])));
|
|
182
|
+
};
|
|
163
183
|
const buildOutlineTree = (entries, document) => {
|
|
164
184
|
const roots = [];
|
|
165
185
|
const ancestors = [];
|
|
@@ -255,6 +275,11 @@ const writePdf = async (list, options) => {
|
|
|
255
275
|
}
|
|
256
276
|
const outlineRoots = buildOutlineTree(list.outline, document);
|
|
257
277
|
const outlineRef = outlineRoots.length === 0 ? void 0 : document.allocate();
|
|
278
|
+
const commentLocations = /* @__PURE__ */ new Map();
|
|
279
|
+
for (const [pageIndex, page] of list.pages.entries()) for (const comment of page.comments) commentLocations.set(comment.commentId, {
|
|
280
|
+
pageIndex,
|
|
281
|
+
comment
|
|
282
|
+
});
|
|
258
283
|
for (const [pageIndex, page] of list.pages.entries()) {
|
|
259
284
|
const pageRef = pageRefs[pageIndex];
|
|
260
285
|
if (pageRef === void 0) return Result.err(new WritePdfError({ message: `page ${String(pageIndex)} lost its object` }));
|
|
@@ -266,7 +291,7 @@ const writePdf = async (list, options) => {
|
|
|
266
291
|
const contentRef = document.add(pdfFlateStream([], new TextEncoder().encode(renderContentStream(parts, resources))));
|
|
267
292
|
const annotationRefs = [];
|
|
268
293
|
for (const link of page.links) {
|
|
269
|
-
const annotation =
|
|
294
|
+
const annotation = buildLinkAnnotation({
|
|
270
295
|
document,
|
|
271
296
|
link,
|
|
272
297
|
page,
|
|
@@ -276,6 +301,16 @@ const writePdf = async (list, options) => {
|
|
|
276
301
|
if (annotation.isErr()) return Result.err(annotation.error);
|
|
277
302
|
annotationRefs.push(annotation.value);
|
|
278
303
|
}
|
|
304
|
+
for (const { pageIndex: commentPageIndex, comment } of commentLocations.values()) {
|
|
305
|
+
if (commentPageIndex !== pageIndex) continue;
|
|
306
|
+
const annotation = buildCommentAnnotation({
|
|
307
|
+
document,
|
|
308
|
+
comment,
|
|
309
|
+
page
|
|
310
|
+
});
|
|
311
|
+
if (annotation.isErr()) return Result.err(annotation.error);
|
|
312
|
+
annotationRefs.push(annotation.value);
|
|
313
|
+
}
|
|
279
314
|
document.define(pageRef, pdfDict([
|
|
280
315
|
["Type", pdfName("Page")],
|
|
281
316
|
["Parent", pagesRef],
|
|
@@ -519,6 +519,7 @@ const readTextBoxAttrs = (node) => {
|
|
|
519
519
|
optionalNumber(attrs, "width", "textBox.attrs.width", issues);
|
|
520
520
|
optionalNumber(attrs, "height", "textBox.attrs.height", issues);
|
|
521
521
|
optionalOneOf(attrs, "autoFit", "textBox.attrs.autoFit", issues, TEXT_BOX_AUTO_FIT_VALUES);
|
|
522
|
+
optionalWordArt(attrs, "wordArt", "textBox.attrs.wordArt", issues);
|
|
522
523
|
optionalOneOf(attrs, "textWrap", "textBox.attrs.textWrap", issues, TEXT_BOX_TEXT_WRAP_VALUES);
|
|
523
524
|
optionalString(attrs, "textBoxId", "textBox.attrs.textBoxId", issues);
|
|
524
525
|
optionalString(attrs, "fillColor", "textBox.attrs.fillColor", issues);
|
|
@@ -1547,6 +1548,40 @@ const optionalNestedRecord = (attrs, key, path, issues, validate) => {
|
|
|
1547
1548
|
}
|
|
1548
1549
|
validate(value, path, issues);
|
|
1549
1550
|
};
|
|
1551
|
+
const optionalWordArt = (attrs, key, path, issues) => {
|
|
1552
|
+
const value = attrs[key];
|
|
1553
|
+
if (value === void 0 || value === null) return;
|
|
1554
|
+
if (!isRecord(value)) {
|
|
1555
|
+
issues.push({
|
|
1556
|
+
path,
|
|
1557
|
+
message: "Expected an object."
|
|
1558
|
+
});
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
optionalString(value, "preset", `${path}.preset`, issues);
|
|
1562
|
+
optionalBoolean(value, "fromWordArt", `${path}.fromWordArt`, issues);
|
|
1563
|
+
const adjustments = value["adjustments"];
|
|
1564
|
+
if (adjustments === void 0 || adjustments === null) return;
|
|
1565
|
+
if (!Array.isArray(adjustments)) {
|
|
1566
|
+
issues.push({
|
|
1567
|
+
path: `${path}.adjustments`,
|
|
1568
|
+
message: "Expected an array."
|
|
1569
|
+
});
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
for (const [index, adjustment] of adjustments.entries()) {
|
|
1573
|
+
const adjustmentPath = `${path}.adjustments[${index}]`;
|
|
1574
|
+
if (!isRecord(adjustment)) {
|
|
1575
|
+
issues.push({
|
|
1576
|
+
path: adjustmentPath,
|
|
1577
|
+
message: "Expected an object."
|
|
1578
|
+
});
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
optionalString(adjustment, "name", `${adjustmentPath}.name`, issues);
|
|
1582
|
+
optionalString(adjustment, "formula", `${adjustmentPath}.formula`, issues);
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1550
1585
|
const PARAGRAPH_FORMATTING_BOOLEAN_KEYS = [
|
|
1551
1586
|
"bidi",
|
|
1552
1587
|
"kinsoku",
|
|
@@ -2902,6 +2902,7 @@ function convertPMTextBox(node, styleResolver = null) {
|
|
|
2902
2902
|
},
|
|
2903
2903
|
textBody: {
|
|
2904
2904
|
content: textBodyContent.type === "source-empty" ? [] : textBodyContent.content,
|
|
2905
|
+
...attrs.wordArt !== void 0 ? { wordArt: attrs.wordArt } : {},
|
|
2905
2906
|
...attrs.autoFit !== void 0 ? { autoFit: attrs.autoFit } : {},
|
|
2906
2907
|
...attrs.textWrap !== void 0 ? { textWrap: attrs.textWrap } : {},
|
|
2907
2908
|
...verticalAlign !== void 0 ? { anchor: verticalAlign } : {},
|
|
@@ -2111,6 +2111,7 @@ function textBoxFromShape(shape, textBody) {
|
|
|
2111
2111
|
if (textBody.autoFit) textBox.autoFit = textBody.autoFit;
|
|
2112
2112
|
if (textBody.textWrap) textBox.textWrap = textBody.textWrap;
|
|
2113
2113
|
if (textBody.anchor) textBox.verticalAlign = textBody.anchor;
|
|
2114
|
+
if (textBody.wordArt) textBox.wordArt = textBody.wordArt;
|
|
2114
2115
|
return textBox;
|
|
2115
2116
|
}
|
|
2116
2117
|
/**
|
|
@@ -2194,6 +2195,7 @@ function convertTextBox(textBox, styleResolver, options) {
|
|
|
2194
2195
|
width: widthPx,
|
|
2195
2196
|
height: heightPx,
|
|
2196
2197
|
autoFit: textBox.autoFit,
|
|
2198
|
+
wordArt: textBox.wordArt,
|
|
2197
2199
|
textWrap: textBox.textWrap,
|
|
2198
2200
|
verticalAlign: textBox.verticalAlign,
|
|
2199
2201
|
textBoxId: textBox.id,
|
|
@@ -2,6 +2,7 @@ import { TextBoxAttrs as TextBoxAttrs$1 } from "../../schema/nodes.js";
|
|
|
2
2
|
import { NodeExtension } from "../types.js";
|
|
3
3
|
//#region src/prosemirror/extensions/nodes/TextBoxExtension.d.ts
|
|
4
4
|
type TextBoxAttrs = TextBoxAttrs$1;
|
|
5
|
+
declare function parseTextBoxWordArt(raw: string): TextBoxAttrs["wordArt"];
|
|
5
6
|
declare const TextBoxExtension: (options?: Partial<Record<string, unknown>> | undefined) => NodeExtension;
|
|
6
7
|
//#endregion
|
|
7
|
-
export { TextBoxAttrs, TextBoxExtension };
|
|
8
|
+
export { TextBoxAttrs, TextBoxExtension, parseTextBoxWordArt };
|
|
@@ -31,6 +31,41 @@ function parseTextBoxTextWrap(raw) {
|
|
|
31
31
|
function parseTextBoxVerticalAlign(raw) {
|
|
32
32
|
return normalizeShapeTextAnchor(raw);
|
|
33
33
|
}
|
|
34
|
+
function parseTextBoxWordArt(raw) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
if (typeof parsed !== "object" || parsed === null) return;
|
|
38
|
+
const wordArt = {};
|
|
39
|
+
const fromWordArt = Reflect.get(parsed, "fromWordArt");
|
|
40
|
+
if (fromWordArt !== void 0) {
|
|
41
|
+
if (typeof fromWordArt !== "boolean") return void 0;
|
|
42
|
+
wordArt.fromWordArt = fromWordArt;
|
|
43
|
+
}
|
|
44
|
+
const preset = Reflect.get(parsed, "preset");
|
|
45
|
+
if (preset !== void 0) {
|
|
46
|
+
if (typeof preset !== "string") return void 0;
|
|
47
|
+
wordArt.preset = preset;
|
|
48
|
+
}
|
|
49
|
+
const adjustments = Reflect.get(parsed, "adjustments");
|
|
50
|
+
if (adjustments !== void 0) {
|
|
51
|
+
if (!Array.isArray(adjustments)) return void 0;
|
|
52
|
+
const validAdjustments = [];
|
|
53
|
+
for (const adjustment of adjustments) {
|
|
54
|
+
if (typeof adjustment !== "object" || adjustment === null) continue;
|
|
55
|
+
const name = Reflect.get(adjustment, "name");
|
|
56
|
+
const formula = Reflect.get(adjustment, "formula");
|
|
57
|
+
if (typeof name === "string" && typeof formula === "string") validAdjustments.push({
|
|
58
|
+
name,
|
|
59
|
+
formula
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (validAdjustments.length > 0) wordArt.adjustments = validAdjustments;
|
|
63
|
+
}
|
|
64
|
+
return wordArt.fromWordArt !== void 0 || wordArt.preset !== void 0 || wordArt.adjustments !== void 0 ? wordArt : void 0;
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
34
69
|
const TextBoxExtension = createNodeExtension({
|
|
35
70
|
name: "textBox",
|
|
36
71
|
schemaNodeName: "textBox",
|
|
@@ -43,6 +78,7 @@ const TextBoxExtension = createNodeExtension({
|
|
|
43
78
|
width: { default: 200 },
|
|
44
79
|
height: { default: null },
|
|
45
80
|
autoFit: { default: null },
|
|
81
|
+
wordArt: { default: null },
|
|
46
82
|
textWrap: { default: null },
|
|
47
83
|
textBoxId: { default: null },
|
|
48
84
|
fillColor: { default: null },
|
|
@@ -86,6 +122,7 @@ const TextBoxExtension = createNodeExtension({
|
|
|
86
122
|
...d["width"] ? { width: Number(d["width"]) } : {},
|
|
87
123
|
...d["height"] ? { height: Number(d["height"]) } : {},
|
|
88
124
|
...autoFit ? { autoFit } : {},
|
|
125
|
+
...d["wordArt"] ? { wordArt: parseTextBoxWordArt(d["wordArt"]) } : {},
|
|
89
126
|
...textWrap ? { textWrap } : {},
|
|
90
127
|
...d["textboxId"] ? { textBoxId: d["textboxId"] } : {},
|
|
91
128
|
...d["fillColor"] ? { fillColor: d["fillColor"] } : {},
|
|
@@ -113,6 +150,7 @@ const TextBoxExtension = createNodeExtension({
|
|
|
113
150
|
toDOM(node) {
|
|
114
151
|
const attrs = expectTextBoxAttrs(node);
|
|
115
152
|
const domAttrs = { class: "docx-textbox" };
|
|
153
|
+
if (attrs.wordArt) domAttrs["data-word-art"] = JSON.stringify(attrs.wordArt);
|
|
116
154
|
if (attrs.width) domAttrs["data-width"] = String(attrs.width);
|
|
117
155
|
if (attrs.height) domAttrs["data-height"] = String(attrs.height);
|
|
118
156
|
if (attrs.autoFit) domAttrs["data-auto-fit"] = attrs.autoFit;
|
|
@@ -184,4 +222,4 @@ const TextBoxExtension = createNodeExtension({
|
|
|
184
222
|
}
|
|
185
223
|
});
|
|
186
224
|
//#endregion
|
|
187
|
-
export { TextBoxExtension };
|
|
225
|
+
export { TextBoxExtension, parseTextBoxWordArt };
|
|
@@ -502,6 +502,8 @@ type TextBoxAttrs = {
|
|
|
502
502
|
height?: number;
|
|
503
503
|
/** Text fitting behavior */
|
|
504
504
|
autoFit?: document_d_exports.ShapeTextBody["autoFit"];
|
|
505
|
+
/** Authored DrawingML WordArt metadata. */
|
|
506
|
+
wordArt?: document_d_exports.ShapeTextBody["wordArt"];
|
|
505
507
|
/** Horizontal text wrapping inside the box */
|
|
506
508
|
textWrap?: document_d_exports.ShapeTextBody["textWrap"];
|
|
507
509
|
/** Unique identifier */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"perf": "bun scripts/profile-editor.ts"
|
|
122
122
|
},
|
|
123
123
|
"dependencies": {
|
|
124
|
-
"@stll/docx-core": "^0.20.
|
|
124
|
+
"@stll/docx-core": "^0.20.1",
|
|
125
125
|
"@stll/docx-utils": "^0.1.0",
|
|
126
126
|
"@stll/template-conditions": ">=0.4.0 <1.0.0",
|
|
127
127
|
"better-result": "3.0.1",
|