@json-to-office/core-docx 2.1.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 || {} },
@@ -4619,34 +4621,36 @@ function validateDocument(document, customComponents, options) {
4619
4621
  const errors = [...documentResult.errors || []];
4620
4622
  function validateComponents(components, pathPrefix = "children") {
4621
4623
  components.forEach((componentData, index) => {
4622
- const customComponent = customComponents.find(
4623
- (cc) => cc.name === componentData.name
4624
- );
4625
- if (!customComponent) {
4624
+ if (!componentData || typeof componentData !== "object" || Array.isArray(componentData)) {
4626
4625
  return;
4627
4626
  }
4628
- const versionEntry = resolveComponentVersion(
4629
- customComponent.name,
4630
- customComponent.versions,
4631
- componentData.version
4632
- );
4633
- const validation = validateComponentProps(
4634
- versionEntry,
4635
- componentData.props,
4636
- customComponent.name,
4637
- // Reject unknown custom props unless the caller allows them.
4638
- { clean: options?.allowUnknownFields === true }
4627
+ const customComponent = customComponents.find(
4628
+ (cc) => cc.name === componentData.name
4639
4629
  );
4640
- if (!validation.valid && validation.errors) {
4641
- const indexedErrors = validation.errors.map(
4642
- (error) => ({
4643
- ...error,
4644
- path: `${pathPrefix}[${index}].${error.path}`
4645
- })
4630
+ if (customComponent) {
4631
+ const versionEntry = resolveComponentVersion(
4632
+ customComponent.name,
4633
+ customComponent.versions,
4634
+ componentData.version
4635
+ );
4636
+ const validation = validateComponentProps(
4637
+ versionEntry,
4638
+ componentData.props,
4639
+ customComponent.name,
4640
+ // Reject unknown custom props unless the caller allows them.
4641
+ { clean: options?.allowUnknownFields === true }
4646
4642
  );
4647
- errors.push(...indexedErrors);
4643
+ if (!validation.valid && validation.errors) {
4644
+ const indexedErrors = validation.errors.map(
4645
+ (error) => ({
4646
+ ...error,
4647
+ path: `${pathPrefix}[${index}].${error.path}`
4648
+ })
4649
+ );
4650
+ errors.push(...indexedErrors);
4651
+ }
4648
4652
  }
4649
- if (componentData.children && Array.isArray(componentData.children)) {
4653
+ if (Array.isArray(componentData.children)) {
4650
4654
  validateComponents(
4651
4655
  componentData.children,
4652
4656
  `${pathPrefix}[${index}].children`
@@ -11475,6 +11479,15 @@ function createRenderContext(document, theme, themeName) {
11475
11479
  }
11476
11480
 
11477
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";
11478
11491
  import { DEFAULT_DOCX_RENDERER_ID as DEFAULT_DOCX_RENDERER_ID2 } from "@json-to-office/shared-docx";
11479
11492
 
11480
11493
  // src/json/normalizer.ts
@@ -11623,6 +11636,164 @@ function tableFact(props, path3, availableWidthTwips) {
11623
11636
  explicitWidths
11624
11637
  };
11625
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
+ }
11626
11797
  var TWIPS_PER_POINT3 = 20;
11627
11798
  var SINGLE_LINE_RATIO = 1.2;
11628
11799
  function finiteNumber(value) {
@@ -11651,20 +11822,23 @@ function characterSpacingPt(spacing) {
11651
11822
  if (rule === void 0 || value === void 0) return 0;
11652
11823
  return rule.type === "condensed" ? -value / TWIPS_PER_POINT3 : value / TWIPS_PER_POINT3;
11653
11824
  }
11654
- function pointerExists(root, pointer) {
11825
+ function nodeAtPointer(root, pointer) {
11655
11826
  let current = root;
11656
11827
  for (const token of pointer.split("/").slice(1)) {
11657
11828
  if (Array.isArray(current)) {
11658
11829
  const index = Number(token);
11659
- if (!Number.isInteger(index)) return false;
11830
+ if (!Number.isInteger(index)) return void 0;
11660
11831
  current = current[index];
11661
11832
  continue;
11662
11833
  }
11663
11834
  const record = asRecord(current);
11664
- if (!record) return false;
11835
+ if (!record) return void 0;
11665
11836
  current = record[token];
11666
11837
  }
11667
- return current !== void 0;
11838
+ return current;
11839
+ }
11840
+ function pointerExists(root, pointer) {
11841
+ return nodeAtPointer(root, pointer) !== void 0;
11668
11842
  }
11669
11843
  function styleKey(node, props) {
11670
11844
  if (node.name === "heading") {
@@ -11845,6 +12019,67 @@ function prepareDocxQualityDocument(document, options = {}) {
11845
12019
  ...fact.relatedPaths && { relatedPaths: fact.relatedPaths }
11846
12020
  };
11847
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
+ });
11848
12083
  let previousVisitPath;
11849
12084
  let lastFrame;
11850
12085
  let flowIndex = 0;
@@ -11854,6 +12089,18 @@ function prepareDocxQualityDocument(document, options = {}) {
11854
12089
  if (node.name === "table") {
11855
12090
  const fact = tableFact(props, path3, page.availableWidthTwips);
11856
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);
11857
12104
  }
11858
12105
  if (node.name === "paragraph" || node.name === "text-box") {
11859
12106
  const floating = asRecord(props.floating);