@json-to-office/core-docx 2.3.0 → 2.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.
@@ -11,6 +11,7 @@ var __export = (target, all) => {
11
11
  // src/themes/defaults.ts
12
12
  function ensureThemeDefaults(theme) {
13
13
  return {
14
+ ...theme,
14
15
  $schema: theme.$schema,
15
16
  name: theme.name || "default",
16
17
  displayName: theme.displayName || "Default Theme",
@@ -21,6 +22,7 @@ function ensureThemeDefaults(theme) {
21
22
  ...theme.colors || {}
22
23
  },
23
24
  fonts: {
25
+ ...theme.fonts || {},
24
26
  heading: { ...DEFAULT_FONTS.heading, ...theme.fonts?.heading || {} },
25
27
  body: { ...DEFAULT_FONTS.body, ...theme.fonts?.body || {} },
26
28
  mono: { ...DEFAULT_FONTS.mono, ...theme.fonts?.mono || {} },
@@ -11477,6 +11479,15 @@ function createRenderContext(document, theme, themeName) {
11477
11479
  }
11478
11480
 
11479
11481
  // src/quality/facts.ts
11482
+ import {
11483
+ chartEncodingFor,
11484
+ collectColorLiterals,
11485
+ collectFontFamilies,
11486
+ collectPlaceholders,
11487
+ hasUnitMarker,
11488
+ normalizeHex,
11489
+ normalizeHighchartsChart
11490
+ } from "@json-to-office/quality";
11480
11491
  import { DEFAULT_DOCX_RENDERER_ID as DEFAULT_DOCX_RENDERER_ID2 } from "@json-to-office/shared-docx";
11481
11492
 
11482
11493
  // src/json/normalizer.ts
@@ -11625,6 +11636,164 @@ function tableFact(props, path3, availableWidthTwips) {
11625
11636
  explicitWidths
11626
11637
  };
11627
11638
  }
11639
+ var SERIES_COLOR_TOKENS = [
11640
+ "primary",
11641
+ "accent",
11642
+ "secondary",
11643
+ "accent4",
11644
+ "accent5",
11645
+ "accent6"
11646
+ ];
11647
+ function chartFact(node, props, path3, paletteTokens) {
11648
+ const base = {
11649
+ id: `docx:chart:${path3}`,
11650
+ kind: "docx/chart",
11651
+ path: path3,
11652
+ componentName: typeof node.name === "string" ? node.name : "",
11653
+ paletteTokens
11654
+ };
11655
+ if (node.name === "highcharts") {
11656
+ const shape = normalizeHighchartsChart(props.options);
11657
+ return {
11658
+ ...base,
11659
+ chartType: shape.chartType,
11660
+ encoding: shape.encoding,
11661
+ threeD: shape.threeD,
11662
+ seriesCount: shape.seriesCount,
11663
+ categoryCount: shape.categoryCount,
11664
+ seriesColorsStated: shape.seriesColorsStated,
11665
+ seriesColorsPath: `${path3}/props/options`,
11666
+ ...shape.valueAxisMin !== void 0 && {
11667
+ valueAxisMin: shape.valueAxisMin
11668
+ },
11669
+ unitStated: shape.unitStated,
11670
+ annotation: {
11671
+ stated: shape.annotationStated,
11672
+ path: path3,
11673
+ slot: "props.options.caption.text"
11674
+ }
11675
+ };
11676
+ }
11677
+ const chartType = typeof props.type === "string" ? props.type : "";
11678
+ if (chartType === "") return void 0;
11679
+ const series = Array.isArray(props.data) ? props.data : [];
11680
+ const categoryCount = Math.max(
11681
+ 0,
11682
+ ...series.map((entry) => {
11683
+ const record = asRecord(entry);
11684
+ const labels = Array.isArray(record?.labels) ? record.labels.length : 0;
11685
+ const values = Array.isArray(record?.values) ? record.values.length : 0;
11686
+ return Math.max(labels, values);
11687
+ })
11688
+ );
11689
+ const caption = typeof props.caption === "string" ? props.caption : "";
11690
+ const title = typeof props.title === "string" ? props.title : "";
11691
+ return {
11692
+ ...base,
11693
+ chartType,
11694
+ encoding: chartEncodingFor(chartType),
11695
+ // No 3D type exists in the DOCX chart schema — `office-open` draws none —
11696
+ // so this is always false here, and stays a field rather than an omission
11697
+ // so both formats answer the same questions.
11698
+ threeD: false,
11699
+ seriesCount: series.length,
11700
+ categoryCount,
11701
+ seriesColorsStated: Array.isArray(props.chartColors) && props.chartColors.length > 0,
11702
+ seriesColorsPath: `${path3}/props/chartColors`,
11703
+ // No `valueAxisMin`: a DOCX chart has no axis-floor property, so the
11704
+ // baseline is always the renderer's, which starts a bar at zero.
11705
+ unitStated: hasUnitMarker(
11706
+ typeof props.valAxisTitle === "string" ? props.valAxisTitle : ""
11707
+ ) || hasUnitMarker(title) || hasUnitMarker(caption),
11708
+ annotation: {
11709
+ stated: caption.trim() !== "",
11710
+ path: path3,
11711
+ slot: "props.caption"
11712
+ }
11713
+ };
11714
+ }
11715
+ var DOCX_ALIGNMENTS = /* @__PURE__ */ new Set(["left", "center", "right", "justify"]);
11716
+ function resolvedCellText(content) {
11717
+ return typeof content === "string" ? content : "[component]";
11718
+ }
11719
+ function statesOwnBorders(authored) {
11720
+ if (!authored) return false;
11721
+ if (authored.borderSize !== void 0 || authored.borderColor !== void 0 || authored.hideBorders !== void 0) {
11722
+ return true;
11723
+ }
11724
+ const layers = [
11725
+ asRecord(authored.cellDefaults),
11726
+ asRecord(authored.headerCellDefaults)
11727
+ ];
11728
+ if (layers.some(
11729
+ (layer) => layer?.borderSize !== void 0 || layer?.borderColor !== void 0
11730
+ )) {
11731
+ return true;
11732
+ }
11733
+ const columns = Array.isArray(authored.columns) ? authored.columns : [];
11734
+ return columns.some((column) => {
11735
+ const record = asRecord(column);
11736
+ const defaults = asRecord(record?.cellDefaults);
11737
+ return defaults?.borderSize !== void 0 || defaults?.borderColor !== void 0;
11738
+ });
11739
+ }
11740
+ function tableDesignFact(props, path3, theme, themeName, authored) {
11741
+ const authoredColumns = Array.isArray(props.columns) ? props.columns : [];
11742
+ if (authoredColumns.length === 0) return void 0;
11743
+ const model = resolveTableModel(
11744
+ props,
11745
+ theme,
11746
+ themeName
11747
+ );
11748
+ const bodyRows = model.rows;
11749
+ const drawnRows = bodyRows.length + (model.header ? 1 : 0);
11750
+ if (drawnRows === 0) return void 0;
11751
+ const fullGrid = statesOwnBorders(authored) && [...model.header ? [model.header] : [], ...bodyRows].every(
11752
+ (row) => row.cells.every(
11753
+ (cell) => ["top", "right", "bottom", "left"].every(
11754
+ (side) => !cell.borders[side].hidden && cell.borders[side].size > 0
11755
+ )
11756
+ )
11757
+ );
11758
+ const columns = authoredColumns.map(
11759
+ (column, index) => {
11760
+ const authoredColumn = asRecord(column) ?? {};
11761
+ const cells = Array.isArray(authoredColumn.cells) ? authoredColumn.cells : [];
11762
+ const alignments = /* @__PURE__ */ new Set();
11763
+ const values = [];
11764
+ bodyRows.forEach((row) => {
11765
+ const cell = row.cells[index];
11766
+ if (!cell || cell.missing) return;
11767
+ values.push(resolvedCellText(cell.content));
11768
+ alignments.add(cell.horizontalAlignment);
11769
+ });
11770
+ const header = model.header?.cells[index];
11771
+ return {
11772
+ index,
11773
+ path: `${path3}/props/columns/${index}`,
11774
+ ...header?.content !== void 0 && {
11775
+ header: resolvedCellText(header.content)
11776
+ },
11777
+ values,
11778
+ alignment: alignments.size === 1 ? [...alignments][0] : alignments.size === 0 ? "left" : "mixed",
11779
+ hasCellDefaults: asRecord(authoredColumn.cellDefaults) !== void 0,
11780
+ hasHeader: asRecord(authoredColumn.header) !== void 0,
11781
+ cellsWithOwnAlignment: cells.flatMap((cell, cellIndex) => {
11782
+ const alignment2 = asRecord(cell)?.horizontalAlignment;
11783
+ return typeof alignment2 === "string" && DOCX_ALIGNMENTS.has(alignment2) && alignment2 !== "right" ? [cellIndex] : [];
11784
+ })
11785
+ };
11786
+ }
11787
+ );
11788
+ return {
11789
+ id: `docx:table-design:${path3}`,
11790
+ kind: "docx/table",
11791
+ path: path3,
11792
+ columns,
11793
+ rowCount: drawnRows,
11794
+ fullGrid
11795
+ };
11796
+ }
11628
11797
  var TWIPS_PER_POINT3 = 20;
11629
11798
  var SINGLE_LINE_RATIO = 1.2;
11630
11799
  function finiteNumber(value) {
@@ -11653,20 +11822,23 @@ function characterSpacingPt(spacing) {
11653
11822
  if (rule === void 0 || value === void 0) return 0;
11654
11823
  return rule.type === "condensed" ? -value / TWIPS_PER_POINT3 : value / TWIPS_PER_POINT3;
11655
11824
  }
11656
- function pointerExists(root, pointer) {
11825
+ function nodeAtPointer(root, pointer) {
11657
11826
  let current = root;
11658
11827
  for (const token of pointer.split("/").slice(1)) {
11659
11828
  if (Array.isArray(current)) {
11660
11829
  const index = Number(token);
11661
- if (!Number.isInteger(index)) return false;
11830
+ if (!Number.isInteger(index)) return void 0;
11662
11831
  current = current[index];
11663
11832
  continue;
11664
11833
  }
11665
11834
  const record = asRecord(current);
11666
- if (!record) return false;
11835
+ if (!record) return void 0;
11667
11836
  current = record[token];
11668
11837
  }
11669
- return current !== void 0;
11838
+ return current;
11839
+ }
11840
+ function pointerExists(root, pointer) {
11841
+ return nodeAtPointer(root, pointer) !== void 0;
11670
11842
  }
11671
11843
  function styleKey(node, props) {
11672
11844
  if (node.name === "heading") {
@@ -11847,6 +12019,67 @@ function prepareDocxQualityDocument(document, options = {}) {
11847
12019
  ...fact.relatedPaths && { relatedPaths: fact.relatedPaths }
11848
12020
  };
11849
12021
  };
12022
+ const paletteHexes = {};
12023
+ for (const [token, value] of Object.entries(
12024
+ resolved.theme.colors ?? {}
12025
+ )) {
12026
+ if (typeof value !== "string") continue;
12027
+ const hex = normalizeHex(value);
12028
+ if (hex) paletteHexes[token] = hex;
12029
+ }
12030
+ const paletteTokens = SERIES_COLOR_TOKENS.filter(
12031
+ (token) => paletteHexes[token] !== void 0
12032
+ );
12033
+ const authoredPropsAt = (pointer) => asRecord(asRecord(nodeAtPointer(context.document, pointer))?.props);
12034
+ addFact({
12035
+ id: "docx:theme",
12036
+ kind: "docx/theme",
12037
+ path: "/props",
12038
+ themeName: context.themeName,
12039
+ paletteHexes,
12040
+ // `heading` and `body` only. A theme also names `mono` and `light`, but
12041
+ // those paint nothing until a component asks for them — counting an
12042
+ // unused `Courier New` against a document's family budget would flag a
12043
+ // report that only ever uses one typeface.
12044
+ fontFamilies: [
12045
+ ...new Set(
12046
+ ["heading", "body"].flatMap((role) => {
12047
+ const family = asRecord(
12048
+ resolved.theme.fonts?.[role]
12049
+ )?.family;
12050
+ return typeof family === "string" && family.trim() !== "" ? [family] : [];
12051
+ })
12052
+ )
12053
+ ]
12054
+ });
12055
+ collectColorLiterals(document).forEach((literal, index) => {
12056
+ addFact({
12057
+ id: `docx:color:${index}:${literal.path}`,
12058
+ kind: "docx/color",
12059
+ path: literal.path,
12060
+ raw: literal.raw,
12061
+ hex: literal.hex
12062
+ });
12063
+ });
12064
+ collectFontFamilies(document).forEach((use, index) => {
12065
+ addFact({
12066
+ id: `docx:font:${index}:${use.path}`,
12067
+ kind: "docx/font-family",
12068
+ path: use.path,
12069
+ family: use.family
12070
+ });
12071
+ });
12072
+ collectPlaceholders(document).forEach((occurrence, index) => {
12073
+ addFact({
12074
+ id: `docx:placeholder:${index}:${occurrence.path}`,
12075
+ kind: "docx/placeholder",
12076
+ path: occurrence.path,
12077
+ text: occurrence.text,
12078
+ placeholderKind: occurrence.match.kind,
12079
+ pattern: occurrence.match.pattern,
12080
+ excerpt: occurrence.match.excerpt
12081
+ });
12082
+ });
11850
12083
  let previousVisitPath;
11851
12084
  let lastFrame;
11852
12085
  let flowIndex = 0;
@@ -11856,6 +12089,18 @@ function prepareDocxQualityDocument(document, options = {}) {
11856
12089
  if (node.name === "table") {
11857
12090
  const fact = tableFact(props, path3, page.availableWidthTwips);
11858
12091
  if (fact) addFact(fact);
12092
+ const design = tableDesignFact(
12093
+ props,
12094
+ path3,
12095
+ resolved.theme,
12096
+ context.themeName,
12097
+ authoredPropsAt(path3)
12098
+ );
12099
+ if (design) addFact(design);
12100
+ }
12101
+ if (node.name === "chart" || node.name === "highcharts") {
12102
+ const fact = chartFact(node, props, path3, paletteTokens);
12103
+ if (fact) addFact(fact);
11859
12104
  }
11860
12105
  if (node.name === "paragraph" || node.name === "text-box") {
11861
12106
  const floating = asRecord(props.floating);