reamkit 1.3.0 → 1.4.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/README.md CHANGED
@@ -34,6 +34,7 @@ const doc = Ream.parse(bytes); // docx or xlsx — sniffed
34
34
  const pdf = await doc.convert('pdf'); // async — fetches a font if needed
35
35
  const svg = await doc.convert('svg'); // same parse, different target
36
36
  const html = await doc.convert('html'); // flowed HTML — needs no fonts at all
37
+ const docx = await doc.convert('docx'); // write WordprocessingML back out
37
38
 
38
39
  // Hand the bytes to the browser: preview, download, upload, …
39
40
  const url = URL.createObjectURL(new Blob([pdf], { type: 'application/pdf' }));
@@ -150,10 +151,11 @@ automatically from the document's `docProps/core.xml`), `attachments`
150
151
 
151
152
  ### Lower-level APIs
152
153
 
153
- - `docxReader` / `xlsxReader`, `svgWriter`, `htmlWriter` — the `@experimental` reader/writer
154
- interfaces of the interlayer, for building custom pipelines (and keeping
155
- unused formats out of your bundle); `layoutStyledDocument` produces the
156
- frozen page model (`PageItem` pages in a top-left `Pt` frame) they consume.
154
+ - `docxReader` / `xlsxReader`, `svgWriter`, `htmlWriter`, `docxWriter` — the `@experimental`
155
+ reader/writer interfaces of the interlayer, for building custom pipelines (and
156
+ keeping unused formats out of your bundle); `layoutStyledDocument` produces the
157
+ frozen page model (`PageItem` pages in a top-left `Pt` frame) the page-based
158
+ writers consume (`docxWriter` works from the flow model, before layout).
157
159
  - `renderStyledPdf` drives the layout engine directly; the typed document
158
160
  model is on the `reamkit/document-model` subpath.
159
161
 
@@ -170,7 +172,8 @@ Liang hyphenation, OpenType ligatures/kerning + Arabic cursive joining,
170
172
  BiDi (UAX #9), hyperlinks (PDF link annotations + HTML anchors,
171
173
  scheme-allowlisted), tagged PDF, PDF/A-1/2/3 (a/b/u), PDF/UA-1, AES-256
172
174
  encryption, digital signatures (PKCS#7/ECDSA/PAdES/RFC 3161), SVG page
173
- preview, flowed HTML export. Reads OOXML Transitional and Strict.
175
+ preview, flowed HTML export, and **docx output** (write WordprocessingML back
176
+ out, incl. docx → docx). Reads OOXML Transitional and Strict.
174
177
 
175
178
  See `handoff.md` for the full feature matrix and known limitations.
176
179
 
@@ -5,8 +5,8 @@ import { FontProvider } from '../fonts/provider.js';
5
5
  import { Loss, LossReport } from '../ir/index.js';
6
6
  import { FontBytesByVariant } from '../font/index.js';
7
7
  export interface ConvertOptions extends ConvertDocxOptions {
8
- /** Target: 'pdf' (default), 'svg' (page-stack preview) or 'html' (flowed). */
9
- readonly to?: 'pdf' | 'svg' | 'html';
8
+ /** Target: 'pdf' (default), 'svg' (page-stack preview), 'html' or 'docx' (flowed). */
9
+ readonly to?: 'pdf' | 'svg' | 'html' | 'docx';
10
10
  /**
11
11
  * Strict mode (handoff v1 §5): throw ConversionLossError on the first
12
12
  * recorded loss instead of returning it in the report.
@@ -6,6 +6,7 @@ import { docxReader } from "../../word/docx-reader.js";
6
6
  import { xlsxReader } from "../../excel/xlsx-reader.js";
7
7
  import { chainProviders } from "../fonts/provider.js";
8
8
  import { flowRenderOptions } from "./project.js";
9
+ import { writeDocx } from "../../word/docx-writer.js";
9
10
  import { writeHtml } from "../../html/html-writer.js";
10
11
  import { writeSvg } from "../../svg/svg-writer.js";
11
12
  import { convertDocxToPdf } from "../../word/docx-to-pdf.js";
@@ -20,6 +21,17 @@ function createConverter(opts = {}) {
20
21
  const reader = detect(bytes);
21
22
  if (!reader) throw new Error("Unrecognized input format (no registered reader sniffs these bytes)");
22
23
  const losses = [];
24
+ if (to === "docx") {
25
+ const { doc: flow, losses: readLosses } = reader.read(bytes);
26
+ losses.push(...readLosses);
27
+ const out = writeDocx(flow);
28
+ losses.push(...out.losses);
29
+ if (strict && losses.length > 0) throw new ConversionLossError(losses[0]);
30
+ return {
31
+ bytes: out.bytes,
32
+ losses
33
+ };
34
+ }
23
35
  if (to === "html") {
24
36
  const { doc: flow, losses: readLosses } = reader.read(bytes);
25
37
  losses.push(...readLosses);
@@ -6,7 +6,7 @@ import { Loss } from '../ir/index.js';
6
6
  import { DocumentReader } from '../ir/adapters.js';
7
7
  import { FlowDoc } from '../ir/flow.js';
8
8
  import { SignatureOptions, StyledRenderOptions } from '../../pdf/index.js';
9
- export type ReamTarget = 'pdf' | 'svg' | 'html';
9
+ export type ReamTarget = 'pdf' | 'svg' | 'html' | 'docx';
10
10
  export interface ReamParseOptions {
11
11
  readonly readers?: ReadonlyArray<DocumentReader>;
12
12
  }
@@ -6,6 +6,7 @@ import { signPdf } from "../../pdf/signature.js";
6
6
  import { renderStyledPdf, renderStyledPdfEncrypted } from "../../pdf/styled-page-renderer.js";
7
7
  import "../../pdf/index.js";
8
8
  import { flowRenderOptions } from "./project.js";
9
+ import { writeDocx } from "../../word/docx-writer.js";
9
10
  import { writeHtml } from "../../html/html-writer.js";
10
11
  import { writeSvg } from "../../svg/svg-writer.js";
11
12
  import { resolveDocxAutoFonts } from "../../word/docx-to-pdf.js";
@@ -46,6 +47,15 @@ var Ream = class Ream {
46
47
  losses
47
48
  };
48
49
  }
50
+ if (to === "docx") {
51
+ const docx = writeDocx(this.flow);
52
+ losses.push(...docx.losses);
53
+ this.enforceStrict(options, losses);
54
+ return {
55
+ bytes: docx.bytes,
56
+ losses
57
+ };
58
+ }
49
59
  const { fonts, registriesByFamily } = await this.resolveFonts(options, losses);
50
60
  const registry = FontRegistry.fromBytes(fonts);
51
61
  if (to === "svg") {
@@ -4,3 +4,5 @@ export { REL_OFFICE_DOCUMENT, REL_CORE_PROPERTIES, REL_EXTENDED_PROPERTIES, } fr
4
4
  export { parseCoreProperties } from './core-properties.js';
5
5
  export type { CoreProperties } from './core-properties.js';
6
6
  export { isOoxmlRel } from './relationship-types.js';
7
+ export { buildOpcPackage, relsPathFor } from './opc-writer.js';
8
+ export type { OpcPart, OpcPartRelationships, OpcWriteOptions } from './opc-writer.js';
@@ -1,3 +1,4 @@
1
1
  import "./relationship-types.js";
2
2
  import "./package.js";
3
3
  import "./core-properties.js";
4
+ import "./opc-writer.js";
@@ -0,0 +1,28 @@
1
+ import { Relationship } from './relationships.js';
2
+ export interface OpcPart {
3
+ /** Package path, e.g. 'word/document.xml'. No leading slash. */
4
+ readonly path: string;
5
+ readonly data: Uint8Array;
6
+ /** Content type for the [Content_Types].xml Override (XML parts). */
7
+ readonly contentType: string;
8
+ }
9
+ export interface OpcPartRelationships {
10
+ /** The owning part, e.g. 'word/document.xml' (or '' for the package root). */
11
+ readonly sourcePart: string;
12
+ readonly relationships: ReadonlyArray<Relationship>;
13
+ }
14
+ export interface OpcWriteOptions {
15
+ readonly parts: ReadonlyArray<OpcPart>;
16
+ /** Package-level relationships (_rels/.rels). */
17
+ readonly rootRelationships: ReadonlyArray<Relationship>;
18
+ /** Per-part relationships (word/_rels/<part>.rels). */
19
+ readonly partRelationships?: ReadonlyArray<OpcPartRelationships>;
20
+ /**
21
+ * Default content types by lowercase extension, for parts not given an
22
+ * Override (typically binaries: png, jpeg…). `rels` and `xml` are always
23
+ * present and need not be repeated.
24
+ */
25
+ readonly defaultsByExtension?: Readonly<Record<string, string>>;
26
+ }
27
+ export declare function buildOpcPackage(options: OpcWriteOptions): Uint8Array;
28
+ export declare function relsPathFor(partPath: string): string;
@@ -0,0 +1,60 @@
1
+ import { zipSync } from "fflate";
2
+ //#region src/core/opc/opc-writer.ts
3
+ var encoder = new TextEncoder();
4
+ var RELS_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
5
+ var CT_NS = "http://schemas.openxmlformats.org/package/2006/content-types";
6
+ var FIXED_MTIME = Date.UTC(1980, 0, 1, 0, 0, 0);
7
+ function buildOpcPackage(options) {
8
+ const entries = {};
9
+ const add = (path, data) => {
10
+ entries[path] = [data, { mtime: FIXED_MTIME }];
11
+ };
12
+ add("[Content_Types].xml", encoder.encode(contentTypesXml(options)));
13
+ add("_rels/.rels", encoder.encode(relationshipsXml(options.rootRelationships)));
14
+ for (const part of options.parts) add(part.path, part.data);
15
+ for (const pr of options.partRelationships ?? []) {
16
+ if (pr.relationships.length === 0) continue;
17
+ add(relsPathFor(pr.sourcePart), encoder.encode(relationshipsXml(pr.relationships)));
18
+ }
19
+ return zipSync(entries);
20
+ }
21
+ function relsPathFor(partPath) {
22
+ const slash = partPath.lastIndexOf("/");
23
+ const dir = slash >= 0 ? partPath.slice(0, slash) : "";
24
+ const name = slash >= 0 ? partPath.slice(slash + 1) : partPath;
25
+ return dir ? `${dir}/_rels/${name}.rels` : `_rels/${name}.rels`;
26
+ }
27
+ function contentTypesXml(options) {
28
+ const defaults = {
29
+ rels: "application/vnd.openxmlformats-package.relationships+xml",
30
+ xml: "application/xml",
31
+ ...options.defaultsByExtension ?? {}
32
+ };
33
+ const lines = ["<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>", `<Types xmlns="${CT_NS}">`];
34
+ for (const ext of Object.keys(defaults).sort()) lines.push(`<Default Extension="${ext}" ContentType="${escapeAttr(defaults[ext])}"/>`);
35
+ for (const part of options.parts) {
36
+ const ext = extensionOf(part.path);
37
+ if (ext !== void 0 && defaults[ext] === part.contentType) continue;
38
+ lines.push(`<Override PartName="/${escapeAttr(part.path)}" ContentType="${escapeAttr(part.contentType)}"/>`);
39
+ }
40
+ lines.push("</Types>");
41
+ return lines.join("");
42
+ }
43
+ function relationshipsXml(rels) {
44
+ const lines = ["<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>", `<Relationships xmlns="${RELS_NS}">`];
45
+ for (const r of rels) {
46
+ const mode = r.targetMode === "External" ? " TargetMode=\"External\"" : "";
47
+ lines.push(`<Relationship Id="${escapeAttr(r.id)}" Type="${escapeAttr(r.type)}" Target="${escapeAttr(r.target)}"${mode}/>`);
48
+ }
49
+ lines.push("</Relationships>");
50
+ return lines.join("");
51
+ }
52
+ function extensionOf(path) {
53
+ const dot = path.lastIndexOf(".");
54
+ return dot >= 0 ? path.slice(dot + 1).toLowerCase() : void 0;
55
+ }
56
+ function escapeAttr(s) {
57
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
58
+ }
59
+ //#endregion
60
+ export { buildOpcPackage, relsPathFor };
@@ -25,5 +25,6 @@ export type { FontProvider, FontRequest, FontAnswer } from './core/fonts/provide
25
25
  export { svgWriter, writeSvg } from './svg/svg-writer.js';
26
26
  export type { SvgWriteOptions } from './svg/svg-writer.js';
27
27
  export { htmlWriter, writeHtml } from './html/html-writer.js';
28
+ export { docxWriter, writeDocx } from './word/docx-writer.js';
28
29
  export { layoutStyledDocument } from './layout/styled-layout.js';
29
30
  export type { LaidOutDocument } from './layout/page-doc.js';
package/dist/esm/index.js CHANGED
@@ -15,8 +15,9 @@ import { createLanguageHyphenator, getHyphenator } from "./core/hyphenation/inde
15
15
  import { docxReader, readDocx } from "./word/docx-reader.js";
16
16
  import { readXlsx, xlsxReader } from "./excel/xlsx-reader.js";
17
17
  import { NO_FONT, callerFontProvider, chainProviders, embeddedDocFontProvider, isEmbeddingRestricted, localFontProvider, readOs2FsType, remoteFontProvider } from "./core/fonts/provider.js";
18
+ import { docxWriter, writeDocx } from "./word/docx-writer.js";
18
19
  import { htmlWriter, writeHtml } from "./html/html-writer.js";
19
20
  import { svgWriter, writeSvg } from "./svg/svg-writer.js";
20
21
  import { createConverter } from "./core/converter/facade.js";
21
22
  import { Ream } from "./core/converter/ream.js";
22
- export { ConversionLossError, FEATURES, FontRegistry, NO_FONT, Ream, ResourceStore, callerFontProvider, chainProviders, createConverter, createHyphenator, createLanguageHyphenator, docxReader, eighthPtToPt, embeddedDocFontProvider, emuToPt, fetchFontSet, formatLoss, getHyphenator, halfPtToPt, htmlWriter, inchToPt, isEmbeddingRestricted, layoutStyledDocument, localFontProvider, mmToPt, parseTtf, pt, pxToPt, readDocx, readOs2FsType, readXlsx, remoteFontProvider, renderStyledPdf, resolveFamilyKey, signPdf, splitPatternBundle, subsetTtf, svgWriter, twipsToPt, writeHtml, writeSvg, xlsxReader };
23
+ export { ConversionLossError, FEATURES, FontRegistry, NO_FONT, Ream, ResourceStore, callerFontProvider, chainProviders, createConverter, createHyphenator, createLanguageHyphenator, docxReader, docxWriter, eighthPtToPt, embeddedDocFontProvider, emuToPt, fetchFontSet, formatLoss, getHyphenator, halfPtToPt, htmlWriter, inchToPt, isEmbeddingRestricted, layoutStyledDocument, localFontProvider, mmToPt, parseTtf, pt, pxToPt, readDocx, readOs2FsType, readXlsx, remoteFontProvider, renderStyledPdf, resolveFamilyKey, signPdf, splitPatternBundle, subsetTtf, svgWriter, twipsToPt, writeDocx, writeHtml, writeSvg, xlsxReader };
@@ -1,8 +1,8 @@
1
1
  import { twipsToPt } from "../core/ir/units.js";
2
- import { poAttr, poChildren, poFindByPath, poIntAttr, poIs, poText } from "../core/po-helpers.js";
2
+ import { poAttr, poChildren, poFindByPath, poFindDescendant, poIntAttr, poIs, poText } from "../core/po-helpers.js";
3
3
  import { defaultColorResolver } from "../core/drawingml/colors.js";
4
4
  import { parseOMath } from "./omml-parser.js";
5
- import { expandMcChildren, parseDrawing } from "./drawing-parser.js";
5
+ import { expandMcChildren, parseDrawing, parseVmlPicture } from "./drawing-parser.js";
6
6
  import { parseRunProperties } from "./run-properties.js";
7
7
  import { parseParagraphProperties } from "./paragraph-properties.js";
8
8
  import { poElementToFlat } from "./po-to-flat.js";
@@ -178,20 +178,44 @@ function parseBodyElements(children, ctx = DEFAULT_PARSE_CONTEXT) {
178
178
  }
179
179
  return out;
180
180
  }
181
- function tryExtractDrawingFromParagraph(p, ctx) {
182
- let drawing;
183
- let hasText = false;
184
- for (const child of poChildren(p)) {
185
- if (!poIs(child, "w:r")) continue;
186
- for (const runChild of expandMcChildren(poChildren(child))) if (poIs(runChild, "w:drawing")) {
187
- if (!drawing) drawing = runChild;
188
- } else if (poIs(runChild, "w:t") && poText(runChild).length > 0) hasText = true;
189
- else if (poIs(runChild, "w:tab") || poIs(runChild, "w:br") || poIs(runChild, "w:noBreakHyphen")) hasText = true;
181
+ var COLLAPSE_TRANSPARENT_TAGS = new Set([
182
+ "w:ins",
183
+ "w:moveTo",
184
+ "w:sdt",
185
+ "w:sdtContent",
186
+ "w:smartTag"
187
+ ]);
188
+ function scanForLoneDrawing(container, acc) {
189
+ for (const child of expandMcChildren(poChildren(container))) {
190
+ if (poIs(child, "w:pPr")) continue;
191
+ if (poIs(child, "w:r")) {
192
+ for (const rc of expandMcChildren(poChildren(child))) if (poIs(rc, "w:drawing")) {
193
+ if (!acc.drawing) acc.drawing = rc;
194
+ } else if (poIs(rc, "w:pict") || poIs(rc, "w:object")) {
195
+ if (!acc.vml && poFindDescendant(rc, "v:imagedata") !== void 0) acc.vml = rc;
196
+ } else if (poIs(rc, "w:t") && poText(rc).length > 0) acc.hasOther = true;
197
+ else if (poIs(rc, "w:tab") || poIs(rc, "w:br") || poIs(rc, "w:noBreakHyphen")) acc.hasOther = true;
198
+ continue;
199
+ }
200
+ if (poIs(child, "m:oMath") || poIs(child, "m:oMathPara")) {
201
+ acc.hasOther = true;
202
+ continue;
203
+ }
204
+ const tag = elementTag(child);
205
+ if (tag && COLLAPSE_TRANSPARENT_TAGS.has(tag)) scanForLoneDrawing(child, acc);
206
+ else if (tag && RUN_CONTAINER_TAGS.has(tag)) acc.hasOther = true;
190
207
  }
191
- if (!drawing || hasText) return null;
208
+ }
209
+ function tryExtractDrawingFromParagraph(p, ctx) {
210
+ const scan = { hasOther: false };
211
+ scanForLoneDrawing(p, scan);
212
+ const { drawing, vml } = scan;
213
+ if (scan.hasOther || !drawing && !vml) return null;
214
+ const fromVml = drawing === void 0;
192
215
  const parseBody = (children) => parseBodyElements(children, ctx);
193
- const content = parseDrawing(drawing, ctx.resolveColor, parseBody);
216
+ const content = fromVml ? parseVmlPicture(vml) : parseDrawing(drawing, ctx.resolveColor, parseBody);
194
217
  if (!content) return null;
218
+ if (fromVml && content.kind === "image" && ctx.resolveImage?.(content.imageId) === void 0) return null;
195
219
  const pPrNode = poChildren(p).find((c) => poIs(c, "w:pPr"));
196
220
  const paragraphProperties = pPrNode ? parseParagraphProperties(poElementToFlat(pPrNode)) : {};
197
221
  if (content.kind === "image") {
@@ -435,6 +459,16 @@ function parseRun(r, ctx) {
435
459
  height: content.height
436
460
  };
437
461
  }
462
+ } else if (poIs(child, "w:pict") || poIs(child, "w:object")) {
463
+ const content = parseVmlPicture(child);
464
+ if (content && content.kind === "image") {
465
+ const resource = ctx.resolveImage?.(content.imageId);
466
+ if (resource) inlineImage = {
467
+ resource,
468
+ width: content.width,
469
+ height: content.height
470
+ };
471
+ }
438
472
  }
439
473
  }
440
474
  return {
@@ -0,0 +1,4 @@
1
+ import { DocumentWriter, WriteResult } from '../core/ir/adapters.js';
2
+ import { FlowDoc } from '../core/ir/flow.js';
3
+ export declare function writeDocx(flow: FlowDoc): WriteResult;
4
+ export declare const docxWriter: DocumentWriter<FlowDoc>;
@@ -0,0 +1,602 @@
1
+ import { FEATURES } from "../core/ir/features.js";
2
+ import { detectImageFormat } from "../core/images.js";
3
+ import { EMPTY_STYLE_SHEET, resolveParagraphProperties, resolveRunProperties } from "../core/style-cascade/resolver.js";
4
+ import "../core/style-cascade/index.js";
5
+ import { buildOpcPackage } from "../core/opc/opc-writer.js";
6
+ import "../core/opc/index.js";
7
+ //#region src/word/docx-writer.ts
8
+ var encoder = new TextEncoder();
9
+ var DEFAULT_RUN = resolveRunProperties({}, {}, EMPTY_STYLE_SHEET);
10
+ var DEFAULT_PARA = resolveParagraphProperties({}, EMPTY_STYLE_SHEET);
11
+ var DOC_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml";
12
+ var NUMBERING_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
13
+ var REL_OFFICE_DOCUMENT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
14
+ var REL_NUMBERING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
15
+ var REL_HYPERLINK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
16
+ var REL_IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
17
+ var NUMBERING_PART = "word/numbering.xml";
18
+ var EMU_PER_PT = 12700;
19
+ var RASTER_MEDIA = {
20
+ png: {
21
+ ext: "png",
22
+ contentType: "image/png"
23
+ },
24
+ jpeg: {
25
+ ext: "jpeg",
26
+ contentType: "image/jpeg"
27
+ },
28
+ jpeg2000: {
29
+ ext: "jp2",
30
+ contentType: "image/jp2"
31
+ }
32
+ };
33
+ function mediaInfo(bytes) {
34
+ const raster = detectImageFormat(bytes);
35
+ if (raster) return RASTER_MEDIA[raster];
36
+ const b = (i) => bytes[i] ?? -1;
37
+ if (b(0) === 71 && b(1) === 73 && b(2) === 70 && b(3) === 56) return {
38
+ ext: "gif",
39
+ contentType: "image/gif"
40
+ };
41
+ if (b(0) === 66 && b(1) === 77) return {
42
+ ext: "bmp",
43
+ contentType: "image/bmp"
44
+ };
45
+ if (b(0) === 73 && b(1) === 73 && b(2) === 42 || b(0) === 77 && b(1) === 77) return {
46
+ ext: "tiff",
47
+ contentType: "image/tiff"
48
+ };
49
+ if (b(0) === 1 && b(1) === 0 && b(2) === 0 && b(3) === 0 && b(40) === 32 && b(41) === 69 && b(42) === 77 && b(43) === 70) return {
50
+ ext: "emf",
51
+ contentType: "image/x-emf"
52
+ };
53
+ if (b(0) === 215 && b(1) === 205 && b(2) === 198 && b(3) === 154 || b(0) === 1 && b(1) === 0 && b(2) === 9 && b(3) === 0) return {
54
+ ext: "wmf",
55
+ contentType: "image/x-wmf"
56
+ };
57
+ if (b(0) === 37 && b(1) === 80 && b(2) === 68 && b(3) === 70) return {
58
+ ext: "pdf",
59
+ contentType: "application/pdf"
60
+ };
61
+ }
62
+ var HEADER_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml";
63
+ var FOOTER_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml";
64
+ var REL_HEADER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
65
+ var REL_FOOTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
66
+ function newScope() {
67
+ return {
68
+ rels: [],
69
+ relSeq: 0,
70
+ relIdByResource: /* @__PURE__ */ new Map()
71
+ };
72
+ }
73
+ function writeDocx(flow) {
74
+ const losses = [];
75
+ const body = [];
76
+ const state = {
77
+ resources: flow.resources,
78
+ mediaParts: [],
79
+ mediaFileByResource: /* @__PURE__ */ new Map(),
80
+ bookmarkSeq: 0,
81
+ drawingSeq: 0
82
+ };
83
+ const docScope = newScope();
84
+ const extraParts = [];
85
+ const extraPartRels = [];
86
+ const hfCache = /* @__PURE__ */ new Map();
87
+ const sections = flow.sections.length > 0 ? flow.sections : flow.section ? [{
88
+ properties: flow.section,
89
+ endIndex: flow.body.length
90
+ }] : [];
91
+ const sectPrByClosingIndex = /* @__PURE__ */ new Map();
92
+ let finalSectPr = "";
93
+ sections.forEach((sec, i) => {
94
+ const refs = emitHeadersFooters(flow, sec.properties, state, docScope, extraParts, extraPartRels, hfCache, losses);
95
+ const sp = sectPrXml(sec.properties, refs);
96
+ if (i === sections.length - 1) finalSectPr = sp;
97
+ else sectPrByClosingIndex.set(sec.endIndex - 1, sp);
98
+ });
99
+ flow.body.forEach((el, idx) => {
100
+ emitBlock(body, el, losses, state, docScope, sectPrByClosingIndex.get(idx));
101
+ });
102
+ if (finalSectPr) body.push(finalSectPr);
103
+ const documentXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:body>${body.join("")}</w:body></w:document>`;
104
+ const numberingPart = flow.body.some((el) => el.kind === "paragraph" && el.paragraph.properties.numbering !== void 0) && flow.numbering ? {
105
+ path: NUMBERING_PART,
106
+ data: encoder.encode(numberingXml(flow.numbering)),
107
+ contentType: NUMBERING_CONTENT_TYPE
108
+ } : void 0;
109
+ if (numberingPart) docScope.rels.push({
110
+ id: `rId${++docScope.relSeq}`,
111
+ type: REL_NUMBERING,
112
+ target: "numbering.xml",
113
+ targetMode: "Internal"
114
+ });
115
+ const partRelationships = [...docScope.rels.length > 0 ? [{
116
+ sourcePart: "word/document.xml",
117
+ relationships: docScope.rels
118
+ }] : [], ...extraPartRels];
119
+ return {
120
+ bytes: buildOpcPackage({
121
+ parts: [
122
+ {
123
+ path: "word/document.xml",
124
+ data: encoder.encode(documentXml),
125
+ contentType: DOC_CONTENT_TYPE
126
+ },
127
+ ...numberingPart ? [numberingPart] : [],
128
+ ...extraParts,
129
+ ...state.mediaParts
130
+ ],
131
+ rootRelationships: [{
132
+ id: "rId1",
133
+ type: REL_OFFICE_DOCUMENT,
134
+ target: "word/document.xml",
135
+ targetMode: "Internal"
136
+ }],
137
+ ...partRelationships.length > 0 ? { partRelationships } : {}
138
+ }),
139
+ losses
140
+ };
141
+ }
142
+ function emitHeadersFooters(flow, section, state, docScope, extraParts, extraPartRels, hfCache, losses) {
143
+ const refs = {
144
+ headers: [],
145
+ footers: []
146
+ };
147
+ if (!section || !flow.headersFooters) return refs;
148
+ const emitOne = (kind, relationshipId, type) => {
149
+ const cached = hfCache.get(relationshipId);
150
+ if (cached !== void 0) return cached;
151
+ const content = flow.headersFooters.get(relationshipId);
152
+ if (!content) return void 0;
153
+ const n = extraParts.filter((p) => p.path.includes(`/${kind}`)).length + 1;
154
+ const path = `word/${kind}${n}.xml`;
155
+ const root = kind === "header" ? "w:hdr" : "w:ftr";
156
+ const scope = newScope();
157
+ const inner = [];
158
+ for (const el of content) emitBlock(inner, el, losses, state, scope);
159
+ const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><${root} xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">${inner.join("")}</${root}>`;
160
+ extraParts.push({
161
+ path,
162
+ data: encoder.encode(xml),
163
+ contentType: kind === "header" ? HEADER_CONTENT_TYPE : FOOTER_CONTENT_TYPE
164
+ });
165
+ if (scope.rels.length > 0) extraPartRels.push({
166
+ sourcePart: path,
167
+ relationships: scope.rels
168
+ });
169
+ const relId = `rId${++docScope.relSeq}`;
170
+ docScope.rels.push({
171
+ id: relId,
172
+ type: kind === "header" ? REL_HEADER : REL_FOOTER,
173
+ target: `${kind}${n}.xml`,
174
+ targetMode: "Internal"
175
+ });
176
+ hfCache.set(relationshipId, relId);
177
+ return relId;
178
+ };
179
+ for (const h of section.headers) {
180
+ const relId = emitOne("header", h.relationshipId, h.type);
181
+ if (relId) refs.headers.push({
182
+ type: h.type,
183
+ relId
184
+ });
185
+ }
186
+ for (const f of section.footers) {
187
+ const relId = emitOne("footer", f.relationshipId, f.type);
188
+ if (relId) refs.footers.push({
189
+ type: f.type,
190
+ relId
191
+ });
192
+ }
193
+ return refs;
194
+ }
195
+ var docxWriter = {
196
+ id: "docx",
197
+ consumes: "flow",
198
+ supports: new Set([FEATURES.text]),
199
+ write: (doc) => writeDocx(doc)
200
+ };
201
+ function emitBlock(out, el, losses, state, scope, closingSectPr) {
202
+ if (el.kind === "paragraph") {
203
+ out.push(paragraphXml(el.paragraph, state, scope, closingSectPr));
204
+ return;
205
+ }
206
+ if (el.kind === "table") {
207
+ out.push(tableXml(el.table, losses, state, scope));
208
+ if (closingSectPr) out.push(`<w:p><w:pPr>${closingSectPr}</w:pPr></w:p>`);
209
+ return;
210
+ }
211
+ if (el.kind === "image") {
212
+ const drawing = drawingXml(el.image.resource, el.image.width, el.image.height, el.image.altText, state, scope);
213
+ if (drawing) out.push(`<w:p>${pPrWithSect(el.image.paragraphProperties, closingSectPr)}<w:r>${drawing}</w:r></w:p>`);
214
+ else {
215
+ losses.push({
216
+ severity: "dropped",
217
+ feature: FEATURES.images,
218
+ detail: "image bytes missing"
219
+ });
220
+ if (closingSectPr) out.push(`<w:p><w:pPr>${closingSectPr}</w:pPr></w:p>`);
221
+ }
222
+ return;
223
+ }
224
+ if (el.kind === "shape") {
225
+ const drawing = shapeDrawingXml(el.shape, losses, state, scope);
226
+ out.push(`<w:p>${pPrWithSect(el.shape.paragraphProperties, closingSectPr)}<w:r>${drawing}</w:r></w:p>`);
227
+ return;
228
+ }
229
+ if (closingSectPr) out.push(`<w:p><w:pPr>${closingSectPr}</w:pPr></w:p>`);
230
+ losses.push({
231
+ severity: "dropped",
232
+ feature: FEATURES.charts,
233
+ detail: `${el.kind} not written by the docx writer (v0)`
234
+ });
235
+ }
236
+ function drawingXml(resource, widthPt, heightPt, altText, state, scope) {
237
+ if (resource === void 0) return "";
238
+ const relId = mediaRelId(resource, state, scope);
239
+ if (relId === void 0) return "";
240
+ const cx = Math.round(widthPt * EMU_PER_PT);
241
+ const cy = Math.round(heightPt * EMU_PER_PT);
242
+ const id = ++state.drawingSeq;
243
+ return `<w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="${cx}" cy="${cy}"/><wp:docPr id="${id}" name="Image ${id}"${altText ? ` descr="${escapeAttr(altText)}"` : ""}/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:nvPicPr><pic:cNvPr id="${id}" name="Image ${id}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${relId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing>`;
244
+ }
245
+ var WPS_URI = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
246
+ function shapeDrawingXml(shape, losses, state, scope) {
247
+ const cx = Math.round(shape.width * EMU_PER_PT);
248
+ const cy = Math.round(shape.height * EMU_PER_PT);
249
+ const id = ++state.drawingSeq;
250
+ const descr = shape.altText ? ` descr="${escapeAttr(shape.altText)}"` : "";
251
+ const spPr = `<wps:spPr>${xfrmXml(shape.transform, cx, cy)}${geomXml(shape.geometry)}${fillXml(shape.fill)}${shape.line ? lineXml(shape.line) : ""}</wps:spPr>`;
252
+ const txbx = shape.text ? txbxXml(shape.text, losses, state, scope) : "";
253
+ return `<w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="${cx}" cy="${cy}"/><wp:docPr id="${id}" name="Shape ${id}"${descr}/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="${WPS_URI}"><wps:wsp xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"><wps:cNvSpPr/>` + spPr + txbx + bodyPrXml(shape.text) + "</wps:wsp></a:graphicData></a:graphic></wp:inline></w:drawing>";
254
+ }
255
+ function xfrmXml(t, cx, cy) {
256
+ return `<a:xfrm${t?.rotation60k !== void 0 ? ` rot="${t.rotation60k}"` : ""}${t?.flipH ? " flipH=\"1\"" : ""}${t?.flipV ? " flipV=\"1\"" : ""}><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm>`;
257
+ }
258
+ function geomXml(g) {
259
+ if (g.kind === "custom" && g.custom) {
260
+ const cmds = g.custom.commands.map((c) => {
261
+ switch (c.cmd) {
262
+ case "move": return `<a:moveTo><a:pt x="${c.x}" y="${c.y}"/></a:moveTo>`;
263
+ case "line": return `<a:lnTo><a:pt x="${c.x}" y="${c.y}"/></a:lnTo>`;
264
+ case "cubic": return `<a:cubicBezTo><a:pt x="${c.x1}" y="${c.y1}"/><a:pt x="${c.x2}" y="${c.y2}"/><a:pt x="${c.x}" y="${c.y}"/></a:cubicBezTo>`;
265
+ case "quad": return `<a:quadBezTo><a:pt x="${c.x1}" y="${c.y1}"/><a:pt x="${c.x}" y="${c.y}"/></a:quadBezTo>`;
266
+ case "arc": return `<a:arcTo wR="${c.wR}" hR="${c.hR}" stAng="${c.stAng}" swAng="${c.swAng}"/>`;
267
+ case "close": return "<a:close/>";
268
+ }
269
+ }).join("");
270
+ const { pathWidth: w, pathHeight: h } = g.custom;
271
+ return `<a:custGeom><a:avLst/><a:gdLst/><a:rect l="0" t="0" r="${w}" b="${h}"/><a:pathLst><a:path w="${w}" h="${h}">${cmds}</a:path></a:pathLst></a:custGeom>`;
272
+ }
273
+ const gds = g.adjust && g.adjust.size > 0 ? [...g.adjust].map(([n, v]) => `<a:gd name="${escapeAttr(n)}" fmla="val ${v}"/>`).join("") : "";
274
+ return `<a:prstGeom prst="${escapeAttr(g.preset ?? "rect")}"><a:avLst>${gds}</a:avLst></a:prstGeom>`;
275
+ }
276
+ function fillXml(f) {
277
+ return f.kind === "solid" && f.colorHex ? `<a:solidFill><a:srgbClr val="${f.colorHex}"/></a:solidFill>` : "<a:noFill/>";
278
+ }
279
+ function lineXml(l) {
280
+ const w = l.width !== void 0 ? ` w="${Math.round(l.width * EMU_PER_PT)}"` : "";
281
+ const cap = l.cap === "round" ? " cap=\"rnd\"" : l.cap === "square" ? " cap=\"sq\"" : l.cap === "flat" ? " cap=\"flat\"" : "";
282
+ const inner = [];
283
+ if (l.fill === "none") inner.push("<a:noFill/>");
284
+ else if (l.colorHex) inner.push(`<a:solidFill><a:srgbClr val="${l.colorHex}"/></a:solidFill>`);
285
+ if (l.dash) inner.push(`<a:prstDash val="${l.dash}"/>`);
286
+ return `<a:ln${w}${cap}>${inner.join("")}</a:ln>`;
287
+ }
288
+ function txbxXml(text, losses, state, scope) {
289
+ const inner = [];
290
+ for (const el of text.content) emitBlock(inner, el, losses, state, scope);
291
+ return `<wps:txbx><w:txbxContent>${inner.join("")}</w:txbxContent></wps:txbx>`;
292
+ }
293
+ function bodyPrXml(text) {
294
+ if (!text) return "<wps:bodyPr/>";
295
+ const ins = (v, name) => v !== void 0 ? ` ${name}="${Math.round(v * EMU_PER_PT)}"` : "";
296
+ const anchor = text.anchor ? ` anchor="${text.anchor}"` : "";
297
+ return "<wps:bodyPr" + ins(text.insetLeft, "lIns") + ins(text.insetTop, "tIns") + ins(text.insetRight, "rIns") + ins(text.insetBottom, "bIns") + anchor + "/>";
298
+ }
299
+ function mediaRelId(resource, state, scope) {
300
+ const existingRel = scope.relIdByResource.get(resource);
301
+ if (existingRel !== void 0) return existingRel;
302
+ let target = state.mediaFileByResource.get(resource);
303
+ if (target === void 0) {
304
+ const bytes = state.resources.get(resource);
305
+ if (!bytes) return void 0;
306
+ const info = mediaInfo(bytes);
307
+ if (!info) return void 0;
308
+ target = `media/image${state.mediaParts.length + 1}.${info.ext}`;
309
+ state.mediaParts.push({
310
+ path: `word/${target}`,
311
+ data: bytes,
312
+ contentType: info.contentType
313
+ });
314
+ state.mediaFileByResource.set(resource, target);
315
+ }
316
+ const relId = `rId${++scope.relSeq}`;
317
+ scope.rels.push({
318
+ id: relId,
319
+ type: REL_IMAGE,
320
+ target,
321
+ targetMode: "Internal"
322
+ });
323
+ scope.relIdByResource.set(resource, relId);
324
+ return relId;
325
+ }
326
+ function tableXml(table, losses, state, scope) {
327
+ const grid = table.grid.map((w) => `<w:gridCol w:w="${twips(w)}"/>`).join("");
328
+ const rows = table.rows.map((row) => rowXml(row, losses, state, scope)).join("");
329
+ return `<w:tbl>${tblPrXml(table.properties)}<w:tblGrid>${grid}</w:tblGrid>${rows}</w:tbl>`;
330
+ }
331
+ function tblPrXml(p) {
332
+ const out = [];
333
+ if (p.widthType !== void 0) {
334
+ const w = p.widthType === "pct" ? Math.round((p.widthFraction ?? 0) * 5e3) : p.widthPt !== void 0 ? twips(p.widthPt) : 0;
335
+ out.push(`<w:tblW w:w="${w}" w:type="${p.widthType}"/>`);
336
+ }
337
+ if (p.alignment && p.alignment !== "left") out.push(`<w:jc w:val="${p.alignment}"/>`);
338
+ const borders = bordersXml("w:tblBorders", p.borders);
339
+ if (borders) out.push(borders);
340
+ const margins = cellMarginsXml("w:tblCellMar", p.defaultCellMargins);
341
+ if (margins) out.push(margins);
342
+ return out.length > 0 ? `<w:tblPr>${out.join("")}</w:tblPr>` : "";
343
+ }
344
+ function rowXml(row, losses, state, scope) {
345
+ const trPr = [];
346
+ if (row.properties.height !== void 0) {
347
+ const rule = row.properties.heightRule ?? "atLeast";
348
+ trPr.push(`<w:trHeight w:val="${twips(row.properties.height)}" w:hRule="${rule}"/>`);
349
+ }
350
+ if (row.properties.isHeader) trPr.push("<w:tblHeader/>");
351
+ if (row.properties.cantSplit) trPr.push("<w:cantSplit/>");
352
+ return `<w:tr>${trPr.length > 0 ? `<w:trPr>${trPr.join("")}</w:trPr>` : ""}${row.cells.map((cell) => cellXml(cell, losses, state, scope)).join("")}</w:tr>`;
353
+ }
354
+ function cellXml(cell, losses, state, scope) {
355
+ const content = [];
356
+ for (const child of cell.content) emitBlock(content, child, losses, state, scope);
357
+ if (content.length === 0) content.push("<w:p/>");
358
+ return `<w:tc>${tcPrXml(cell.properties)}${content.join("")}</w:tc>`;
359
+ }
360
+ function tcPrXml(p) {
361
+ const out = [];
362
+ if (p.width !== void 0) out.push(`<w:tcW w:w="${twips(p.width)}" w:type="dxa"/>`);
363
+ if (p.colSpan !== void 0 && p.colSpan > 1) out.push(`<w:gridSpan w:val="${p.colSpan}"/>`);
364
+ if (p.merge !== void 0) out.push(p.merge === "start" ? "<w:vMerge w:val=\"restart\"/>" : "<w:vMerge w:val=\"continue\"/>");
365
+ const borders = bordersXml("w:tcBorders", p.borders);
366
+ if (borders) out.push(borders);
367
+ const margins = cellMarginsXml("w:tcMar", p.margins);
368
+ if (margins) out.push(margins);
369
+ if (p.shading) out.push(`<w:shd w:val="clear" w:color="auto" w:fill="${p.shading.colorHex}"/>`);
370
+ return out.length > 0 ? `<w:tcPr>${out.join("")}</w:tcPr>` : "";
371
+ }
372
+ var BORDER_SIDES = [
373
+ ["top", "w:top"],
374
+ ["left", "w:left"],
375
+ ["bottom", "w:bottom"],
376
+ ["right", "w:right"],
377
+ ["insideH", "w:insideH"],
378
+ ["insideV", "w:insideV"]
379
+ ];
380
+ function bordersXml(tag, borders) {
381
+ if (!borders) return "";
382
+ const sides = BORDER_SIDES.map(([key, el]) => {
383
+ const b = borders[key];
384
+ if (!b) return "";
385
+ const sz = b.width !== void 0 ? ` w:sz="${Math.round(b.width * 8)}"` : "";
386
+ const color = b.colorHex !== void 0 ? ` w:color="${b.colorHex}"` : "";
387
+ return `<${el} w:val="${b.style}"${sz}${color}/>`;
388
+ }).join("");
389
+ return sides ? `<${tag}>${sides}</${tag}>` : "";
390
+ }
391
+ function cellMarginsXml(tag, margins) {
392
+ if (!margins) return "";
393
+ const inner = [
394
+ ["top", "w:top"],
395
+ ["left", "w:left"],
396
+ ["bottom", "w:bottom"],
397
+ ["right", "w:right"]
398
+ ].map(([key, el]) => {
399
+ const v = margins[key];
400
+ return v !== void 0 ? `<${el} w:w="${twips(v)}" w:type="dxa"/>` : "";
401
+ }).join("");
402
+ return inner ? `<${tag}>${inner}</${tag}>` : "";
403
+ }
404
+ function paragraphXml(p, state, scope, closingSectPr) {
405
+ const visible = p.runs.filter((run) => !run.listMarker && run.math === void 0 && (run.text !== "" || run.inlineImage !== void 0 || run.pageBreak || run.href !== void 0 || run.anchor !== void 0));
406
+ const inner = [];
407
+ let i = 0;
408
+ while (i < visible.length) {
409
+ const run = visible[i];
410
+ const key = run.href ?? run.anchor;
411
+ if (key === void 0) {
412
+ inner.push(runXml(run, state, scope));
413
+ i++;
414
+ continue;
415
+ }
416
+ let j = i + 1;
417
+ while (j < visible.length && (visible[j].href ?? visible[j].anchor) === key) j++;
418
+ const group = visible.slice(i, j).map((r) => runXml(r, state, scope)).join("");
419
+ inner.push(hyperlinkXml(run, group === "" ? "<w:r/>" : group, state, scope));
420
+ i = j;
421
+ }
422
+ const bookmarks = (p.bookmarks ?? []).map((name) => {
423
+ const id = state.bookmarkSeq++;
424
+ return `<w:bookmarkStart w:id="${id}" w:name="${escapeAttr(name)}"/><w:bookmarkEnd w:id="${id}"/>`;
425
+ }).join("");
426
+ const pPrInner = pPrBody(p.properties) + (closingSectPr ?? "");
427
+ return `<w:p>${pPrInner !== "" ? `<w:pPr>${pPrInner}</w:pPr>` : ""}${bookmarks}${inner.join("")}</w:p>`;
428
+ }
429
+ function hyperlinkXml(run, inner, _state, scope) {
430
+ if (run.href !== void 0) {
431
+ const id = `rId${++scope.relSeq}`;
432
+ scope.rels.push({
433
+ id,
434
+ type: REL_HYPERLINK,
435
+ target: run.href,
436
+ targetMode: "External"
437
+ });
438
+ return `<w:hyperlink r:id="${id}">${inner}</w:hyperlink>`;
439
+ }
440
+ return `<w:hyperlink w:anchor="${escapeAttr(run.anchor)}">${inner}</w:hyperlink>`;
441
+ }
442
+ function runXml(run, state, scope) {
443
+ const rPr = rPrXml(run.properties);
444
+ if (run.inlineImage !== void 0) {
445
+ const img = run.inlineImage;
446
+ const drawing = drawingXml(img.resource, img.width, img.height, void 0, state, scope);
447
+ if (drawing) return `<w:r>${rPr}${drawing}</w:r>`;
448
+ if (run.text === "" && !run.pageBreak) return "";
449
+ }
450
+ const brk = run.pageBreak ? "<w:br w:type=\"page\"/>" : "";
451
+ if (run.text === "") return brk ? `<w:r>${rPr}${brk}</w:r>` : "";
452
+ return `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(run.text)}</w:t>${brk}</w:r>`;
453
+ }
454
+ function rPrXml(r) {
455
+ const out = [];
456
+ if (r.bold !== DEFAULT_RUN.bold) out.push(toggle("w:b", r.bold));
457
+ if (r.italic !== DEFAULT_RUN.italic) out.push(toggle("w:i", r.italic));
458
+ if (r.strike !== DEFAULT_RUN.strike) out.push(toggle("w:strike", r.strike));
459
+ if (r.underline !== DEFAULT_RUN.underline) out.push(`<w:u w:val="${r.underline}"/>`);
460
+ const fonts = rFontsXml(r.fontFamily);
461
+ if (fonts) out.push(fonts);
462
+ if (r.fontSizePt !== DEFAULT_RUN.fontSizePt) out.push(`<w:sz w:val="${Math.round(r.fontSizePt * 2)}"/>`);
463
+ if (r.colorHex !== DEFAULT_RUN.colorHex) out.push(`<w:color w:val="${r.colorHex}"/>`);
464
+ if (r.verticalAlign !== DEFAULT_RUN.verticalAlign) out.push(`<w:vertAlign w:val="${r.verticalAlign}"/>`);
465
+ if (r.rtl !== DEFAULT_RUN.rtl) out.push(toggle("w:rtl", r.rtl));
466
+ if (r.lang !== void 0) out.push(`<w:lang w:val="${escapeAttr(r.lang)}"/>`);
467
+ return out.length > 0 ? `<w:rPr>${out.join("")}</w:rPr>` : "";
468
+ }
469
+ function pPrBody(p) {
470
+ const out = [];
471
+ if (p.numbering) out.push(`<w:numPr><w:ilvl w:val="${p.numbering.ilvl}"/><w:numId w:val="${escapeAttr(p.numbering.numId)}"/></w:numPr>`);
472
+ if (p.outlineLevel !== void 0) out.push(`<w:outlineLvl w:val="${p.outlineLevel}"/>`);
473
+ if (p.pageBreakBefore) out.push("<w:pageBreakBefore/>");
474
+ if (p.bidi !== DEFAULT_PARA.bidi) out.push(toggle("w:bidi", p.bidi));
475
+ const ind = indXml(p);
476
+ if (ind) out.push(ind);
477
+ const spacing = spacingXml(p);
478
+ if (spacing) out.push(spacing);
479
+ if (p.alignment !== DEFAULT_PARA.alignment) out.push(`<w:jc w:val="${p.alignment}"/>`);
480
+ return out.join("");
481
+ }
482
+ function pPrWithSect(p, closingSectPr) {
483
+ const inner = pPrBody(p) + (closingSectPr ?? "");
484
+ return inner !== "" ? `<w:pPr>${inner}</w:pPr>` : "";
485
+ }
486
+ function indXml(p) {
487
+ const attrs = [];
488
+ if (p.indentLeft !== DEFAULT_PARA.indentLeft) attrs.push(`w:left="${twips(p.indentLeft)}"`);
489
+ if (p.indentRight !== DEFAULT_PARA.indentRight) attrs.push(`w:right="${twips(p.indentRight)}"`);
490
+ if (p.indentFirstLine !== DEFAULT_PARA.indentFirstLine) if (p.indentFirstLine < 0) attrs.push(`w:hanging="${twips(-p.indentFirstLine)}"`);
491
+ else attrs.push(`w:firstLine="${twips(p.indentFirstLine)}"`);
492
+ return attrs.length > 0 ? `<w:ind ${attrs.join(" ")}/>` : "";
493
+ }
494
+ function spacingXml(p) {
495
+ const attrs = [];
496
+ if (p.spacingBefore !== DEFAULT_PARA.spacingBefore) attrs.push(`w:before="${twips(p.spacingBefore)}"`);
497
+ if (p.spacingAfter !== DEFAULT_PARA.spacingAfter) attrs.push(`w:after="${twips(p.spacingAfter)}"`);
498
+ if ((p.spacingLineRule !== DEFAULT_PARA.spacingLineRule || p.spacingLine !== DEFAULT_PARA.spacingLine) && p.spacingLine > 0) {
499
+ const lineVal = p.spacingLineRule === "auto" ? Math.round(p.spacingLine * 12) : twips(p.spacingLine);
500
+ attrs.push(`w:line="${lineVal}"`, `w:lineRule="${p.spacingLineRule}"`);
501
+ }
502
+ return attrs.length > 0 ? `<w:spacing ${attrs.join(" ")}/>` : "";
503
+ }
504
+ function rFontsXml(fonts) {
505
+ const d = DEFAULT_RUN.fontFamily;
506
+ const attrs = [];
507
+ if (fonts.ascii && fonts.ascii !== d.ascii) attrs.push(`w:ascii="${escapeAttr(fonts.ascii)}"`);
508
+ if (fonts.hAnsi && fonts.hAnsi !== d.hAnsi) attrs.push(`w:hAnsi="${escapeAttr(fonts.hAnsi)}"`);
509
+ if (fonts.cs && fonts.cs !== d.cs) attrs.push(`w:cs="${escapeAttr(fonts.cs)}"`);
510
+ return attrs.length > 0 ? `<w:rFonts ${attrs.join(" ")}/>` : "";
511
+ }
512
+ function toggle(tag, on) {
513
+ return on ? `<${tag}/>` : `<${tag} w:val="false"/>`;
514
+ }
515
+ function numberingXml(numbering) {
516
+ const out = ["<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>", "<w:numbering xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">"];
517
+ for (const abstractNum of numbering.abstractNums.values()) {
518
+ out.push(`<w:abstractNum w:abstractNumId="${escapeAttr(abstractNum.id)}">`);
519
+ for (const level of [...abstractNum.levels.values()].sort((a, b) => a.ilvl - b.ilvl)) out.push(levelXml(level));
520
+ out.push("</w:abstractNum>");
521
+ }
522
+ for (const inst of numbering.numInstances.values()) out.push(`<w:num w:numId="${escapeAttr(inst.numId)}"><w:abstractNumId w:val="${escapeAttr(inst.abstractNumId)}"/></w:num>`);
523
+ out.push("</w:numbering>");
524
+ return out.join("");
525
+ }
526
+ function levelXml(level) {
527
+ const inner = [
528
+ `<w:start w:val="${level.start}"/>`,
529
+ `<w:numFmt w:val="${level.format}"/>`,
530
+ `<w:lvlText w:val="${escapeAttr(level.lvlText)}"/>`
531
+ ];
532
+ const pPr = rawParaPrXml(level.paragraphProperties);
533
+ if (pPr) inner.push(pPr);
534
+ const rPr = rawRunPrXml(level.runProperties);
535
+ if (rPr) inner.push(rPr);
536
+ return `<w:lvl w:ilvl="${level.ilvl}">${inner.join("")}</w:lvl>`;
537
+ }
538
+ function rawParaPrXml(p) {
539
+ const attrs = [];
540
+ if (p.indentLeft !== void 0) attrs.push(`w:left="${twips(p.indentLeft)}"`);
541
+ if (p.indentRight !== void 0) attrs.push(`w:right="${twips(p.indentRight)}"`);
542
+ if (p.indentFirstLine !== void 0) if (p.indentFirstLine < 0) attrs.push(`w:hanging="${twips(-p.indentFirstLine)}"`);
543
+ else attrs.push(`w:firstLine="${twips(p.indentFirstLine)}"`);
544
+ const ind = attrs.length > 0 ? `<w:ind ${attrs.join(" ")}/>` : "";
545
+ const jc = p.alignment !== void 0 ? `<w:jc w:val="${p.alignment}"/>` : "";
546
+ return ind || jc ? `<w:pPr>${jc}${ind}</w:pPr>` : "";
547
+ }
548
+ function rawRunPrXml(r) {
549
+ const out = [];
550
+ if (r.bold !== void 0) out.push(toggle("w:b", r.bold));
551
+ if (r.italic !== void 0) out.push(toggle("w:i", r.italic));
552
+ if (r.strike !== void 0) out.push(toggle("w:strike", r.strike));
553
+ if (r.underline !== void 0) out.push(`<w:u w:val="${r.underline}"/>`);
554
+ const fonts = r.fontFamily ? rawRFontsXml(r.fontFamily) : "";
555
+ if (fonts) out.push(fonts);
556
+ if (r.fontSizePt !== void 0) out.push(`<w:sz w:val="${Math.round(r.fontSizePt * 2)}"/>`);
557
+ if (r.colorHex !== void 0) out.push(`<w:color w:val="${r.colorHex}"/>`);
558
+ if (r.verticalAlign !== void 0) out.push(`<w:vertAlign w:val="${r.verticalAlign}"/>`);
559
+ return out.length > 0 ? `<w:rPr>${out.join("")}</w:rPr>` : "";
560
+ }
561
+ function rawRFontsXml(fonts) {
562
+ const attrs = [];
563
+ if (fonts.ascii) attrs.push(`w:ascii="${escapeAttr(fonts.ascii)}"`);
564
+ if (fonts.hAnsi) attrs.push(`w:hAnsi="${escapeAttr(fonts.hAnsi)}"`);
565
+ if (fonts.cs) attrs.push(`w:cs="${escapeAttr(fonts.cs)}"`);
566
+ return attrs.length > 0 ? `<w:rFonts ${attrs.join(" ")}/>` : "";
567
+ }
568
+ function sectPrXml(s, hf) {
569
+ const parts = [];
570
+ for (const h of hf.headers) parts.push(`<w:headerReference w:type="${h.type}" r:id="${h.relId}"/>`);
571
+ for (const f of hf.footers) parts.push(`<w:footerReference w:type="${f.type}" r:id="${f.relId}"/>`);
572
+ if (s.pageSize) {
573
+ const orient = s.pageSize.orientation === "landscape" ? " w:orient=\"landscape\"" : "";
574
+ parts.push(`<w:pgSz w:w="${twips(s.pageSize.width)}" w:h="${twips(s.pageSize.height)}"${orient}/>`);
575
+ }
576
+ if (s.margins) {
577
+ const m = s.margins;
578
+ const header = m.header !== void 0 ? ` w:header="${twips(m.header)}"` : "";
579
+ const footer = m.footer !== void 0 ? ` w:footer="${twips(m.footer)}"` : "";
580
+ parts.push(`<w:pgMar w:top="${twips(m.top)}" w:right="${twips(m.right)}" w:bottom="${twips(m.bottom)}" w:left="${twips(m.left)}"${header}${footer}/>`);
581
+ }
582
+ if (s.columns) parts.push(colsXml(s.columns));
583
+ if (s.titlePg) parts.push("<w:titlePg/>");
584
+ if (parts.length === 0) return "";
585
+ return `<w:sectPr>${parts.join("")}</w:sectPr>`;
586
+ }
587
+ function colsXml(cols) {
588
+ if (cols.explicit && cols.explicit.length > 0) {
589
+ const inner = cols.explicit.map((c) => `<w:col w:w="${twips(c.widthPt)}" w:space="${twips(c.spacePt)}"/>`).join("");
590
+ return `<w:cols w:num="${cols.explicit.length}" w:equalWidth="0">${inner}</w:cols>`;
591
+ }
592
+ return `<w:cols w:num="${cols.count}" w:space="${twips(cols.spacePt)}"/>`;
593
+ }
594
+ var twips = (pt) => Math.round(pt * 20);
595
+ function escapeXml(s) {
596
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
597
+ }
598
+ function escapeAttr(s) {
599
+ return escapeXml(s).replace(/"/g, "&quot;");
600
+ }
601
+ //#endregion
602
+ export { docxWriter, writeDocx };
@@ -35,3 +35,4 @@ export type DrawingContent = {
35
35
  export declare function resolveMc(altContent: PoNode): ReadonlyArray<PoNode>;
36
36
  export declare function expandMcChildren(children: ReadonlyArray<PoNode>): Array<PoNode>;
37
37
  export declare function parseDrawing(drawing: PoNode, resolveColor: ColorResolver, parseBody?: ParseBody): DrawingContent | null;
38
+ export declare function parseVmlPicture(node: PoNode): DrawingContent | null;
@@ -1,4 +1,4 @@
1
- import { emuToPt } from "../core/ir/units.js";
1
+ import { emuToPt, pt } from "../core/ir/units.js";
2
2
  import { poAttr, poChildren, poFindDescendant, poIntAttr, poIs, poTag, poText } from "../core/po-helpers.js";
3
3
  import { resolveColorNode } from "../core/drawingml/colors.js";
4
4
  //#region src/word/drawing-parser.ts
@@ -115,6 +115,41 @@ function parseDrawing(drawing, resolveColor, parseBody) {
115
115
  };
116
116
  return null;
117
117
  }
118
+ function parseVmlPicture(node) {
119
+ const imagedata = poFindDescendant(node, "v:imagedata");
120
+ if (!imagedata) return null;
121
+ const imageId = poAttr(imagedata, "id");
122
+ if (imageId === void 0) return null;
123
+ const shape = poFindDescendant(node, "v:shape") ?? poFindDescendant(node, "v:rect") ?? poFindDescendant(node, "v:oval");
124
+ const width = vmlStyleLength(shape, "width");
125
+ const height = vmlStyleLength(shape, "height");
126
+ if (width === void 0 || height === void 0) return null;
127
+ const altText = (shape ? poAttr(shape, "alt") : void 0)?.trim() || poAttr(imagedata, "title")?.trim() || void 0;
128
+ return {
129
+ kind: "image",
130
+ imageId,
131
+ width,
132
+ height,
133
+ ...altText ? { altText } : {}
134
+ };
135
+ }
136
+ var VML_UNIT_TO_PT = {
137
+ pt: 1,
138
+ in: 72,
139
+ px: .75,
140
+ cm: 72 / 2.54,
141
+ mm: 72 / 25.4,
142
+ pc: 12
143
+ };
144
+ function vmlStyleLength(shape, prop) {
145
+ const style = shape ? poAttr(shape, "style") : void 0;
146
+ if (style === void 0) return void 0;
147
+ const m = new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*(-?[0-9.]+)(pt|in|px|cm|mm|pc)?`, "i").exec(style);
148
+ if (!m) return void 0;
149
+ const value = Number.parseFloat(m[1]);
150
+ if (!Number.isFinite(value) || value <= 0) return void 0;
151
+ return pt(value * (VML_UNIT_TO_PT[(m[2] ?? "px").toLowerCase()] ?? 1));
152
+ }
118
153
  function parseWsp(graphicData, extentCx, extentCy, resolveColor, parseBody) {
119
154
  const wsp = expandMcChildren(poChildren(graphicData)).find((c) => poIs(c, "wps:wsp"));
120
155
  if (!wsp) return null;
@@ -395,4 +430,4 @@ function colorFromGradient(grad, resolveColor) {
395
430
  return (toHex(r) + toHex(g) + toHex(b)).toUpperCase();
396
431
  }
397
432
  //#endregion
398
- export { expandMcChildren, parseDrawing };
433
+ export { expandMcChildren, parseDrawing, parseVmlPicture };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reamkit",
3
- "version": "1.3.0",
4
- "description": "Ream — DOCX/XLSX to PDF (and SVG), built from scratch on the ECMA-376 and ISO 32000 specifications. Parse once, convert anywhere.",
3
+ "version": "1.4.0",
4
+ "description": "Ream — DOCX/XLSX to PDF, SVG, HTML and back to DOCX, built from scratch on the ECMA-376 and ISO 32000 specifications. Parse once, convert anywhere.",
5
5
  "license": "MIT",
6
6
  "author": "Alex Krassavin <info@reamkit.dev>",
7
7
  "repository": {
@@ -66,7 +66,8 @@
66
66
  "corpus:build": "tsx scripts/corpus/build-corpus.ts",
67
67
  "corpus:fetch": "tsx scripts/corpus/fetch-corpus.ts",
68
68
  "corpus:sandbox:build": "docker build -t docgen-losandbox:latest scripts/corpus/sandbox/",
69
- "corpus": "tsx scripts/corpus/run.ts"
69
+ "corpus": "tsx scripts/corpus/run.ts",
70
+ "corpus:roundtrip": "tsx scripts/corpus/roundtrip.ts"
70
71
  },
71
72
  "dependencies": {
72
73
  "fast-xml-parser": "5.7.0",