@json-to-office/core-docx 2.5.0 → 2.6.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/dist/index.js CHANGED
@@ -238,43 +238,6 @@ var init_colorUtils = __esm({
238
238
  }
239
239
  });
240
240
 
241
- // src/utils/generationContext.ts
242
- import { AsyncLocalStorage } from "async_hooks";
243
- import { isAbsolute, resolve } from "path";
244
- function runWithBaseDir(baseDir, callback) {
245
- return baseDir === void 0 ? callback() : baseDirStorage.run(resolve(baseDir), callback);
246
- }
247
- function resolveFromBaseDir(filePath) {
248
- const base = baseDirStorage.getStore();
249
- if (!base || isAbsolute(filePath)) return filePath;
250
- return resolve(base, filePath);
251
- }
252
- function runWithWarnings(warnings, callback) {
253
- return warnings === void 0 ? callback() : warningsStorage.run(warnings, callback);
254
- }
255
- function reportWarning(component, code, message, context) {
256
- const warnings = warningsStorage.getStore();
257
- if (warnings) {
258
- warnings.push({
259
- component,
260
- message,
261
- severity: "warning",
262
- context: { code, ...context }
263
- });
264
- return;
265
- }
266
- console.warn(`[json-to-docx] [${code}] ${message}`);
267
- }
268
- var generationDateStorage, baseDirStorage, warningsStorage;
269
- var init_generationContext = __esm({
270
- "src/utils/generationContext.ts"() {
271
- "use strict";
272
- generationDateStorage = new AsyncLocalStorage();
273
- baseDirStorage = new AsyncLocalStorage();
274
- warningsStorage = new AsyncLocalStorage();
275
- }
276
- });
277
-
278
241
  // src/templates/themes/minimal.docx.theme.json
279
242
  var minimal_docx_theme_default;
280
243
  var init_minimal_docx_theme = __esm({
@@ -880,6 +843,320 @@ var init_vermilion_docx_theme = __esm({
880
843
  }
881
844
  });
882
845
 
846
+ // src/templates/themes/consulting.docx.theme.json
847
+ var consulting_docx_theme_default;
848
+ var init_consulting_docx_theme = __esm({
849
+ "src/templates/themes/consulting.docx.theme.json"() {
850
+ consulting_docx_theme_default = {
851
+ name: "consulting",
852
+ displayName: "Consulting House",
853
+ description: "The house style for client and technical reports \u2014 near-black ink, three greys and one deep-blue accent, Calibri body under Arial headings, hairline rules, no fills behind body text, safe fonts only",
854
+ version: "1.0.0",
855
+ colors: {
856
+ primary: "#1A1F26",
857
+ secondary: "#4B5563",
858
+ accent: "#1B4F8A",
859
+ text: "#1A1F26",
860
+ background: "#FFFFFF",
861
+ border: "#C9CED6",
862
+ textPrimary: "#1A1F26",
863
+ textSecondary: "#4B5563",
864
+ textMuted: "#7B8794",
865
+ borderPrimary: "#C9CED6",
866
+ borderSecondary: "#E4E7EB",
867
+ backgroundPrimary: "#FFFFFF",
868
+ backgroundSecondary: "#F2F4F7",
869
+ accent4: "#5B8DC9",
870
+ accent5: "#A9C4E4",
871
+ accent6: "#7B8794"
872
+ },
873
+ palette: {
874
+ rule: "#C9CED6",
875
+ textMuted: "#7B8794",
876
+ onPrimary: "#FFFFFF",
877
+ surfaceInverse: "#1A1F26",
878
+ positive: "#1E7F4F",
879
+ negative: "#B42318",
880
+ chart: ["accent", "secondary", "accent4", "textMuted", "accent5", "rule"]
881
+ },
882
+ fonts: {
883
+ heading: { family: "Arial", size: 16 },
884
+ body: { family: "Calibri", size: 10.5 },
885
+ mono: { family: "Consolas", size: 9.5 },
886
+ light: { family: "Arial", size: 16 }
887
+ },
888
+ page: {
889
+ size: "A4",
890
+ margins: {
891
+ top: 1440,
892
+ bottom: 1440,
893
+ left: 1440,
894
+ right: 1440,
895
+ header: 680,
896
+ footer: 680,
897
+ gutter: 0
898
+ }
899
+ },
900
+ typography: {
901
+ roles: {
902
+ eyebrow: {
903
+ face: "heading",
904
+ size: 8,
905
+ weight: 700,
906
+ case: "upper",
907
+ tracking: 8,
908
+ color: "accent",
909
+ spaceAfter: 4
910
+ },
911
+ display: {
912
+ face: "heading",
913
+ weight: 700,
914
+ color: "primary",
915
+ lineHeight: 1.05,
916
+ spaceAfter: 8
917
+ },
918
+ stat: {
919
+ face: "heading",
920
+ weight: 700,
921
+ color: "accent",
922
+ lineHeight: 1
923
+ },
924
+ quote: {
925
+ face: "body",
926
+ color: "textSecondary",
927
+ lineHeight: 1.3
928
+ },
929
+ label: {
930
+ face: "body",
931
+ size: 9,
932
+ weight: 700,
933
+ color: "textSecondary"
934
+ },
935
+ footer: { face: "body", size: 8, color: "textMuted" },
936
+ tableHeader: {
937
+ face: "body",
938
+ size: 9,
939
+ weight: 700,
940
+ color: "primary"
941
+ },
942
+ tableCell: { face: "body", size: 9.5, color: "textPrimary" },
943
+ chartLabel: {
944
+ face: "body",
945
+ size: 9,
946
+ weight: 400,
947
+ color: "textSecondary"
948
+ },
949
+ tracker: {
950
+ face: "body",
951
+ size: 8,
952
+ case: "upper",
953
+ tracking: 6,
954
+ color: "textMuted"
955
+ },
956
+ source: { face: "body", size: 8, color: "textMuted" }
957
+ },
958
+ scale: {
959
+ a4: { base: 10.5, ratio: 1.2, baselinePt: 2 },
960
+ letter: { base: 10.5, ratio: 1.2, baselinePt: 2 }
961
+ }
962
+ },
963
+ spacing: {
964
+ basePt: 8,
965
+ blockGap: { tight: 4, normal: 8, loose: 16 },
966
+ canvas: {
967
+ a4: { safeAreaIn: 1, gutterIn: 0, columns: 12 },
968
+ letter: { safeAreaIn: 1, gutterIn: 0, columns: 12 }
969
+ }
970
+ },
971
+ chrome: {
972
+ runningHead: {
973
+ type: "tracker",
974
+ color: "textMuted",
975
+ rule: { weightPt: 0.5, color: "rule" },
976
+ alignment: "right"
977
+ },
978
+ tracker: {
979
+ type: "tracker",
980
+ color: "textMuted",
981
+ alignment: "right"
982
+ },
983
+ actionTitle: { type: "display", color: "primary" },
984
+ keyTakeaways: {
985
+ type: "label",
986
+ color: "primary",
987
+ rule: { weightPt: 2, color: "accent" },
988
+ padPt: 8
989
+ },
990
+ sourceLine: { type: "source", color: "textMuted" },
991
+ confidentialFooter: {
992
+ type: "footer",
993
+ color: "textMuted",
994
+ rule: { weightPt: 0.5, color: "rule" },
995
+ alignment: "left"
996
+ },
997
+ logoSlot: { alignment: "right" },
998
+ cover: {
999
+ type: "display",
1000
+ color: "primary",
1001
+ rule: { weightPt: 3, color: "accent" }
1002
+ }
1003
+ },
1004
+ motif: {
1005
+ kind: "rule",
1006
+ color: "accent",
1007
+ weightPt: 3,
1008
+ placement: "top"
1009
+ },
1010
+ styles: {
1011
+ normal: {
1012
+ font: "body",
1013
+ color: "textPrimary",
1014
+ lineSpacing: { type: "multiple", value: 1.2 },
1015
+ alignment: "left",
1016
+ spacing: { after: 8 }
1017
+ },
1018
+ title: {
1019
+ font: "heading",
1020
+ size: 26,
1021
+ bold: true,
1022
+ color: "primary",
1023
+ alignment: "left",
1024
+ lineSpacing: { type: "multiple", value: 1.05 },
1025
+ spacing: { before: 0, after: 6 }
1026
+ },
1027
+ subtitle: {
1028
+ font: "body",
1029
+ size: 13,
1030
+ color: "textSecondary",
1031
+ alignment: "left",
1032
+ lineSpacing: { type: "multiple", value: 1.2 },
1033
+ spacing: { before: 0, after: 18 }
1034
+ },
1035
+ heading1: {
1036
+ font: "heading",
1037
+ size: 16,
1038
+ bold: true,
1039
+ color: "primary",
1040
+ lineSpacing: { type: "multiple", value: 1.1 },
1041
+ spacing: { before: 24, after: 8 },
1042
+ keepNext: true,
1043
+ alignment: "left",
1044
+ borders: {
1045
+ bottom: { style: "single", size: 4, color: "rule", space: 4 }
1046
+ }
1047
+ },
1048
+ heading2: {
1049
+ font: "heading",
1050
+ size: 12.5,
1051
+ bold: true,
1052
+ color: "primary",
1053
+ spacing: { before: 16, after: 6 },
1054
+ keepNext: true,
1055
+ alignment: "left"
1056
+ },
1057
+ heading3: {
1058
+ font: "body",
1059
+ size: 11,
1060
+ bold: true,
1061
+ color: "primary",
1062
+ spacing: { before: 12, after: 4 },
1063
+ keepNext: true,
1064
+ alignment: "left"
1065
+ },
1066
+ heading4: {
1067
+ font: "body",
1068
+ size: 10.5,
1069
+ bold: true,
1070
+ color: "textSecondary",
1071
+ spacing: { before: 10, after: 4 },
1072
+ keepNext: true,
1073
+ alignment: "left"
1074
+ },
1075
+ heading5: {
1076
+ font: "body",
1077
+ size: 10.5,
1078
+ bold: false,
1079
+ italic: true,
1080
+ color: "textSecondary",
1081
+ spacing: { before: 8, after: 4 },
1082
+ keepNext: true,
1083
+ alignment: "left"
1084
+ },
1085
+ heading6: {
1086
+ font: "body",
1087
+ size: 9,
1088
+ bold: true,
1089
+ case: "upper",
1090
+ color: "textMuted",
1091
+ characterSpacing: { type: "expanded", value: 20 },
1092
+ spacing: { before: 8, after: 4 },
1093
+ keepNext: true,
1094
+ alignment: "left"
1095
+ },
1096
+ TOC1: {
1097
+ font: "body",
1098
+ size: 10.5,
1099
+ bold: true,
1100
+ color: "textPrimary",
1101
+ spacing: { before: 4, after: 3 },
1102
+ tabStops: [{ type: "right", position: 9026, leader: "none" }]
1103
+ },
1104
+ TOC2: {
1105
+ font: "body",
1106
+ size: 10.5,
1107
+ color: "textSecondary",
1108
+ indent: { left: 240 },
1109
+ spacing: { after: 2 },
1110
+ tabStops: [{ type: "right", position: 9026, leader: "none" }]
1111
+ },
1112
+ TOC3: {
1113
+ font: "body",
1114
+ size: 10,
1115
+ color: "textMuted",
1116
+ indent: { left: 480 },
1117
+ spacing: { after: 2 },
1118
+ tabStops: [{ type: "right", position: 9026, leader: "none" }]
1119
+ }
1120
+ },
1121
+ componentDefaults: {
1122
+ table: {
1123
+ width: 100,
1124
+ borderColor: "#C9CED6",
1125
+ borderSize: 0.5,
1126
+ hideBorders: { left: true, right: true, insideVertical: true },
1127
+ cellDefaults: {
1128
+ font: { size: 9.5 },
1129
+ padding: { top: 4, bottom: 4 },
1130
+ verticalAlignment: "middle"
1131
+ },
1132
+ headerCellDefaults: {
1133
+ color: "#1A1F26",
1134
+ font: { size: 9, bold: true },
1135
+ padding: { top: 0, bottom: 5 },
1136
+ verticalAlignment: "bottom",
1137
+ borderColor: { bottom: "#1A1F26" },
1138
+ borderSize: { bottom: 0.75 }
1139
+ }
1140
+ },
1141
+ list: {
1142
+ format: "bullet",
1143
+ bullet: "\u2022",
1144
+ spacing: { item: 2 }
1145
+ },
1146
+ image: {
1147
+ alignment: "center"
1148
+ },
1149
+ statistic: {
1150
+ alignment: "left"
1151
+ },
1152
+ section: {
1153
+ pageBreak: false
1154
+ }
1155
+ }
1156
+ };
1157
+ }
1158
+ });
1159
+
883
1160
  // src/templates/themes/index.ts
884
1161
  import fs from "fs";
885
1162
  import path from "path";
@@ -894,7 +1171,8 @@ function loadThemesFromJson() {
894
1171
  const themes2 = {
895
1172
  minimal: ensureThemeDefaults(minimal_docx_theme_default),
896
1173
  devportal: ensureThemeDefaults(devportal_docx_theme_default),
897
- vermilion: ensureThemeDefaults(vermilion_docx_theme_default)
1174
+ vermilion: ensureThemeDefaults(vermilion_docx_theme_default),
1175
+ consulting: ensureThemeDefaults(consulting_docx_theme_default)
898
1176
  };
899
1177
  try {
900
1178
  const __filename = fileURLToPath(import.meta.url);
@@ -932,7 +1210,7 @@ function loadThemesFromJson() {
932
1210
  _themesCache = themes2;
933
1211
  return themes2;
934
1212
  }
935
- var _themesCache, themes, getTheme, getThemeWithFallback, hasTheme, getThemeNames, minimalTheme, devportalTheme, vermilionTheme;
1213
+ var _themesCache, themes, getTheme, getThemeWithFallback, hasTheme, getThemeNames, minimalTheme, devportalTheme, vermilionTheme, consultingTheme;
936
1214
  var init_themes = __esm({
937
1215
  "src/templates/themes/index.ts"() {
938
1216
  "use strict";
@@ -940,6 +1218,7 @@ var init_themes = __esm({
940
1218
  init_minimal_docx_theme();
941
1219
  init_devportal_docx_theme();
942
1220
  init_vermilion_docx_theme();
1221
+ init_consulting_docx_theme();
943
1222
  _themesCache = null;
944
1223
  themes = loadThemesFromJson();
945
1224
  getTheme = (themeName) => {
@@ -963,6 +1242,7 @@ var init_themes = __esm({
963
1242
  minimalTheme = themes["minimal"];
964
1243
  devportalTheme = themes["devportal"];
965
1244
  vermilionTheme = themes["vermilion"];
1245
+ consultingTheme = themes["consulting"];
966
1246
  }
967
1247
  });
968
1248
 
@@ -1279,6 +1559,43 @@ var init_layoutUtils = __esm({
1279
1559
  }
1280
1560
  });
1281
1561
 
1562
+ // src/utils/generationContext.ts
1563
+ import { AsyncLocalStorage } from "async_hooks";
1564
+ import { isAbsolute, resolve } from "path";
1565
+ function runWithBaseDir(baseDir, callback) {
1566
+ return baseDir === void 0 ? callback() : baseDirStorage.run(resolve(baseDir), callback);
1567
+ }
1568
+ function resolveFromBaseDir(filePath) {
1569
+ const base = baseDirStorage.getStore();
1570
+ if (!base || isAbsolute(filePath)) return filePath;
1571
+ return resolve(base, filePath);
1572
+ }
1573
+ function runWithWarnings(warnings, callback) {
1574
+ return warnings === void 0 ? callback() : warningsStorage.run(warnings, callback);
1575
+ }
1576
+ function reportWarning(component, code, message, context) {
1577
+ const warnings = warningsStorage.getStore();
1578
+ if (warnings) {
1579
+ warnings.push({
1580
+ component,
1581
+ message,
1582
+ severity: "warning",
1583
+ context: { code, ...context }
1584
+ });
1585
+ return;
1586
+ }
1587
+ console.warn(`[json-to-docx] [${code}] ${message}`);
1588
+ }
1589
+ var generationDateStorage, baseDirStorage, warningsStorage;
1590
+ var init_generationContext = __esm({
1591
+ "src/utils/generationContext.ts"() {
1592
+ "use strict";
1593
+ generationDateStorage = new AsyncLocalStorage();
1594
+ baseDirStorage = new AsyncLocalStorage();
1595
+ warningsStorage = new AsyncLocalStorage();
1596
+ }
1597
+ });
1598
+
1282
1599
  // src/themes/json/parser.ts
1283
1600
  import { validateThemeJson } from "@json-to-office/shared-docx/validation/unified";
1284
1601
  var ThemeValidationError, ThemeParseError, ThemeParser;
@@ -4688,7 +5005,10 @@ async function resolveDocumentFonts(document, theme, fonts, warnings, forceMater
4688
5005
  }
4689
5006
 
4690
5007
  // src/core/generateFromIr.ts
4691
- import { toRasterizeFontFaces } from "@json-to-office/shared/fonts/node";
5008
+ import {
5009
+ toChartFontFaces,
5010
+ toRasterizeFontFaces
5011
+ } from "@json-to-office/shared/fonts/node";
4692
5012
 
4693
5013
  // src/core/prerasterizeVisuals.ts
4694
5014
  import {
@@ -5106,6 +5426,87 @@ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
5106
5426
  return map;
5107
5427
  }
5108
5428
 
5429
+ // src/core/componentTransform.ts
5430
+ async function transformComponents(doc, replace) {
5431
+ return await walk(doc, replace);
5432
+ }
5433
+ async function walk(node, replace) {
5434
+ if (Array.isArray(node)) {
5435
+ return Promise.all(node.map((child) => walk(child, replace)));
5436
+ }
5437
+ if (!node || typeof node !== "object") return node;
5438
+ const obj = node;
5439
+ if (typeof obj.name === "string" && obj.props) {
5440
+ const replacement = await replace(obj);
5441
+ if (replacement) return replacement;
5442
+ }
5443
+ const next = { ...obj };
5444
+ if (Array.isArray(obj.children)) {
5445
+ next.children = await walk(obj.children, replace);
5446
+ }
5447
+ if (obj.props && typeof obj.props === "object") {
5448
+ const props = obj.props;
5449
+ let changed = false;
5450
+ const nextProps = { ...props };
5451
+ for (const key of ["header", "footer"]) {
5452
+ if (Array.isArray(props[key])) {
5453
+ nextProps[key] = await walk(props[key], replace);
5454
+ changed = true;
5455
+ }
5456
+ }
5457
+ if (Array.isArray(props.columns)) {
5458
+ nextProps.columns = await Promise.all(
5459
+ props.columns.map(
5460
+ (column) => walkColumn(column, replace)
5461
+ )
5462
+ );
5463
+ changed = true;
5464
+ }
5465
+ if (changed) next.props = nextProps;
5466
+ }
5467
+ return next;
5468
+ }
5469
+ async function walkColumn(column, replace) {
5470
+ if (!column || typeof column !== "object") return column;
5471
+ const source = column;
5472
+ const next = { ...source };
5473
+ const header = source.header;
5474
+ if (header && typeof header === "object" && "content" in header) {
5475
+ next.header = { ...header, content: await walk(header.content, replace) };
5476
+ }
5477
+ if (Array.isArray(source.cells)) {
5478
+ next.cells = await Promise.all(
5479
+ source.cells.map(async (cell) => {
5480
+ if (cell && typeof cell === "object" && "content" in cell) {
5481
+ const source2 = cell;
5482
+ return { ...source2, content: await walk(source2.content, replace) };
5483
+ }
5484
+ return cell;
5485
+ })
5486
+ );
5487
+ }
5488
+ return next;
5489
+ }
5490
+ function withNodeIdentity(original, replacement) {
5491
+ const out = { ...replacement };
5492
+ if (original.id !== void 0) out.id = original.id;
5493
+ if (original.enabled !== void 0) out.enabled = original.enabled;
5494
+ return out;
5495
+ }
5496
+ function containsComponent(root, name) {
5497
+ const seen = /* @__PURE__ */ new WeakSet();
5498
+ const visit = (node) => {
5499
+ if (!node || typeof node !== "object" || seen.has(node)) return false;
5500
+ seen.add(node);
5501
+ if (Array.isArray(node)) return node.some(visit);
5502
+ const obj = node;
5503
+ if (typeof obj.name === "string" && obj.enabled === false) return false;
5504
+ if (obj.name === name && obj.props !== void 0) return true;
5505
+ return Object.values(obj).some(visit);
5506
+ };
5507
+ return visit(root);
5508
+ }
5509
+
5109
5510
  // src/core/desugarExternals.ts
5110
5511
  import {
5111
5512
  clampVisualDpi as clampVisualDpi2,
@@ -5117,6 +5518,14 @@ import {
5117
5518
 
5118
5519
  // src/components/highcharts.ts
5119
5520
  init_colorUtils();
5521
+ init_layoutUtils();
5522
+ import {
5523
+ chartFamilyResolver,
5524
+ chartPointsPerPixel,
5525
+ POINTS_PER_PIXEL_96DPI,
5526
+ withChartFontFaceCss,
5527
+ withChartTypography
5528
+ } from "@json-to-office/shared";
5120
5529
  var DEFAULT_EXPORT_SERVER_URL = "http://localhost:7801";
5121
5530
  async function generateChart(config, servicesConfig) {
5122
5531
  if (!isNodeEnvironment()) {
@@ -5180,8 +5589,73 @@ function withThemeColors(config, theme) {
5180
5589
  options: { ...config.options, colors: palette }
5181
5590
  };
5182
5591
  }
5183
- async function renderChartToImageProps(props, theme, servicesConfig) {
5184
- const config = withThemeColors(props, theme);
5592
+ var POINTS_PER_TWIP = 1 / 20;
5593
+ var POINTS_PER_PIXEL = POINTS_PER_PIXEL_96DPI;
5594
+ function placedWidthPt(config, theme) {
5595
+ const { width, height } = config;
5596
+ const chart = config.options.chart;
5597
+ if (typeof width === "number") return width * POINTS_PER_PIXEL;
5598
+ if (typeof width === "string") {
5599
+ const page = getPageSetup(theme);
5600
+ const contentWidthPt = (page.size.width - page.margin.left - page.margin.right) * POINTS_PER_TWIP;
5601
+ return parseFloat(width) / 100 * contentWidthPt;
5602
+ }
5603
+ if (typeof height === "number" && chart.height > 0) {
5604
+ return height / chart.height * chart.width * POINTS_PER_PIXEL;
5605
+ }
5606
+ return void 0;
5607
+ }
5608
+ function toHex(token, theme) {
5609
+ return `#${resolveColor(token, theme)}`;
5610
+ }
5611
+ function themeChartTypography(theme) {
5612
+ const styles = theme.styles ?? {};
5613
+ const family = chartFamilyResolver(theme);
5614
+ const bodyPt = styles.normal?.size ?? theme.fonts.body.size ?? 11;
5615
+ const label = styles.chartLabel;
5616
+ const heading = styles.heading3;
5617
+ const labelPt = label?.size ?? Math.max(bodyPt - 1, 6);
5618
+ return {
5619
+ bodyFamily: family(theme.fonts.body.family),
5620
+ headingFamily: family(theme.fonts.heading.family),
5621
+ textColor: toHex("textPrimary", theme),
5622
+ mutedColor: toHex("textSecondary", theme),
5623
+ labelPt,
5624
+ labelWeight: label?.fontWeight ?? (label?.bold ? 700 : void 0),
5625
+ titlePt: heading?.size ?? bodyPt + 2,
5626
+ titleWeight: heading?.fontWeight ?? (heading?.bold === false ? void 0 : 700),
5627
+ sourcePt: styles.source?.size ?? Math.max(labelPt - 1, 6)
5628
+ };
5629
+ }
5630
+ function withThemeTypography(config, theme) {
5631
+ if (!theme?.fonts?.body?.family || !theme.fonts.heading?.family) {
5632
+ return config;
5633
+ }
5634
+ return {
5635
+ ...config,
5636
+ options: withChartTypography(
5637
+ config.options,
5638
+ themeChartTypography(theme),
5639
+ chartPointsPerPixel(
5640
+ config.options.chart.width,
5641
+ placedWidthPt(config, theme)
5642
+ )
5643
+ )
5644
+ };
5645
+ }
5646
+ function withChartFontFaces(config, theme, faces) {
5647
+ if (!faces?.length || !theme?.fonts) return config;
5648
+ return withChartFontFaceCss(config, faces, [
5649
+ theme.fonts.body.family,
5650
+ theme.fonts.heading.family
5651
+ ]);
5652
+ }
5653
+ async function renderChartToImageProps(props, theme, servicesConfig, chartFonts) {
5654
+ const config = withChartFontFaces(
5655
+ withThemeTypography(withThemeColors(props, theme), theme),
5656
+ theme,
5657
+ chartFonts
5658
+ );
5185
5659
  const chart = await generateChart(config, servicesConfig);
5186
5660
  const hasConfigDimensions = config.width !== void 0 || config.height !== void 0;
5187
5661
  return {
@@ -5192,74 +5666,6 @@ async function renderChartToImageProps(props, theme, servicesConfig) {
5192
5666
  };
5193
5667
  }
5194
5668
 
5195
- // src/core/componentTransform.ts
5196
- async function transformComponents(doc, replace) {
5197
- return await walk(doc, replace);
5198
- }
5199
- async function walk(node, replace) {
5200
- if (Array.isArray(node)) {
5201
- return Promise.all(node.map((child) => walk(child, replace)));
5202
- }
5203
- if (!node || typeof node !== "object") return node;
5204
- const obj = node;
5205
- if (typeof obj.name === "string" && obj.props) {
5206
- const replacement = await replace(obj);
5207
- if (replacement) return replacement;
5208
- }
5209
- const next = { ...obj };
5210
- if (Array.isArray(obj.children)) {
5211
- next.children = await walk(obj.children, replace);
5212
- }
5213
- if (obj.props && typeof obj.props === "object") {
5214
- const props = obj.props;
5215
- let changed = false;
5216
- const nextProps = { ...props };
5217
- for (const key of ["header", "footer"]) {
5218
- if (Array.isArray(props[key])) {
5219
- nextProps[key] = await walk(props[key], replace);
5220
- changed = true;
5221
- }
5222
- }
5223
- if (Array.isArray(props.columns)) {
5224
- nextProps.columns = await Promise.all(
5225
- props.columns.map(
5226
- (column) => walkColumn(column, replace)
5227
- )
5228
- );
5229
- changed = true;
5230
- }
5231
- if (changed) next.props = nextProps;
5232
- }
5233
- return next;
5234
- }
5235
- async function walkColumn(column, replace) {
5236
- if (!column || typeof column !== "object") return column;
5237
- const source = column;
5238
- const next = { ...source };
5239
- const header = source.header;
5240
- if (header && typeof header === "object" && "content" in header) {
5241
- next.header = { ...header, content: await walk(header.content, replace) };
5242
- }
5243
- if (Array.isArray(source.cells)) {
5244
- next.cells = await Promise.all(
5245
- source.cells.map(async (cell) => {
5246
- if (cell && typeof cell === "object" && "content" in cell) {
5247
- const source2 = cell;
5248
- return { ...source2, content: await walk(source2.content, replace) };
5249
- }
5250
- return cell;
5251
- })
5252
- );
5253
- }
5254
- return next;
5255
- }
5256
- function withNodeIdentity(original, replacement) {
5257
- const out = { ...replacement };
5258
- if (original.id !== void 0) out.id = original.id;
5259
- if (original.enabled !== void 0) out.enabled = original.enabled;
5260
- return out;
5261
- }
5262
-
5263
5669
  // src/core/desugarExternals.ts
5264
5670
  async function desugarExternals(document, options) {
5265
5671
  const prerastered = await prerasterizeVisuals(
@@ -5289,7 +5695,8 @@ async function desugarExternals(document, options) {
5289
5695
  props: await renderChartToImageProps(
5290
5696
  node.props,
5291
5697
  options.theme,
5292
- options.services?.highcharts
5698
+ options.services?.highcharts,
5699
+ options.chartFonts
5293
5700
  )
5294
5701
  });
5295
5702
  }
@@ -8047,6 +8454,210 @@ function emptyToUndefined(formatting) {
8047
8454
  return entries.length === 0 ? void 0 : Object.fromEntries(entries);
8048
8455
  }
8049
8456
 
8457
+ // src/blocks/index.ts
8458
+ import { KEY_TAKEAWAYS_BUDGET } from "@json-to-office/shared-docx";
8459
+
8460
+ // src/blocks/keyTakeaways.ts
8461
+ var MIN_RULE_PT = 0.25;
8462
+ var MAX_RULE_PT = 12;
8463
+ var KEY_TAKEAWAYS_DEFAULT_LABEL = "Key takeaways";
8464
+ var FALLBACK_RECIPE = {
8465
+ ruleWeightPt: 1.5,
8466
+ ruleColor: "accent",
8467
+ padPt: 6,
8468
+ type: "label"
8469
+ };
8470
+ function compileKeyTakeaways(props, theme) {
8471
+ const recipe = theme.chrome?.keyTakeaways;
8472
+ const labelRole = recipe?.type ?? FALLBACK_RECIPE.type;
8473
+ const labelStyle = theme.styles?.[labelRole];
8474
+ const ruleWeightPt = Math.min(
8475
+ MAX_RULE_PT,
8476
+ Math.max(
8477
+ MIN_RULE_PT,
8478
+ recipe?.rule?.weightPt ?? FALLBACK_RECIPE.ruleWeightPt
8479
+ )
8480
+ );
8481
+ const ruleColor = recipe?.rule?.color ?? recipe?.color ?? FALLBACK_RECIPE.ruleColor;
8482
+ const padPt = recipe?.padPt ?? FALLBACK_RECIPE.padPt;
8483
+ const children = [
8484
+ {
8485
+ name: "divider",
8486
+ props: {
8487
+ thickness: ruleWeightPt,
8488
+ color: ruleColor,
8489
+ spacing: { before: 12, after: padPt }
8490
+ }
8491
+ },
8492
+ {
8493
+ name: "paragraph",
8494
+ props: {
8495
+ text: props.label ?? KEY_TAKEAWAYS_DEFAULT_LABEL,
8496
+ keepNext: true,
8497
+ spacing: { after: 4 },
8498
+ // The label role when the theme resolves one; a bold run in the
8499
+ // recipe's colour otherwise, so the label still reads as a label.
8500
+ ...labelStyle ? { themeStyle: labelRole } : {
8501
+ font: {
8502
+ bold: true,
8503
+ ...recipe?.color !== void 0 && { color: recipe.color }
8504
+ }
8505
+ }
8506
+ }
8507
+ },
8508
+ {
8509
+ name: "list",
8510
+ props: { items: [...props.items] }
8511
+ },
8512
+ {
8513
+ name: "divider",
8514
+ props: {
8515
+ thickness: 0.5,
8516
+ color: "borderPrimary",
8517
+ spacing: { before: padPt, after: 12 }
8518
+ }
8519
+ }
8520
+ ];
8521
+ return {
8522
+ children,
8523
+ sourceMap: {
8524
+ "/children/0": "",
8525
+ "/children/1": "",
8526
+ "/children/1/props/text": "/props/label",
8527
+ "/children/2": "",
8528
+ "/children/2/props/items": "/props/items",
8529
+ "/children/3": ""
8530
+ }
8531
+ };
8532
+ }
8533
+
8534
+ // src/blocks/index.ts
8535
+ var BLOCK_NAMES = ["key-takeaways"];
8536
+ var BLOCKS = {
8537
+ "key-takeaways": {
8538
+ compile: (props, theme) => compileKeyTakeaways(props, theme),
8539
+ budgets: (props, pointer) => (Array.isArray(props.items) ? props.items : []).flatMap(
8540
+ (item, index) => typeof item === "string" ? [
8541
+ {
8542
+ block: "key-takeaways",
8543
+ slot: "items",
8544
+ path: `${pointer}/props/items/${index}`,
8545
+ words: wordCount(item),
8546
+ maxWords: KEY_TAKEAWAYS_BUDGET.items.maxWords
8547
+ }
8548
+ ] : []
8549
+ )
8550
+ }
8551
+ };
8552
+ function isBlockName(name) {
8553
+ return BLOCK_NAMES.includes(name);
8554
+ }
8555
+ function isRecord(value) {
8556
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8557
+ }
8558
+ function expandBlocks(document, theme) {
8559
+ const sourceMap = {};
8560
+ const blocks = [];
8561
+ const walk2 = (node, pointer) => {
8562
+ if (Array.isArray(node)) {
8563
+ return node.map((child, index) => walk2(child, `${pointer}/${index}`));
8564
+ }
8565
+ if (!isRecord(node)) return node;
8566
+ if (typeof node.name === "string" && isRecord(node.props)) {
8567
+ if (node.enabled !== false && isBlockName(node.name)) {
8568
+ const compiled = BLOCKS[node.name].compile(node.props, theme);
8569
+ blocks.push(pointer);
8570
+ for (const [emitted, authored] of Object.entries(compiled.sourceMap)) {
8571
+ sourceMap[`${pointer}${emitted}`] = `${pointer}${authored}`;
8572
+ }
8573
+ return { ...node, children: compiled.children };
8574
+ }
8575
+ }
8576
+ const next = { ...node };
8577
+ if (isRecord(node.props)) {
8578
+ const props = { ...node.props };
8579
+ let changed = false;
8580
+ for (const key of ["header", "footer"]) {
8581
+ if (Array.isArray(props[key])) {
8582
+ props[key] = walk2(props[key], `${pointer}/props/${key}`);
8583
+ changed = true;
8584
+ }
8585
+ }
8586
+ if (Array.isArray(props.columns)) {
8587
+ props.columns = props.columns.map((column, index) => {
8588
+ if (!isRecord(column)) return column;
8589
+ const base = `${pointer}/props/columns/${index}`;
8590
+ const out = { ...column };
8591
+ if (isRecord(column.header) && "content" in column.header) {
8592
+ out.header = {
8593
+ ...column.header,
8594
+ content: walk2(column.header.content, `${base}/header/content`)
8595
+ };
8596
+ }
8597
+ if (Array.isArray(column.cells)) {
8598
+ out.cells = column.cells.map(
8599
+ (cell, cellIndex) => isRecord(cell) && "content" in cell ? {
8600
+ ...cell,
8601
+ content: walk2(
8602
+ cell.content,
8603
+ `${base}/cells/${cellIndex}/content`
8604
+ )
8605
+ } : cell
8606
+ );
8607
+ }
8608
+ return out;
8609
+ });
8610
+ changed = true;
8611
+ }
8612
+ if (changed) next.props = props;
8613
+ }
8614
+ if (Array.isArray(node.children)) {
8615
+ next.children = walk2(node.children, `${pointer}/children`);
8616
+ }
8617
+ return next;
8618
+ };
8619
+ const expanded = walk2(document, "");
8620
+ return {
8621
+ document: blocks.length > 0 ? expanded : document,
8622
+ sourceMap,
8623
+ blocks
8624
+ };
8625
+ }
8626
+ function toAuthoredPointer(sourceMap, pointer) {
8627
+ let best;
8628
+ for (const emitted of Object.keys(sourceMap)) {
8629
+ if ((pointer === emitted || pointer.startsWith(`${emitted}/`)) && (best === void 0 || emitted.length > best.length)) {
8630
+ best = emitted;
8631
+ }
8632
+ }
8633
+ if (best === void 0) return pointer;
8634
+ return `${sourceMap[best]}${pointer.slice(best.length)}`;
8635
+ }
8636
+ function blockSlotBudgets(document, blocks) {
8637
+ return blocks.flatMap((pointer) => {
8638
+ const node = nodeAt(document, pointer);
8639
+ if (!isRecord(node) || !isRecord(node.props) || !isBlockName(node.name)) {
8640
+ return [];
8641
+ }
8642
+ return BLOCKS[node.name].budgets(node.props, pointer);
8643
+ });
8644
+ }
8645
+ function wordCount(text) {
8646
+ const trimmed = text.trim();
8647
+ return trimmed === "" ? 0 : trimmed.split(/\s+/).length;
8648
+ }
8649
+ function nodeAt(root, pointer) {
8650
+ if (pointer === "") return root;
8651
+ let current = root;
8652
+ for (const segment of pointer.slice(1).split("/")) {
8653
+ const key = segment.replace(/~1/g, "/").replace(/~0/g, "~");
8654
+ if (Array.isArray(current)) current = current[Number(key)];
8655
+ else if (isRecord(current)) current = current[key];
8656
+ else return void 0;
8657
+ }
8658
+ return current;
8659
+ }
8660
+
8050
8661
  // src/ir/compiler.ts
8051
8662
  init_types();
8052
8663
  init_units();
@@ -8458,6 +9069,10 @@ function compileComponent(component, scope) {
8458
9069
  case "table":
8459
9070
  return compileTable(component, scope);
8460
9071
  default:
9072
+ if (isBlockName(component.name)) {
9073
+ if (component.enabled === false) return [];
9074
+ return compileBlock(component, scope);
9075
+ }
8461
9076
  scope.ctx.unsupported.push({
8462
9077
  name: component.name,
8463
9078
  path: scope.path
@@ -8465,6 +9080,20 @@ function compileComponent(component, scope) {
8465
9080
  return [];
8466
9081
  }
8467
9082
  }
9083
+ function compileBlock(component, scope) {
9084
+ const children = component.children ?? [];
9085
+ if (children.length === 0) {
9086
+ scope.ctx.unsupported.push({ name: component.name, path: scope.path });
9087
+ return [];
9088
+ }
9089
+ return children.flatMap(
9090
+ (child, index) => compileComponent(child, {
9091
+ ...scope,
9092
+ path: `${scope.path}.children[${index}]`,
9093
+ id: `${scope.id}:b${index}`
9094
+ })
9095
+ );
9096
+ }
8468
9097
  var MARKDOWN_LIST_LEVELS = {
8469
9098
  unordered: [
8470
9099
  { level: 0, format: "bullet", text: "\u2022", alignment: "left" },
@@ -12071,11 +12700,13 @@ function walkActive(node, path4, page, visit) {
12071
12700
  );
12072
12701
  }
12073
12702
  function prepareDocxQualityDocument(document, options = {}) {
12074
- const context = options.context ?? resolveThemeContext(normalizeDocument(document)[0], {
12703
+ const themed = options.context ?? resolveThemeContext(normalizeDocument(document)[0], {
12075
12704
  customThemes: options.customThemes,
12076
12705
  fonts: options.fonts,
12077
12706
  warnings: options.warnings
12078
12707
  });
12708
+ const expanded = expandBlocks(themed.document, themed.theme);
12709
+ const context = expanded.blocks.length > 0 ? { ...themed, document: expanded.document } : themed;
12079
12710
  const resolved = resolveDocumentTree(context.document, context.theme);
12080
12711
  const basePage = pageBox(resolved.theme, context.themeName);
12081
12712
  const typography = {
@@ -12085,13 +12716,28 @@ function prepareDocxQualityDocument(document, options = {}) {
12085
12716
  const facts = [];
12086
12717
  const provenance = {};
12087
12718
  let previousHeadingLevel;
12088
- const addFact = (fact) => {
12719
+ const authoredPath = (path4) => toAuthoredPointer(expanded.sourceMap, path4);
12720
+ const addFact = (raw) => {
12721
+ const fact = {
12722
+ ...raw,
12723
+ path: authoredPath(raw.path),
12724
+ ...raw.relatedPaths && {
12725
+ relatedPaths: raw.relatedPaths.map(authoredPath)
12726
+ }
12727
+ };
12089
12728
  facts.push(fact);
12090
12729
  provenance[fact.id] = {
12091
12730
  path: fact.path,
12092
12731
  ...fact.relatedPaths && { relatedPaths: fact.relatedPaths }
12093
12732
  };
12094
12733
  };
12734
+ for (const budget of blockSlotBudgets(context.document, expanded.blocks)) {
12735
+ addFact({
12736
+ id: `docx:block-slot:${budget.path}`,
12737
+ kind: "docx/block-slot",
12738
+ ...budget
12739
+ });
12740
+ }
12095
12741
  const paletteHexes = {};
12096
12742
  const visualColors = designColors2(
12097
12743
  resolved.theme.colors,
@@ -12234,7 +12880,16 @@ function prepareDocxQualityDocument(document, options = {}) {
12234
12880
  },
12235
12881
  facts,
12236
12882
  provenance,
12237
- renderer: options.renderer ?? DEFAULT_DOCX_RENDERER_ID2
12883
+ renderer: options.renderer ?? DEFAULT_DOCX_RENDERER_ID2,
12884
+ ...expanded.blocks.length > 0 && {
12885
+ metadata: {
12886
+ blocks: {
12887
+ sourceMap: expanded.sourceMap,
12888
+ blocks: expanded.blocks,
12889
+ document: context.document
12890
+ }
12891
+ }
12892
+ }
12238
12893
  };
12239
12894
  }
12240
12895
 
@@ -12274,19 +12929,22 @@ async function compileDocumentScoped(document, options, warnings) {
12274
12929
  });
12275
12930
  const { context } = prepared.model;
12276
12931
  const hasVisual = collectVisualProps(context.document).length > 0;
12932
+ const hasHighcharts = containsComponent(context.document, "highcharts");
12277
12933
  const resolvedFonts = await resolveDocumentFonts(
12278
12934
  context.document,
12279
12935
  context.theme,
12280
12936
  options.fonts,
12281
12937
  warnings,
12282
- hasVisual
12938
+ hasVisual || hasHighcharts
12283
12939
  );
12284
12940
  const visualFonts = hasVisual ? toRasterizeFontFaces(resolvedFonts, warnings) : [];
12941
+ const chartFonts = hasHighcharts ? toChartFontFaces(resolvedFonts) : [];
12285
12942
  const desugared = await desugarExternals(context.document, {
12286
12943
  theme: context.theme,
12287
12944
  ...options.services ? { services: options.services } : {},
12288
12945
  ...options.baseDir !== void 0 ? { baseDir: options.baseDir } : {},
12289
- ...visualFonts.length > 0 ? { visualFonts } : {}
12946
+ ...visualFonts.length > 0 ? { visualFonts } : {},
12947
+ ...chartFonts.length > 0 ? { chartFonts } : {}
12290
12948
  });
12291
12949
  const resolved = desugared === context.document ? prepared.model.document : resolveDocumentTree(desugared, context.theme);
12292
12950
  const structure = await processResolvedDocument(
@@ -13102,6 +13760,28 @@ var docxPlaceholderRule = {
13102
13760
  })
13103
13761
  )
13104
13762
  };
13763
+ var docxSlotBudgetRule = {
13764
+ id: "docx/slot-budget",
13765
+ code: QUALITY_CODES.SLOT_BUDGET,
13766
+ category: "composition",
13767
+ defaultSeverity: "warning",
13768
+ defaultCertainty: "deterministic",
13769
+ formats: ["docx"],
13770
+ evaluate: ({ facts }) => facts.filter(
13771
+ (fact) => fact.kind === "docx/block-slot"
13772
+ ).filter((fact) => fact.words > fact.maxWords).map((fact) => ({
13773
+ path: fact.path,
13774
+ message: `${fact.block} ${fact.slot} entry runs to ${fact.words} words; the slot holds ${fact.maxWords} \u2014 one claim, one sentence.`,
13775
+ suggestion: "Cut it to the conclusion. Move the evidence into the body it summarises.",
13776
+ evidence: {
13777
+ summary: "words in the slot against its budget",
13778
+ actual: fact.words,
13779
+ expected: fact.maxWords,
13780
+ unit: "words"
13781
+ },
13782
+ context: { block: fact.block, slot: fact.slot }
13783
+ }))
13784
+ };
13105
13785
  var DEFAULT_MAX_FONT_FAMILIES = 3;
13106
13786
  var docxFontCountRule = {
13107
13787
  id: "docx/font-count",
@@ -13267,6 +13947,7 @@ var DOCX_QUALITY_RULES = {
13267
13947
  docxSvgTextBoundsRule,
13268
13948
  docxLineBoxRule,
13269
13949
  docxPlaceholderRule,
13950
+ docxSlotBudgetRule,
13270
13951
  docxChartRule,
13271
13952
  docxTableDesignRule,
13272
13953
  docxFontCountRule,
@@ -14274,6 +14955,7 @@ function getCoreVersion() {
14274
14955
  return "Core v1.0.0";
14275
14956
  }
14276
14957
  export {
14958
+ BLOCK_NAMES,
14277
14959
  ComponentValidationError2 as ComponentValidationError,
14278
14960
  DocumentGenerator as CoreDocumentGenerator,
14279
14961
  DEFAULT_DOCX_RENDERER_ID,
@@ -14286,7 +14968,10 @@ export {
14286
14968
  UncompiledComponentError,
14287
14969
  UnknownPreservedComponentError,
14288
14970
  analyzeDocxQuality,
14971
+ blockSlotBudgets,
14289
14972
  cleanComponentProps,
14973
+ compileKeyTakeaways,
14974
+ consultingTheme,
14290
14975
  createComponent,
14291
14976
  createDocumentGenerator,
14292
14977
  createMinimalTheme,
@@ -14296,6 +14981,7 @@ export {
14296
14981
  docxRendererIds,
14297
14982
  docxRendererStatuses,
14298
14983
  examples,
14984
+ expandBlocks,
14299
14985
  exportPluginSchema,
14300
14986
  exportThemeToJson,
14301
14987
  flattenVisuals,
@@ -14339,6 +15025,7 @@ export {
14339
15025
  resolveDocxQualityProfile,
14340
15026
  runExample,
14341
15027
  themes,
15028
+ toAuthoredPointer,
14342
15029
  validateComponentProps,
14343
15030
  validateDocument,
14344
15031
  validateJsonSchema,