@json-to-office/core-docx 1.4.0 → 1.5.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.
@@ -5206,88 +5206,6 @@ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
5206
5206
  return map;
5207
5207
  }
5208
5208
 
5209
- // src/json/normalizer.ts
5210
- function normalizeComponent(component) {
5211
- if (component.name === "columns") {
5212
- return normalizeColumnsComponent(component);
5213
- }
5214
- if ("children" in component && component.children) {
5215
- return {
5216
- ...component,
5217
- // Preserve ALL properties from validated component
5218
- children: component.children.map(normalizeComponent)
5219
- };
5220
- }
5221
- if (component.name === "section") {
5222
- return normalizeSectionComponent(component);
5223
- }
5224
- return component;
5225
- }
5226
- function normalizeColumnsComponent(component) {
5227
- const cfg = component.props || {};
5228
- const applyTopLevelGap = (cols, topGap) => cols.map((c, i) => {
5229
- const isLast = i === cols.length - 1;
5230
- if (!isLast && c.gap === void 0 && topGap !== void 0) {
5231
- return { ...c, gap: topGap };
5232
- }
5233
- return c;
5234
- });
5235
- const normalizeAutoWidth = (cols) => cols.map((c) => ({
5236
- ...c,
5237
- width: c.width === "auto" ? void 0 : c.width
5238
- }));
5239
- let columnsArray;
5240
- if (typeof cfg.columns === "number" && cfg.columns > 0) {
5241
- const n = cfg.columns;
5242
- const base = Array.from(
5243
- { length: n },
5244
- () => ({})
5245
- );
5246
- const topLevelGap = cfg.gap !== void 0 ? cfg.gap : "5%";
5247
- columnsArray = applyTopLevelGap(base, topLevelGap);
5248
- } else if (Array.isArray(cfg.columns)) {
5249
- columnsArray = cfg.columns;
5250
- const topLevelGap = cfg.gap !== void 0 ? cfg.gap : "5%";
5251
- columnsArray = applyTopLevelGap(columnsArray, topLevelGap);
5252
- columnsArray = normalizeAutoWidth(columnsArray);
5253
- } else {
5254
- columnsArray = [{}];
5255
- }
5256
- const normalizedProps = {
5257
- columns: columnsArray
5258
- };
5259
- return {
5260
- ...component,
5261
- // Preserve any additional properties
5262
- name: "columns",
5263
- props: normalizedProps,
5264
- children: component.children ? component.children.map(normalizeComponent) : void 0
5265
- };
5266
- }
5267
- function normalizeSectionComponent(component) {
5268
- const props = component.props || {};
5269
- return {
5270
- ...component,
5271
- // Preserve ALL properties
5272
- props: {
5273
- ...props,
5274
- // Preserve ALL props properties
5275
- header: props.header ? props.header === "linkToPrevious" ? "linkToPrevious" : props.header.map((m) => normalizeComponent(m)) : void 0,
5276
- footer: props.footer ? props.footer === "linkToPrevious" ? "linkToPrevious" : props.footer.map((m) => normalizeComponent(m)) : void 0
5277
- },
5278
- children: component.children ? component.children.map(normalizeComponent) : void 0
5279
- };
5280
- }
5281
- function normalizeDocument(document) {
5282
- if (document.name !== "docx") {
5283
- throw new Error(
5284
- 'Top-level document must be a docx component with name="docx"'
5285
- );
5286
- }
5287
- const normalized = normalizeComponent(document);
5288
- return [normalized];
5289
- }
5290
-
5291
5209
  // src/core/desugarExternals.ts
5292
5210
  import {
5293
5211
  clampVisualDpi as clampVisualDpi2,
@@ -11198,8 +11116,7 @@ function resolveComponentTree(components, theme) {
11198
11116
  }
11199
11117
 
11200
11118
  // src/core/structure.ts
11201
- async function processDocument(document, theme, themeName, generationDate) {
11202
- const metadata = createDocumentMetadata(document.props, generationDate);
11119
+ function resolveDocumentTree(document, theme) {
11203
11120
  const docDefaults = document.props.componentDefaults;
11204
11121
  const mergedComponentDefaults = docDefaults ? mergeWithDefaults2(docDefaults, theme.componentDefaults || {}) : void 0;
11205
11122
  const docNoProofWords = document.props.noProofWords;
@@ -11213,6 +11130,14 @@ async function processDocument(document, theme, themeName, generationDate) {
11213
11130
  },
11214
11131
  ...mergedNoProofWords && { noProofWords: mergedNoProofWords }
11215
11132
  } : theme;
11133
+ return {
11134
+ theme: effectiveTheme,
11135
+ children: resolveComponentTree(document.children || [], effectiveTheme)
11136
+ };
11137
+ }
11138
+ async function processResolvedDocument(document, resolved, themeName, generationDate) {
11139
+ const metadata = createDocumentMetadata(document.props, generationDate);
11140
+ const effectiveTheme = resolved.theme;
11216
11141
  const context = createRenderContext(
11217
11142
  {
11218
11143
  metadata,
@@ -11223,11 +11148,7 @@ async function processDocument(document, theme, themeName, generationDate) {
11223
11148
  effectiveTheme,
11224
11149
  themeName
11225
11150
  );
11226
- const resolvedChildren = resolveComponentTree(
11227
- document.children || [],
11228
- effectiveTheme
11229
- );
11230
- const sections = await extractSections(resolvedChildren, context);
11151
+ const sections = await extractSections(resolved.children, context);
11231
11152
  return {
11232
11153
  metadata,
11233
11154
  sections,
@@ -11327,6 +11248,326 @@ function createRenderContext(document, theme, themeName) {
11327
11248
  };
11328
11249
  }
11329
11250
 
11251
+ // src/quality/facts.ts
11252
+ import { DEFAULT_DOCX_RENDERER_ID as DEFAULT_DOCX_RENDERER_ID2 } from "@json-to-office/shared-docx";
11253
+
11254
+ // src/json/normalizer.ts
11255
+ function normalizeComponent(component) {
11256
+ if (component.name === "columns") {
11257
+ return normalizeColumnsComponent(component);
11258
+ }
11259
+ if ("children" in component && component.children) {
11260
+ return {
11261
+ ...component,
11262
+ // Preserve ALL properties from validated component
11263
+ children: component.children.map(normalizeComponent)
11264
+ };
11265
+ }
11266
+ if (component.name === "section") {
11267
+ return normalizeSectionComponent(component);
11268
+ }
11269
+ return component;
11270
+ }
11271
+ function normalizeColumnsComponent(component) {
11272
+ const cfg = component.props || {};
11273
+ const applyTopLevelGap = (cols, topGap) => cols.map((c, i) => {
11274
+ const isLast = i === cols.length - 1;
11275
+ if (!isLast && c.gap === void 0 && topGap !== void 0) {
11276
+ return { ...c, gap: topGap };
11277
+ }
11278
+ return c;
11279
+ });
11280
+ const normalizeAutoWidth = (cols) => cols.map((c) => ({
11281
+ ...c,
11282
+ width: c.width === "auto" ? void 0 : c.width
11283
+ }));
11284
+ let columnsArray;
11285
+ if (typeof cfg.columns === "number" && cfg.columns > 0) {
11286
+ const n = cfg.columns;
11287
+ const base = Array.from(
11288
+ { length: n },
11289
+ () => ({})
11290
+ );
11291
+ const topLevelGap = cfg.gap !== void 0 ? cfg.gap : "5%";
11292
+ columnsArray = applyTopLevelGap(base, topLevelGap);
11293
+ } else if (Array.isArray(cfg.columns)) {
11294
+ columnsArray = cfg.columns;
11295
+ const topLevelGap = cfg.gap !== void 0 ? cfg.gap : "5%";
11296
+ columnsArray = applyTopLevelGap(columnsArray, topLevelGap);
11297
+ columnsArray = normalizeAutoWidth(columnsArray);
11298
+ } else {
11299
+ columnsArray = [{}];
11300
+ }
11301
+ const normalizedProps = {
11302
+ columns: columnsArray
11303
+ };
11304
+ return {
11305
+ ...component,
11306
+ // Preserve any additional properties
11307
+ name: "columns",
11308
+ props: normalizedProps,
11309
+ children: component.children ? component.children.map(normalizeComponent) : void 0
11310
+ };
11311
+ }
11312
+ function normalizeSectionComponent(component) {
11313
+ const props = component.props || {};
11314
+ return {
11315
+ ...component,
11316
+ // Preserve ALL properties
11317
+ props: {
11318
+ ...props,
11319
+ // Preserve ALL props properties
11320
+ header: props.header ? props.header === "linkToPrevious" ? "linkToPrevious" : props.header.map((m) => normalizeComponent(m)) : void 0,
11321
+ footer: props.footer ? props.footer === "linkToPrevious" ? "linkToPrevious" : props.footer.map((m) => normalizeComponent(m)) : void 0
11322
+ },
11323
+ children: component.children ? component.children.map(normalizeComponent) : void 0
11324
+ };
11325
+ }
11326
+ function normalizeDocument(document) {
11327
+ if (document.name !== "docx") {
11328
+ throw new Error(
11329
+ 'Top-level document must be a docx component with name="docx"'
11330
+ );
11331
+ }
11332
+ const normalized = normalizeComponent(document);
11333
+ return [normalized];
11334
+ }
11335
+
11336
+ // src/quality/facts.ts
11337
+ init_widthUtils();
11338
+ function asRecord(value) {
11339
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
11340
+ }
11341
+ function pageBox(theme, themeName, pageOverride) {
11342
+ const page = createSectionProperties(
11343
+ getColumnSettings("single"),
11344
+ theme,
11345
+ themeName,
11346
+ "nextPage",
11347
+ pageOverride
11348
+ ).page;
11349
+ return {
11350
+ availableWidthTwips: Math.max(
11351
+ 0,
11352
+ page.size.width - page.margin.left - page.margin.right
11353
+ ),
11354
+ pageBottomTwips: page.size.height
11355
+ };
11356
+ }
11357
+ function tableFact(props, path3, availableWidthTwips) {
11358
+ const columns = Array.isArray(props.columns) ? props.columns : void 0;
11359
+ if (!columns) return void 0;
11360
+ let totalWidthTwips = 0;
11361
+ let hasExplicitWidth = false;
11362
+ let pointSum = 0;
11363
+ let percentSum = 0;
11364
+ const explicitWidths = [];
11365
+ columns.forEach((column, index) => {
11366
+ const width = asRecord(column)?.width;
11367
+ if (typeof width === "number" && Number.isFinite(width)) {
11368
+ hasExplicitWidth = true;
11369
+ explicitWidths.push({ index, width });
11370
+ totalWidthTwips += relativeLengthToTwips(width, availableWidthTwips);
11371
+ pointSum += width;
11372
+ } else if (typeof width === "string") {
11373
+ hasExplicitWidth = true;
11374
+ const percent = width.trim().endsWith("%") ? Number(width.trim().slice(0, -1)) : Number.NaN;
11375
+ if (Number.isFinite(percent)) {
11376
+ explicitWidths.push({ index, width });
11377
+ percentSum += percent;
11378
+ totalWidthTwips += Math.max(
11379
+ 0,
11380
+ Math.round(availableWidthTwips * percent / 100)
11381
+ );
11382
+ }
11383
+ }
11384
+ });
11385
+ return {
11386
+ id: `docx:table-width:${path3}`,
11387
+ kind: "docx/table-width",
11388
+ path: `${path3}/props/columns`,
11389
+ totalWidthTwips,
11390
+ availableWidthTwips,
11391
+ hasExplicitWidth,
11392
+ allColumnsExplicit: explicitWidths.length === columns.length,
11393
+ pointSum,
11394
+ percentSum,
11395
+ explicitWidths
11396
+ };
11397
+ }
11398
+ var TWIPS_PER_POINT3 = 20;
11399
+ var SINGLE_LINE_RATIO = 1.2;
11400
+ function finiteNumber(value) {
11401
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
11402
+ }
11403
+ function lineHeightPt(fontSizePt, spacing) {
11404
+ const rule = asRecord(spacing);
11405
+ const value = finiteNumber(rule?.value);
11406
+ switch (rule?.type) {
11407
+ // `exactly` is an absolute line box, and display type routinely sets it
11408
+ // below the font size — reading it as a multiplier would inflate every
11409
+ // estimate on exactly the headings that matter most.
11410
+ case "exactly":
11411
+ return value ?? fontSizePt * SINGLE_LINE_RATIO;
11412
+ case "atLeast":
11413
+ return Math.max(value ?? 0, fontSizePt * SINGLE_LINE_RATIO);
11414
+ case "multiple":
11415
+ return fontSizePt * SINGLE_LINE_RATIO * (value ?? 1);
11416
+ default:
11417
+ return fontSizePt * SINGLE_LINE_RATIO;
11418
+ }
11419
+ }
11420
+ function characterSpacingPt(spacing) {
11421
+ const rule = asRecord(spacing);
11422
+ const value = finiteNumber(rule?.value);
11423
+ if (rule === void 0 || value === void 0) return 0;
11424
+ return rule.type === "condensed" ? -value / TWIPS_PER_POINT3 : value / TWIPS_PER_POINT3;
11425
+ }
11426
+ function frameTextFact(props, path3, page) {
11427
+ const floating = asRecord(props.floating);
11428
+ const frameWidthTwips = finiteNumber(floating?.width);
11429
+ if (frameWidthTwips === void 0) return void 0;
11430
+ const text = typeof props.text === "string" ? props.text : "";
11431
+ if (text.trim() === "") return void 0;
11432
+ const font = asRecord(props.font);
11433
+ const fontSizePt = finiteNumber(font?.size);
11434
+ if (fontSizePt === void 0) return void 0;
11435
+ const longestWord = text.split(/\s+/).reduce(
11436
+ (longest, word) => word.length > longest.length ? word : longest,
11437
+ ""
11438
+ );
11439
+ const offsetY = finiteNumber(asRecord(floating?.verticalPosition)?.offset);
11440
+ return {
11441
+ id: `docx:frame-text:${path3}`,
11442
+ kind: "docx/frame-text",
11443
+ path: path3,
11444
+ text,
11445
+ fontSizePt,
11446
+ lineHeightPt: lineHeightPt(fontSizePt, font?.lineSpacing),
11447
+ characterSpacingPt: characterSpacingPt(font?.characterSpacing),
11448
+ frameWidthTwips,
11449
+ ...finiteNumber(floating?.height) !== void 0 && {
11450
+ frameHeightTwips: finiteNumber(floating?.height)
11451
+ },
11452
+ ...offsetY !== void 0 && { offsetYTwips: offsetY },
11453
+ pageBottomTwips: page.pageBottomTwips,
11454
+ longestWord
11455
+ };
11456
+ }
11457
+ var SVG_VIEWBOX = /viewBox\s*=\s*"\s*(-?[\d.]+)[\s,]+(-?[\d.]+)[\s,]+(-?[\d.]+)[\s,]+(-?[\d.]+)\s*"/;
11458
+ var SVG_TEXT = /<text\b([^>]*)>([^<]*)<\/text>/g;
11459
+ var SVG_ATTR = (name) => new RegExp(`\\b${name}\\s*=\\s*"(-?[\\d.]+)"`);
11460
+ function svgTextFacts(props, path3) {
11461
+ const svg = typeof props.svg === "string" ? props.svg : void 0;
11462
+ if (!svg) return [];
11463
+ const box = SVG_VIEWBOX.exec(svg);
11464
+ if (!box) return [];
11465
+ const viewBoxMinY = Number(box[2]);
11466
+ const viewBoxHeight = Number(box[4]);
11467
+ if (!Number.isFinite(viewBoxMinY) || !Number.isFinite(viewBoxHeight)) {
11468
+ return [];
11469
+ }
11470
+ const facts = [];
11471
+ let match;
11472
+ SVG_TEXT.lastIndex = 0;
11473
+ let index = 0;
11474
+ while ((match = SVG_TEXT.exec(svg)) !== null) {
11475
+ const [, attributes, content] = match;
11476
+ const baselineY = Number(SVG_ATTR("y").exec(attributes)?.[1]);
11477
+ if (!Number.isFinite(baselineY)) {
11478
+ index += 1;
11479
+ continue;
11480
+ }
11481
+ const fontSizeUnits = Number(
11482
+ SVG_ATTR("font-size").exec(attributes)?.[1] ?? 0
11483
+ );
11484
+ facts.push({
11485
+ id: `docx:svg-text:${path3}:${index}`,
11486
+ kind: "docx/svg-text",
11487
+ path: `${path3}/props/svg`,
11488
+ content: content.trim(),
11489
+ baselineY,
11490
+ fontSizeUnits: Number.isFinite(fontSizeUnits) ? fontSizeUnits : 0,
11491
+ viewBoxMinY,
11492
+ viewBoxHeight
11493
+ });
11494
+ index += 1;
11495
+ }
11496
+ return facts;
11497
+ }
11498
+ function walkActive(node, path3, page, visit) {
11499
+ const rec = asRecord(node);
11500
+ if (!rec || rec.enabled === false) return;
11501
+ visit(rec, path3, page);
11502
+ const children = Array.isArray(rec.children) ? rec.children : [];
11503
+ children.forEach(
11504
+ (child, index) => walkActive(child, `${path3}/children/${index}`, page, visit)
11505
+ );
11506
+ }
11507
+ function prepareDocxQualityDocument(document, options = {}) {
11508
+ const context = options.context ?? resolveThemeContext(normalizeDocument(document)[0], {
11509
+ customThemes: options.customThemes,
11510
+ fonts: options.fonts,
11511
+ warnings: options.warnings
11512
+ });
11513
+ const resolved = resolveDocumentTree(context.document, context.theme);
11514
+ const basePage = pageBox(resolved.theme, context.themeName);
11515
+ const facts = [];
11516
+ const provenance = {};
11517
+ let previousHeadingLevel;
11518
+ const addFact = (fact) => {
11519
+ facts.push(fact);
11520
+ provenance[fact.id] = {
11521
+ path: fact.path,
11522
+ ...fact.relatedPaths && { relatedPaths: fact.relatedPaths }
11523
+ };
11524
+ };
11525
+ const visit = (node, path3, page) => {
11526
+ const props = asRecord(node.props) ?? {};
11527
+ if (node.name === "table") {
11528
+ const fact = tableFact(props, path3, page.availableWidthTwips);
11529
+ if (fact) addFact(fact);
11530
+ }
11531
+ if (node.name === "paragraph" || node.name === "text-box") {
11532
+ const fact = frameTextFact(props, path3, page);
11533
+ if (fact) addFact(fact);
11534
+ }
11535
+ if (node.name === "image" || node.name === "visual") {
11536
+ for (const fact of svgTextFacts(props, path3)) addFact(fact);
11537
+ }
11538
+ if (node.name === "heading") {
11539
+ const level = typeof props.level === "number" && Number.isFinite(props.level) ? props.level : 1;
11540
+ addFact({
11541
+ id: `docx:heading:${path3}`,
11542
+ kind: "docx/heading",
11543
+ path: `${path3}/props/level`,
11544
+ level,
11545
+ ...previousHeadingLevel !== void 0 && {
11546
+ previousLevel: previousHeadingLevel
11547
+ }
11548
+ });
11549
+ previousHeadingLevel = level;
11550
+ }
11551
+ };
11552
+ resolved.children.forEach((component, index) => {
11553
+ const rec = component;
11554
+ const page = rec.name === "section" ? pageBox(resolved.theme, context.themeName, rec.props?.page) : basePage;
11555
+ walkActive(component, `/children/${index}`, page, visit);
11556
+ });
11557
+ return {
11558
+ format: "docx",
11559
+ model: {
11560
+ authored: document,
11561
+ context,
11562
+ document: resolved,
11563
+ themeName: context.themeName
11564
+ },
11565
+ facts,
11566
+ provenance,
11567
+ renderer: options.renderer ?? DEFAULT_DOCX_RENDERER_ID2
11568
+ };
11569
+ }
11570
+
11330
11571
  // src/core/generateFromIr.ts
11331
11572
  var UncompiledComponentError = class extends Error {
11332
11573
  code = "UNCOMPILED_COMPONENT";
@@ -11354,11 +11595,14 @@ async function compileDocumentToIr(document, options = {}, collector) {
11354
11595
  );
11355
11596
  }
11356
11597
  async function compileDocumentScoped(document, options, warnings) {
11357
- const context = options.context ?? resolveThemeContext(normalizeDocument(document)[0], {
11598
+ const prepared = options.prepared ?? prepareDocxQualityDocument(document, {
11358
11599
  customThemes: options.customThemes,
11359
11600
  fonts: options.fonts,
11360
- warnings
11601
+ warnings,
11602
+ context: options.context,
11603
+ renderer: options.renderer
11361
11604
  });
11605
+ const { context } = prepared.model;
11362
11606
  const hasVisual = collectVisualProps(context.document).length > 0;
11363
11607
  const resolvedFonts = await resolveDocumentFonts(
11364
11608
  context.document,
@@ -11374,9 +11618,10 @@ async function compileDocumentScoped(document, options, warnings) {
11374
11618
  ...options.baseDir !== void 0 ? { baseDir: options.baseDir } : {},
11375
11619
  ...visualFonts.length > 0 ? { visualFonts } : {}
11376
11620
  });
11377
- const structure = await processDocument(
11621
+ const resolved = desugared === context.document ? prepared.model.document : resolveDocumentTree(desugared, context.theme);
11622
+ const structure = await processResolvedDocument(
11378
11623
  desugared,
11379
- context.theme,
11624
+ resolved,
11380
11625
  context.themeName,
11381
11626
  resolveGenerationDate(options)
11382
11627
  );