@json-to-office/core-docx 0.27.0 → 0.29.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.
@@ -27,6 +27,12 @@ export interface DocumentGeneratorOptions {
27
27
  deterministic?: boolean;
28
28
  /** Default build timestamp for generated metadata. */
29
29
  generatedAt?: string | Date;
30
+ /**
31
+ * Directory that relative asset paths (image `path` props) resolve against.
32
+ * Per-call `options.baseDir` overrides it; defaults to `process.cwd()`
33
+ * when neither is set (#142).
34
+ */
35
+ baseDir?: string;
30
36
  }
31
37
  /**
32
38
  * Create a document generator with chainable component registration.
@@ -1 +1 @@
1
- {"version":3,"file":"createDocumentGenerator.d.ts","sourceRoot":"","sources":["../../src/plugin/createDocumentGenerator.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAK7C,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9E,OAAO,KAAK,EAEV,wBAAwB,EAOxB,2BAA2B,EAC5B,MAAM,SAAS,CAAC;AAmBjB;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC3C,4CAA4C;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,oFAAoF;IACpF,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB;;;;OAIG;IACH,UAAU,CAAC,EAAE,2BAA2B,CAAC;IACzC,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAsoBD;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,wBAAwB,GAChC,wBAAwB,CAAC,SAAS,EAAE,CAAC,CAgBvC"}
1
+ {"version":3,"file":"createDocumentGenerator.d.ts","sourceRoot":"","sources":["../../src/plugin/createDocumentGenerator.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAK7C,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9E,OAAO,KAAK,EAEV,wBAAwB,EAOxB,2BAA2B,EAC5B,MAAM,SAAS,CAAC;AAmBjB;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC3C,4CAA4C;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,oFAAoF;IACpF,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB;;;;OAIG;IACH,UAAU,CAAC,EAAE,2BAA2B,CAAC;IACzC,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAyoBD;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,wBAAwB,GAChC,wBAAwB,CAAC,SAAS,EAAE,CAAC,CAiBvC"}
@@ -2870,9 +2870,34 @@ import {
2870
2870
  } from "docx";
2871
2871
 
2872
2872
  // src/utils/imageUtils.ts
2873
- init_widthUtils();
2874
2873
  import { readFileSync } from "fs";
2875
2874
  import probe from "probe-image-size";
2875
+
2876
+ // src/utils/generationContext.ts
2877
+ import { AsyncLocalStorage } from "async_hooks";
2878
+ import { isAbsolute, resolve } from "path";
2879
+ var generationDateStorage = new AsyncLocalStorage();
2880
+ function runWithGenerationDate(date, callback) {
2881
+ return generationDateStorage.run(date, callback);
2882
+ }
2883
+ function getGenerationDate() {
2884
+ return generationDateStorage.getStore() ?? /* @__PURE__ */ new Date();
2885
+ }
2886
+ var baseDirStorage = new AsyncLocalStorage();
2887
+ function runWithBaseDir(baseDir, callback) {
2888
+ return baseDir === void 0 ? callback() : baseDirStorage.run(resolve(baseDir), callback);
2889
+ }
2890
+ function getBaseDir() {
2891
+ return baseDirStorage.getStore();
2892
+ }
2893
+ function resolveFromBaseDir(filePath) {
2894
+ const base = baseDirStorage.getStore();
2895
+ if (!base || isAbsolute(filePath)) return filePath;
2896
+ return resolve(base, filePath);
2897
+ }
2898
+
2899
+ // src/utils/imageUtils.ts
2900
+ init_widthUtils();
2876
2901
  import { ImageRun } from "docx";
2877
2902
  function parseWidthValue(width, availableWidthPx) {
2878
2903
  if (typeof width === "number") {
@@ -3048,7 +3073,7 @@ async function getImageBuffer(imagePath) {
3048
3073
  if (isValidUrl(imagePath)) {
3049
3074
  return await downloadImageFromUrl(imagePath);
3050
3075
  }
3051
- return { buffer: readFileSync(imagePath) };
3076
+ return { buffer: readFileSync(resolveFromBaseDir(imagePath)) };
3052
3077
  }
3053
3078
  async function getImageDimensions(imagePath) {
3054
3079
  try {
@@ -4120,16 +4145,6 @@ function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
4120
4145
  return runs;
4121
4146
  }
4122
4147
 
4123
- // src/utils/generationContext.ts
4124
- import { AsyncLocalStorage } from "async_hooks";
4125
- var generationDateStorage = new AsyncLocalStorage();
4126
- function runWithGenerationDate(date, callback) {
4127
- return generationDateStorage.run(date, callback);
4128
- }
4129
- function getGenerationDate() {
4130
- return generationDateStorage.getStore() ?? /* @__PURE__ */ new Date();
4131
- }
4132
-
4133
4148
  // src/utils/placeholderProcessor.ts
4134
4149
  function styledPlaceholderText(text, context) {
4135
4150
  return new TextRun2({
@@ -4476,8 +4491,9 @@ async function renderComponentWithCache(component, theme, themeName, context, by
4476
4491
  const themeHash = createThemeHash(theme);
4477
4492
  const contextKey = context?.section ? `${context.section.currentLayout}:${context.section.columnCount}` : "no-section";
4478
4493
  const generationDateKey = getGenerationDate().toISOString();
4494
+ const baseDirKey = getBaseDir() ?? "";
4479
4495
  const childrenKey = "children" in component && component.children ? `:children:${JSON.stringify(component.children)}` : "";
4480
- const cacheKey = `component:${component.name}:${themeHash}:${contextKey}:${generationDateKey}:${componentProps}${childrenKey}`;
4496
+ const cacheKey = `component:${component.name}:${themeHash}:${contextKey}:${generationDateKey}:${baseDirKey}:${componentProps}${childrenKey}`;
4481
4497
  const cached = await componentCache.get(cacheKey);
4482
4498
  if (cached) {
4483
4499
  return cached.result;
@@ -7130,6 +7146,7 @@ async function renderHighchartsComponent(component, theme, themeName, context) {
7130
7146
  }
7131
7147
 
7132
7148
  // src/components/visual.ts
7149
+ import { createHash as createHash4 } from "crypto";
7133
7150
  import {
7134
7151
  clampVisualDpi,
7135
7152
  DEFAULT_VISUAL_DPI
@@ -7172,14 +7189,21 @@ function visualToImageOptions(props) {
7172
7189
  ...props.keepLines !== void 0 && { keepLines: props.keepLines }
7173
7190
  };
7174
7191
  }
7175
- async function rasterize(presentation, dpi, propsServerUrl, serviceConfig) {
7192
+ function visualRasterKey(presentation, dpi, serverUrl) {
7193
+ return createHash4("sha256").update(JSON.stringify({ p: presentation, d: dpi, u: serverUrl ?? null })).digest("hex");
7194
+ }
7195
+ function effectiveVisualServerUrl(props, serviceConfig) {
7196
+ if (serviceConfig?.render) return void 0;
7197
+ return props.serverUrl;
7198
+ }
7199
+ async function rasterizeVisualSlide(presentation, dpi, propsServerUrl, serviceConfig, baseDir) {
7176
7200
  if (!isNodeEnvironment()) {
7177
7201
  throw new Error(
7178
7202
  "Visual rasterization requires a Node.js environment. It is not available in browser environments."
7179
7203
  );
7180
7204
  }
7181
7205
  if (serviceConfig?.render) {
7182
- return serviceConfig.render({ presentation, dpi });
7206
+ return serviceConfig.render({ presentation, dpi, baseDir });
7183
7207
  }
7184
7208
  const serverUrl = resolveServiceUrl(
7185
7209
  propsServerUrl,
@@ -7189,7 +7213,7 @@ async function rasterize(presentation, dpi, propsServerUrl, serviceConfig) {
7189
7213
  const response = await postJsonToService({
7190
7214
  url: serverUrl,
7191
7215
  path: "/rasterize",
7192
- body: { presentation, dpi },
7216
+ body: { presentation, dpi, ...baseDir !== void 0 && { baseDir } },
7193
7217
  headers: serviceConfig?.headers,
7194
7218
  serviceLabel: "PPTX rasterization service",
7195
7219
  onUnreachable: (url, cause) => `PPTX rasterization service is not reachable at ${url}. Configure services.pptx with a \`render\` callback or a running \`serverUrl\`.
@@ -7218,12 +7242,26 @@ async function renderVisualComponent(component, theme, themeName, context) {
7218
7242
  const dpi = clampVisualDpi(
7219
7243
  props.dpi ?? serviceConfig?.dpi ?? DEFAULT_VISUAL_DPI
7220
7244
  );
7221
- const result = await rasterize(
7222
- presentation,
7223
- dpi,
7224
- props.serverUrl,
7225
- serviceConfig
7245
+ const preRasterized = context?.visualRasterResults?.get(
7246
+ visualRasterKey(
7247
+ presentation,
7248
+ dpi,
7249
+ effectiveVisualServerUrl(props, serviceConfig)
7250
+ )
7226
7251
  );
7252
+ let result;
7253
+ if (preRasterized) {
7254
+ if (!preRasterized.ok) throw new Error(preRasterized.error);
7255
+ result = preRasterized;
7256
+ } else {
7257
+ result = await rasterizeVisualSlide(
7258
+ presentation,
7259
+ dpi,
7260
+ props.serverUrl,
7261
+ serviceConfig,
7262
+ getBaseDir()
7263
+ );
7264
+ }
7227
7265
  return await createImage(
7228
7266
  result.base64DataUri,
7229
7267
  theme,
@@ -7285,6 +7323,203 @@ var textSpaceAfterComponent = createComponent({
7285
7323
 
7286
7324
  // src/core/render.ts
7287
7325
  init_docxImagePositioning();
7326
+
7327
+ // src/core/prerasterizeVisuals.ts
7328
+ import {
7329
+ clampVisualDpi as clampVisualDpi2,
7330
+ DEFAULT_VISUAL_DPI as DEFAULT_VISUAL_DPI2,
7331
+ MAX_RASTERIZE_BATCH_SLIDES
7332
+ } from "@json-to-office/shared";
7333
+
7334
+ // src/utils/promiseLimiter.ts
7335
+ function createLimiter(max) {
7336
+ let active = 0;
7337
+ const queue = [];
7338
+ const release = () => {
7339
+ active--;
7340
+ queue.shift()?.();
7341
+ };
7342
+ return async function limit(fn) {
7343
+ while (active >= max)
7344
+ await new Promise((resolve2) => queue.push(resolve2));
7345
+ active++;
7346
+ try {
7347
+ return await fn();
7348
+ } finally {
7349
+ release();
7350
+ }
7351
+ };
7352
+ }
7353
+
7354
+ // src/core/prerasterizeVisuals.ts
7355
+ var DEFAULT_FALLBACK_CONCURRENCY = 4;
7356
+ var BATCH_TIMEOUT_BASE_MS = 3e4;
7357
+ var BATCH_TIMEOUT_PER_SLIDE_MS = 1e4;
7358
+ function collectVisualProps(root) {
7359
+ const found = [];
7360
+ const seen = /* @__PURE__ */ new WeakSet();
7361
+ const visit = (node) => {
7362
+ if (!node || typeof node !== "object") return;
7363
+ if (seen.has(node)) return;
7364
+ seen.add(node);
7365
+ if (Array.isArray(node)) {
7366
+ for (const item of node) visit(item);
7367
+ return;
7368
+ }
7369
+ const obj = node;
7370
+ if (typeof obj.name === "string" && obj.enabled === false) return;
7371
+ if (obj.name === "visual" && obj.props && typeof obj.props === "object") {
7372
+ found.push(obj.props);
7373
+ return;
7374
+ }
7375
+ for (const value of Object.values(obj)) visit(value);
7376
+ };
7377
+ visit(root);
7378
+ return found;
7379
+ }
7380
+ function toErrorMessage(error) {
7381
+ return error instanceof Error ? error.message : String(error);
7382
+ }
7383
+ function* chunksOf(items, size) {
7384
+ for (let i = 0; i < items.length; i += size) yield items.slice(i, i + size);
7385
+ }
7386
+ function applyBatchResponse(chunk, response, map) {
7387
+ const results = response?.results;
7388
+ if (!Array.isArray(results) || results.length !== chunk.length) return false;
7389
+ const entries = [];
7390
+ for (let i = 0; i < chunk.length; i++) {
7391
+ const item = results[i];
7392
+ if (item && item.ok === true && typeof item.base64DataUri === "string" && item.base64DataUri.length > 0 && typeof item.width === "number" && typeof item.height === "number") {
7393
+ entries.push([
7394
+ chunk[i].key,
7395
+ {
7396
+ ok: true,
7397
+ base64DataUri: item.base64DataUri,
7398
+ width: item.width,
7399
+ height: item.height
7400
+ }
7401
+ ]);
7402
+ } else if (item && item.ok === false && typeof item.error === "string") {
7403
+ entries.push([chunk[i].key, { ok: false, error: item.error }]);
7404
+ } else {
7405
+ return false;
7406
+ }
7407
+ }
7408
+ for (const [key, value] of entries) map.set(key, value);
7409
+ return true;
7410
+ }
7411
+ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
7412
+ const map = /* @__PURE__ */ new Map();
7413
+ if (!isNodeEnvironment() && !serviceConfig?.renderBatch && !serviceConfig?.render) {
7414
+ return map;
7415
+ }
7416
+ const visuals = collectVisualProps(root);
7417
+ if (visuals.length === 0) return map;
7418
+ const targets = /* @__PURE__ */ new Map();
7419
+ for (const props of visuals) {
7420
+ try {
7421
+ const serverUrl2 = effectiveVisualServerUrl(props, serviceConfig);
7422
+ if (serverUrl2 !== void 0) continue;
7423
+ const presentation = buildVisualPresentation(props);
7424
+ const dpi = clampVisualDpi2(
7425
+ props.dpi ?? serviceConfig?.dpi ?? DEFAULT_VISUAL_DPI2
7426
+ );
7427
+ const key = visualRasterKey(presentation, dpi, serverUrl2);
7428
+ if (!targets.has(key)) targets.set(key, { key, presentation, dpi });
7429
+ } catch {
7430
+ continue;
7431
+ }
7432
+ }
7433
+ if (targets.size === 0) return map;
7434
+ const unique = [...targets.values()];
7435
+ const limit = createLimiter(
7436
+ Math.max(1, options.concurrency ?? DEFAULT_FALLBACK_CONCURRENCY)
7437
+ );
7438
+ const rasterizeIndividually = async (chunk) => {
7439
+ await Promise.all(
7440
+ chunk.map(
7441
+ (target) => limit(async () => {
7442
+ try {
7443
+ const result = await rasterizeVisualSlide(
7444
+ target.presentation,
7445
+ target.dpi,
7446
+ void 0,
7447
+ serviceConfig,
7448
+ options.baseDir
7449
+ );
7450
+ map.set(target.key, { ok: true, ...result });
7451
+ } catch (error) {
7452
+ map.set(target.key, { ok: false, error: toErrorMessage(error) });
7453
+ }
7454
+ })
7455
+ )
7456
+ );
7457
+ };
7458
+ if (serviceConfig?.renderBatch) {
7459
+ for (const chunk of chunksOf(unique, MAX_RASTERIZE_BATCH_SLIDES)) {
7460
+ try {
7461
+ const response = await serviceConfig.renderBatch({
7462
+ slides: chunk.map((target) => ({
7463
+ presentation: target.presentation,
7464
+ dpi: target.dpi
7465
+ })),
7466
+ ...options.baseDir !== void 0 && { baseDir: options.baseDir }
7467
+ });
7468
+ if (!applyBatchResponse(chunk, response, map)) {
7469
+ throw new Error(
7470
+ "batch rasterizer returned a malformed result (expected index-aligned results[])"
7471
+ );
7472
+ }
7473
+ } catch (error) {
7474
+ if (serviceConfig.render || serviceConfig.serverUrl) {
7475
+ await rasterizeIndividually(chunk);
7476
+ } else {
7477
+ const message = toErrorMessage(error);
7478
+ for (const target of chunk) {
7479
+ map.set(target.key, { ok: false, error: message });
7480
+ }
7481
+ }
7482
+ }
7483
+ }
7484
+ return map;
7485
+ }
7486
+ if (serviceConfig?.render) {
7487
+ await rasterizeIndividually(unique);
7488
+ return map;
7489
+ }
7490
+ const serverUrl = resolveServiceUrl(
7491
+ void 0,
7492
+ serviceConfig?.serverUrl,
7493
+ DEFAULT_RASTERIZE_SERVER_URL
7494
+ );
7495
+ for (const chunk of chunksOf(unique, MAX_RASTERIZE_BATCH_SLIDES)) {
7496
+ let applied = false;
7497
+ try {
7498
+ const response = await postJsonToService({
7499
+ url: serverUrl,
7500
+ path: "/rasterize/batch",
7501
+ body: {
7502
+ slides: chunk.map((target) => ({
7503
+ presentation: target.presentation,
7504
+ dpi: target.dpi
7505
+ })),
7506
+ ...options.baseDir !== void 0 && { baseDir: options.baseDir }
7507
+ },
7508
+ headers: serviceConfig?.headers,
7509
+ timeoutMs: BATCH_TIMEOUT_BASE_MS + BATCH_TIMEOUT_PER_SLIDE_MS * chunk.length,
7510
+ serviceLabel: "PPTX batch rasterization service",
7511
+ onUnreachable: (url, cause) => `PPTX rasterization service is not reachable at ${url}. Cause: ${cause}`
7512
+ });
7513
+ applied = applyBatchResponse(chunk, await response.json(), map);
7514
+ } catch {
7515
+ applied = false;
7516
+ }
7517
+ if (!applied) await rasterizeIndividually(chunk);
7518
+ }
7519
+ return map;
7520
+ }
7521
+
7522
+ // src/core/render.ts
7288
7523
  function getAlignment3(alignment) {
7289
7524
  switch (alignment) {
7290
7525
  case "center":
@@ -7316,10 +7551,13 @@ function coreProperties(metadata) {
7316
7551
  async function renderDocument(structure, layout, options) {
7317
7552
  return runWithGenerationDate(
7318
7553
  structure.metadata.date,
7319
- () => globalBookmarkRegistry.runScoped(
7320
- () => globalRevisionIdRegistry.runScoped(
7321
- () => globalNumberingRegistry.runScoped(
7322
- () => renderDocumentScoped(structure, layout, options)
7554
+ () => runWithBaseDir(
7555
+ options?.baseDir,
7556
+ () => globalBookmarkRegistry.runScoped(
7557
+ () => globalRevisionIdRegistry.runScoped(
7558
+ () => globalNumberingRegistry.runScoped(
7559
+ () => renderDocumentScoped(structure, layout, options)
7560
+ )
7323
7561
  )
7324
7562
  )
7325
7563
  )
@@ -7338,6 +7576,21 @@ async function renderDocumentScoped(structure, layout, options) {
7338
7576
  structure.themeName
7339
7577
  );
7340
7578
  context.services = options?.services;
7579
+ try {
7580
+ const visualRasterResults = await prerasterizeVisuals(
7581
+ layout.sections,
7582
+ options?.services?.pptx,
7583
+ { baseDir: getBaseDir() }
7584
+ );
7585
+ if (visualRasterResults.size > 0) {
7586
+ context.visualRasterResults = visualRasterResults;
7587
+ }
7588
+ } catch (error) {
7589
+ console.warn(
7590
+ "[core-docx] Visual pre-rasterization failed; falling back to per-visual rasterization:",
7591
+ error instanceof Error ? error.message : error
7592
+ );
7593
+ }
7341
7594
  let sectionBookmarkCounter = 0;
7342
7595
  let previousHeader = void 0;
7343
7596
  let previousFooter = void 0;
@@ -8009,7 +8262,8 @@ function createBuilderImpl(state) {
8009
8262
  fonts: state.fonts,
8010
8263
  validation: state.validation,
8011
8264
  deterministic: state.deterministic,
8012
- generatedAt: state.generatedAt
8265
+ generatedAt: state.generatedAt,
8266
+ baseDir: state.baseDir
8013
8267
  };
8014
8268
  return createBuilderImpl(
8015
8269
  newState
@@ -8108,7 +8362,8 @@ function createBuilderImpl(state) {
8108
8362
  const layout = applyLayout(structure.sections, modedTheme, themeName);
8109
8363
  const generatedDocument = await renderDocument(structure, layout, {
8110
8364
  services: state.services,
8111
- bypassCache: !state.enableCache
8365
+ bypassCache: !state.enableCache,
8366
+ baseDir: options?.baseDir ?? state.baseDir
8112
8367
  });
8113
8368
  const preservedDefinition = preserveSet ? {
8114
8369
  ...modedRoot,
@@ -8238,7 +8493,8 @@ function createDocumentGenerator(options) {
8238
8493
  fonts: options.fonts,
8239
8494
  validation: options.validation,
8240
8495
  deterministic: options.deterministic ?? true,
8241
- generatedAt: options.generatedAt
8496
+ generatedAt: options.generatedAt,
8497
+ baseDir: options.baseDir
8242
8498
  };
8243
8499
  return createBuilderImpl(initialState);
8244
8500
  }
@@ -8292,7 +8548,7 @@ var WeatherV2PropsSchema = Type3.Object(
8292
8548
  }
8293
8549
  );
8294
8550
  async function fetchWeather(city, units) {
8295
- await new Promise((resolve) => setTimeout(resolve, 100));
8551
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
8296
8552
  const mockData = {
8297
8553
  London: {
8298
8554
  temperature: units === "imperial" ? 59 : 15,
@@ -8325,7 +8581,7 @@ async function fetchWeather(city, units) {
8325
8581
  };
8326
8582
  }
8327
8583
  async function fetchForecast(city, units, days) {
8328
- await new Promise((resolve) => setTimeout(resolve, 50));
8584
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
8329
8585
  const dayNames = ["Mon", "Tue", "Wed", "Thu", "Fri"];
8330
8586
  const base = units === "imperial" ? 65 : 18;
8331
8587
  return dayNames.slice(0, days).map((day, i) => ({