@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.
Files changed (30) hide show
  1. package/dist/controller/layoutPipeline.js +4 -0
  2. package/dist/display-list/build/buildContext.d.ts +3 -1
  3. package/dist/display-list/build/buildDisplayList.d.ts +4 -1
  4. package/dist/display-list/build/buildDisplayList.js +38 -4
  5. package/dist/display-list/build/commentAnnotations.d.ts +10 -0
  6. package/dist/display-list/build/commentAnnotations.js +17 -0
  7. package/dist/display-list/build/paragraphPrimitives.js +39 -1
  8. package/dist/display-list/primitives.d.ts +1 -1
  9. package/dist/display-list/types.d.ts +16 -1
  10. package/dist/docx/diagramPreview.d.ts +7 -0
  11. package/dist/docx/diagramPreview.js +191 -0
  12. package/dist/docx/paragraphTextBoxEnrichment.js +2 -1
  13. package/dist/docx/parser.js +13 -0
  14. package/dist/docx/runParser.js +8 -0
  15. package/dist/docx/serializer/runSerializer.js +3 -1
  16. package/dist/docx/textBoxParser.js +25 -5
  17. package/dist/export-pdf.js +2 -0
  18. package/dist/headless-layout.js +1 -0
  19. package/dist/layout-engine/index.js +5 -5
  20. package/dist/layout-engine/sectionVerticalAlignment.d.ts +7 -0
  21. package/dist/layout-engine/sectionVerticalAlignment.js +52 -0
  22. package/dist/layout-engine/types.d.ts +3 -1
  23. package/dist/pdf/writePdf.js +37 -2
  24. package/dist/prosemirror/attrs/index.js +35 -0
  25. package/dist/prosemirror/conversion/fromProseDoc.js +1 -0
  26. package/dist/prosemirror/conversion/toProseDoc.js +2 -0
  27. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -1
  28. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +39 -1
  29. package/dist/prosemirror/schema/nodes.d.ts +2 -0
  30. 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 (background && glyphs.widthPx > 0) {
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: ("glyphRun" | "rect" | "line" | "image" | "clipGroup" | "rotateGroup" | "opacityGroup")[];
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;
@@ -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) {
@@ -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 bodyPrXml = autoFitXml ? `<wps:bodyPr ${bpAttrs.join(" ")}>${autoFitXml}</wps:bodyPr>` : `<wps:bodyPr ${bpAttrs.join(" ")}/>`;
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 wspChildren = getChildElements(wsp);
123
- const spPr = wspChildren.find((el) => el.name === "wps:spPr");
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 = wspChildren.find((el) => el.name === "wps: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);
@@ -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, {
@@ -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 layout = layoutDocumentPass(blocks, measures, options);
149
- if (!options.footnoteHeightById) return layout;
150
- return reflowFootnoteColumns({
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). */
@@ -116,7 +116,7 @@ const buildDestination = ({ pageRefs, pages, pageIndex, yPx }) => {
116
116
  pdfNull
117
117
  ]));
118
118
  };
119
- const buildAnnotation = ({ document, link, page, pages, pageRefs }) => {
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 = buildAnnotation({
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.38.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.0",
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",