@json-to-office/core-docx 3.2.0 → 4.0.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.
@@ -193,6 +193,7 @@ var init_minimal_docx_theme = __esm({
193
193
  name: "minimal",
194
194
  displayName: "Minimal Clean",
195
195
  description: "A quiet, warm minimal theme \u2014 Calibri throughout, sage-green ink on ivory neutrals, a large tracked-tight bold title, wide margins and hairline rules",
196
+ whenToUse: "Quiet documents where the type carries the tone: notes, briefs, drafts and internal papers that want warmth without decoration.",
196
197
  version: "4.0.0",
197
198
  colors: {
198
199
  primary: "#2B302B",
@@ -382,6 +383,7 @@ var init_devportal_docx_theme = __esm({
382
383
  name: "devportal",
383
384
  displayName: "Field Editorial",
384
385
  description: "A compact editorial theme \u2014 Helvetica in near-black ink with a burnt-orange accent, tight condensed titles, letterspaced labels, warm tinted fills and hairline rules",
386
+ whenToUse: "Developer and product documentation, API guides and technical handbooks: dense text, code and tables that need compact, letterspaced structure.",
385
387
  version: "4.0.0",
386
388
  colors: {
387
389
  primary: "#12191F",
@@ -580,6 +582,7 @@ var init_vermilion_docx_theme = __esm({
580
582
  name: "vermilion",
581
583
  displayName: "Vermilion Editorial",
582
584
  description: "Poster-red editorial system \u2014 vermilion display headings, ink text, warm creams and hairline rules",
585
+ whenToUse: "Editorial and annual-report work that wants a strong red display voice: covers, feature-style sections, reports built around a few large headings.",
583
586
  version: "1.0.0",
584
587
  colors: {
585
588
  primary: "#282829",
@@ -798,6 +801,7 @@ var init_consulting_docx_theme = __esm({
798
801
  name: "consulting",
799
802
  displayName: "Consulting House",
800
803
  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",
804
+ whenToUse: "Client and technical reports, memos and anything a client will read as the house's own work; the default choice for a report unless the brief names a brand.",
801
805
  version: "1.0.0",
802
806
  colors: {
803
807
  primary: "#1A1F26",
@@ -5307,6 +5311,10 @@ function resolveServiceUrl(propsUrl, servicesUrl, defaultUrl) {
5307
5311
  const withScheme = /^https?:\/\//i.test(raw) ? raw : `http://${raw}`;
5308
5312
  return withScheme.replace(/\/+$/, "");
5309
5313
  }
5314
+ var SERVICE_UNAVAILABLE_CODE = "SERVICE_UNAVAILABLE";
5315
+ function serviceUnavailable(message) {
5316
+ return Object.assign(new Error(message), { code: SERVICE_UNAVAILABLE_CODE });
5317
+ }
5310
5318
  async function postJsonToService(opts) {
5311
5319
  const resolvedHeaders = typeof opts.headers === "function" ? await opts.headers(opts.body) : opts.headers;
5312
5320
  const headers = {
@@ -5326,12 +5334,12 @@ async function postJsonToService(opts) {
5326
5334
  });
5327
5335
  } catch (error) {
5328
5336
  if (error?.name === "AbortError") {
5329
- throw new Error(
5337
+ throw serviceUnavailable(
5330
5338
  `${opts.serviceLabel} timed out after ${timeoutMs}ms at ${opts.url}.`
5331
5339
  );
5332
5340
  }
5333
5341
  const cause = error instanceof Error ? error.message : String(error);
5334
- throw new Error(opts.onUnreachable(opts.url, cause));
5342
+ throw serviceUnavailable(opts.onUnreachable(opts.url, cause));
5335
5343
  } finally {
5336
5344
  clearTimeout(timer);
5337
5345
  }
@@ -12026,6 +12034,7 @@ async function processResolvedDocument(document, resolved, themeName, generation
12026
12034
  };
12027
12035
  }
12028
12036
  function createDocumentMetadata(props, generationDate = /* @__PURE__ */ new Date()) {
12037
+ const parsed = props.metadata?.date ? new Date(props.metadata.date) : void 0;
12029
12038
  return {
12030
12039
  title: props.metadata?.title,
12031
12040
  subtitle: props.metadata?.subtitle,
@@ -12034,7 +12043,7 @@ function createDocumentMetadata(props, generationDate = /* @__PURE__ */ new Date
12034
12043
  company: props.metadata?.company,
12035
12044
  version: props.metadata?.version,
12036
12045
  tags: props.metadata?.tags,
12037
- date: props.metadata?.date ? new Date(props.metadata.date) : generationDate
12046
+ date: parsed && !Number.isNaN(parsed.getTime()) ? parsed : generationDate
12038
12047
  };
12039
12048
  }
12040
12049
  async function extractSections(components, context) {
@@ -12126,7 +12135,12 @@ import {
12126
12135
  normalizeHighchartsChart
12127
12136
  } from "@json-to-office/quality";
12128
12137
  import { DEFAULT_DOCX_RENDERER_ID as DEFAULT_DOCX_RENDERER_ID2 } from "@json-to-office/shared-docx";
12129
- import { designColors as designColors2, resolveDesignColor as resolveDesignColor2 } from "@json-to-office/shared";
12138
+ import {
12139
+ designCanvas as designCanvas2,
12140
+ designColors as designColors2,
12141
+ resolveDesignColor as resolveDesignColor2,
12142
+ typeScaleSizes
12143
+ } from "@json-to-office/shared";
12130
12144
 
12131
12145
  // src/json/normalizer.ts
12132
12146
  function normalizeComponent(component) {
@@ -12895,6 +12909,93 @@ function walkActive(node, path3, page, visit) {
12895
12909
  (child, index) => walkActive(child, `${path3}/children/${index}`, page, visit)
12896
12910
  );
12897
12911
  }
12912
+ function textSizeFact(node, props, path3, typography, role) {
12913
+ const size = effectiveFontSize(node, props, typography);
12914
+ if (size === void 0) return void 0;
12915
+ return {
12916
+ id: `docx:text-size:${path3}`,
12917
+ kind: "docx/text-size",
12918
+ path: path3,
12919
+ role: role ?? styleKey(node, props),
12920
+ fontSizePt: size.fontSizePt,
12921
+ authored: size.authored,
12922
+ ...size.authored && { sizePath: `${path3}/props/font/size` }
12923
+ };
12924
+ }
12925
+ function tableTextSizeFacts(props, authored, path3, typography, page) {
12926
+ const facts = [];
12927
+ const sizeOf = (holder) => finiteNumber(asRecord(asRecord(holder)?.font)?.size);
12928
+ const authoredSize = (holder, pointer, role) => {
12929
+ const size = sizeOf(holder);
12930
+ if (size === void 0) return;
12931
+ facts.push({
12932
+ id: `docx:text-size:${pointer}`,
12933
+ kind: "docx/text-size",
12934
+ path: pointer,
12935
+ role,
12936
+ fontSizePt: size,
12937
+ authored: true,
12938
+ sizePath: `${pointer}/font/size`
12939
+ });
12940
+ };
12941
+ const cellContent = (holder, pointer, role) => {
12942
+ const content = asRecord(holder)?.content;
12943
+ if (asRecord(content) === void 0) return;
12944
+ walkActive(content, `${pointer}/content`, page, (node, nodePath) => {
12945
+ if (node.name !== "paragraph" && node.name !== "heading") return;
12946
+ const nodeProps = asRecord(node.props) ?? {};
12947
+ if (typeof nodeProps.text !== "string" || nodeProps.text.trim() === "")
12948
+ return;
12949
+ const fact = textSizeFact(node, nodeProps, nodePath, typography, role);
12950
+ if (fact) facts.push(fact);
12951
+ });
12952
+ };
12953
+ if (authored) {
12954
+ authoredSize(
12955
+ authored.cellDefaults,
12956
+ `${path3}/props/cellDefaults`,
12957
+ "tableCell"
12958
+ );
12959
+ authoredSize(
12960
+ authored.headerCellDefaults,
12961
+ `${path3}/props/headerCellDefaults`,
12962
+ "tableHeader"
12963
+ );
12964
+ const columns = Array.isArray(authored.columns) ? authored.columns : [];
12965
+ columns.forEach((column, index) => {
12966
+ const record = asRecord(column) ?? {};
12967
+ const columnPath = `${path3}/props/columns/${index}`;
12968
+ authoredSize(
12969
+ record.cellDefaults,
12970
+ `${columnPath}/cellDefaults`,
12971
+ "tableCell"
12972
+ );
12973
+ authoredSize(record.header, `${columnPath}/header`, "tableHeader");
12974
+ cellContent(record.header, `${columnPath}/header`, "tableHeader");
12975
+ const cells = Array.isArray(record.cells) ? record.cells : [];
12976
+ cells.forEach((cell, cellIndex) => {
12977
+ authoredSize(cell, `${columnPath}/cells/${cellIndex}`, "tableCell");
12978
+ cellContent(cell, `${columnPath}/cells/${cellIndex}`, "tableCell");
12979
+ });
12980
+ });
12981
+ }
12982
+ for (const [key, role] of [
12983
+ ["cellDefaults", "tableCell"],
12984
+ ["headerCellDefaults", "tableHeader"]
12985
+ ]) {
12986
+ const size = sizeOf(props[key]);
12987
+ if (size === void 0) continue;
12988
+ facts.push({
12989
+ id: `docx:text-size:${path3}:${role}`,
12990
+ kind: "docx/text-size",
12991
+ path: path3,
12992
+ role,
12993
+ fontSizePt: size,
12994
+ authored: false
12995
+ });
12996
+ }
12997
+ return facts;
12998
+ }
12898
12999
  function prepareDocxQualityDocument(document, options = {}) {
12899
13000
  const themed = options.context ?? resolveThemeContext(normalizeDocument(document)[0], {
12900
13001
  customThemes: options.customThemes,
@@ -12942,6 +13043,75 @@ function prepareDocxQualityDocument(document, options = {}) {
12942
13043
  ...budget
12943
13044
  });
12944
13045
  }
13046
+ for (const role of blockSlotRoles2(themed.document, expanded.blocks)) {
13047
+ addFact({
13048
+ id: `docx:chrome-slot:${role.path}`,
13049
+ kind: "docx/chrome-slot",
13050
+ path: role.path,
13051
+ relatedPaths: [role.invocation],
13052
+ block: role.block,
13053
+ slot: role.slot,
13054
+ role: role.role,
13055
+ present: slotIsFilled(role.value),
13056
+ invocation: role.invocation
13057
+ });
13058
+ }
13059
+ const topLevel = Array.isArray(context.document.children) ? context.document.children : [];
13060
+ let inherited = {};
13061
+ topLevel.forEach((node, index) => {
13062
+ if (node?.name !== "section") return;
13063
+ const props = asRecord(node.props) ?? {};
13064
+ const part = (kind) => props[kind] === "linkToPrevious" ? inherited[kind] : props[kind];
13065
+ const header = part("header");
13066
+ const footer = part("footer");
13067
+ inherited = { header, footer };
13068
+ for (const kind of ["header", "footer"]) {
13069
+ if (!Array.isArray(props[kind])) continue;
13070
+ const drawnByBlock = !Array.isArray(
13071
+ asRecord(
13072
+ asRecord(
13073
+ (Array.isArray(themed.document.children) ? themed.document.children : [])[index]
13074
+ )?.props
13075
+ )?.[kind]
13076
+ );
13077
+ const part2 = props[kind];
13078
+ const visitChrome = (child, childPath) => {
13079
+ if (child.name !== "paragraph" && child.name !== "heading") return;
13080
+ const childProps = asRecord(child.props) ?? {};
13081
+ if (typeof childProps.text !== "string" || childProps.text.trim() === "")
13082
+ return;
13083
+ const fact = textSizeFact(
13084
+ child,
13085
+ childProps,
13086
+ childPath,
13087
+ typography,
13088
+ kind
13089
+ );
13090
+ if (fact)
13091
+ addFact({
13092
+ ...fact,
13093
+ generated: drawnByBlock || authoredPath(childPath) !== childPath
13094
+ });
13095
+ };
13096
+ part2.forEach(
13097
+ (child, childIndex) => walkActive(
13098
+ child,
13099
+ `/children/${index}/props/${kind}/${childIndex}`,
13100
+ basePage,
13101
+ visitChrome
13102
+ )
13103
+ );
13104
+ }
13105
+ addFact({
13106
+ id: `docx:section-chrome:${index}`,
13107
+ kind: "docx/section-chrome",
13108
+ path: `/children/${index}`,
13109
+ index,
13110
+ header: partPresent(header),
13111
+ footer: partPresent(footer),
13112
+ pageNumber: hasPageField(header) || hasPageField(footer)
13113
+ });
13114
+ });
12945
13115
  const paletteHexes = {};
12946
13116
  const visualColors = designColors2(
12947
13117
  resolved.theme.colors,
@@ -12965,12 +13135,34 @@ function prepareDocxQualityDocument(document, options = {}) {
12965
13135
  (value) => `#${resolveDesignColor2(value, visualColors)}`
12966
13136
  ) ?? SERIES_COLOR_TOKENS.filter((token) => paletteHexes[token] !== void 0);
12967
13137
  const authoredPropsAt = (pointer) => asRecord(asRecord(nodeAtPointer(context.document, pointer))?.props);
13138
+ const roleSizesPt = {};
13139
+ for (const key of Object.keys(typography.styles)) {
13140
+ const size = effectiveFontSize(
13141
+ { name: "paragraph" },
13142
+ { themeStyle: key },
13143
+ typography
13144
+ );
13145
+ if (size) roleSizesPt[key] = size.fontSizePt;
13146
+ }
13147
+ const scale = resolved.theme.typography?.scale?.[designCanvas2("docx", resolved.theme.page?.size)];
13148
+ const typeScalePt = [
13149
+ .../* @__PURE__ */ new Set([
13150
+ ...Object.values(roleSizesPt),
13151
+ ...["heading", "body", "mono", "light"].flatMap((role) => {
13152
+ const size = resolveFontSize(resolved.theme, role);
13153
+ return size === void 0 ? [] : [size];
13154
+ }),
13155
+ ...scale ? typeScaleSizes(scale) : []
13156
+ ])
13157
+ ].sort((a, b) => a - b);
12968
13158
  addFact({
12969
13159
  id: "docx:theme",
12970
13160
  kind: "docx/theme",
12971
13161
  path: "/props",
12972
13162
  themeName: context.themeName,
12973
13163
  paletteHexes,
13164
+ typeScalePt,
13165
+ roleSizesPt,
12974
13166
  // `heading` and `body` only. A theme also names `mono` and `light`, but
12975
13167
  // those paint nothing until a component asks for them — counting an
12976
13168
  // unused `Courier New` against a document's family budget would flag a
@@ -13023,6 +13215,15 @@ function prepareDocxQualityDocument(document, options = {}) {
13023
13215
  if (node.name === "table") {
13024
13216
  const fact = tableFact(props, path3, page.availableWidthTwips);
13025
13217
  if (fact) addFact(fact);
13218
+ for (const size of tableTextSizeFacts(
13219
+ props,
13220
+ authoredPropsAt(path3),
13221
+ path3,
13222
+ typography,
13223
+ page
13224
+ )) {
13225
+ addFact({ ...size, generated: authoredPath(size.path) !== size.path });
13226
+ }
13026
13227
  const design = tableDesignFact(
13027
13228
  props,
13028
13229
  path3,
@@ -13081,6 +13282,10 @@ function prepareDocxQualityDocument(document, options = {}) {
13081
13282
  if (node.name === "paragraph" || node.name === "heading") {
13082
13283
  const fact = lineBoxFact(node, props, path3, typography, context.document);
13083
13284
  if (fact) addFact(fact);
13285
+ if (typeof props.text === "string" && props.text.trim() !== "") {
13286
+ const fact2 = textSizeFact(node, props, path3, typography);
13287
+ if (fact2) addFact({ ...fact2, generated: authoredPath(path3) !== path3 });
13288
+ }
13084
13289
  }
13085
13290
  if (node.name === "image" || node.name === "visual") {
13086
13291
  for (const fact of svgTextFacts(props, path3)) addFact(fact);
@@ -13136,6 +13341,17 @@ function prepareDocxQualityDocument(document, options = {}) {
13136
13341
  }
13137
13342
  };
13138
13343
  }
13344
+ function slotIsFilled(value) {
13345
+ if (typeof value === "string") return value.trim() !== "";
13346
+ return value !== void 0 && value !== null && value !== false && (!Array.isArray(value) || value.length > 0);
13347
+ }
13348
+ function partPresent(part) {
13349
+ return Array.isArray(part) && part.length > 0;
13350
+ }
13351
+ var PAGE_FIELD = /\{(PAGE|PAGE_NUMBER|TOTAL_PAGES|NUMPAGES)\}/;
13352
+ function hasPageField(part) {
13353
+ return Array.isArray(part) && PAGE_FIELD.test(JSON.stringify(part));
13354
+ }
13139
13355
 
13140
13356
  // src/core/generateFromIr.ts
13141
13357
  var UncompiledComponentError = class extends Error {