@office-kit/pptx-preview 0.16.0 → 0.18.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.
@@ -484,6 +484,21 @@ const renderEmfToSvg = (bytes) => {
484
484
  return null;
485
485
  };
486
486
  //#endregion
487
+ //#region src/link-href.ts
488
+ const ALLOWED_PROTOCOLS = new Set([
489
+ "http:",
490
+ "https:",
491
+ "mailto:"
492
+ ]);
493
+ const safeLinkHref = (href) => {
494
+ if (href.startsWith("#")) return href;
495
+ try {
496
+ return ALLOWED_PROTOCOLS.has(new URL(href).protocol) ? href : void 0;
497
+ } catch {
498
+ return;
499
+ }
500
+ };
501
+ //#endregion
487
502
  //#region src/text-layout.ts
488
503
  const SANS = "Carlito";
489
504
  const SERIF = "Caladea";
@@ -651,8 +666,27 @@ const layoutCore = (input, measure) => {
651
666
  }
652
667
  }
653
668
  const canBreak = EAST_ASIAN_CHAR.test(segment) ? width > avail - bulletLead : para.latinLineBreak === true || width > avail - bulletLead;
654
- if (input.wrap && canBreak && [...segment].length > 1) for (const fragment of fragments) for (const ch of fragment.text) tokens.push(makeToken(ch, fragment.piece));
655
- else for (const fragment of fragments) tokens.push(fragment);
669
+ if (input.wrap && canBreak && [...segment].length > 1) {
670
+ const breakAnywhere = para.latinLineBreak === true || EAST_ASIAN_CHAR.test(segment);
671
+ let chunk = [];
672
+ let chunkWidth = 0;
673
+ const flushChunk = () => {
674
+ const keepWhole = chunk.length > 1 && chunkWidth <= avail - bulletLead;
675
+ chunk.forEach((token, ci) => tokens.push(keepWhole && ci > 0 ? {
676
+ ...token,
677
+ continuesSegment: true
678
+ } : token));
679
+ chunk = [];
680
+ chunkWidth = 0;
681
+ };
682
+ for (const fragment of fragments) for (const ch of fragment.text) {
683
+ const token = makeToken(ch, fragment.piece);
684
+ chunk.push(token);
685
+ chunkWidth += token.width;
686
+ if (breakAnywhere || ch === "-" || ch === "/") flushChunk();
687
+ }
688
+ flushChunk();
689
+ } else for (const fragment of fragments) tokens.push(fragment);
656
690
  }
657
691
  wordParts = [];
658
692
  };
@@ -1044,10 +1078,11 @@ const emitLine = (line, baselineY, dx, descent, nextEffectId, effectsOnly, effec
1044
1078
  const groups = groupTokens(content);
1045
1079
  const tspans = groups.map((g) => {
1046
1080
  const span = tspan(g);
1047
- if (!g.piece.href) return span;
1048
- const target = g.piece.href.startsWith("#") ? "" : " target=\"_blank\" rel=\"noopener noreferrer\"";
1081
+ const href = g.piece.href ? safeLinkHref(g.piece.href) : void 0;
1082
+ if (!href) return span;
1083
+ const target = href.startsWith("#") ? "" : " target=\"_blank\" rel=\"noopener noreferrer\"";
1049
1084
  const title = g.piece.hrefTip ? `<title>${escapeXml$1(g.piece.hrefTip)}</title>` : "";
1050
- return `<a href="${escapeXml$1(g.piece.href)}"${target}>${title}${span}</a>`;
1085
+ return `<a href="${escapeXml$1(href)}"${target}>${title}${span}</a>`;
1051
1086
  }).join("");
1052
1087
  if (tspans === "") return "";
1053
1088
  const x0 = line.anchorX + dx + GRID_NUDGE_X;
@@ -1691,7 +1726,7 @@ const bevelFilterPrimitives = (shape3d) => {
1691
1726
  //#region src/render-slide.ts
1692
1727
  const clickActionHref = (pres, action) => {
1693
1728
  if (!action) return void 0;
1694
- if (action.kind === "url") return action.url;
1729
+ if (action.kind === "url") return safeLinkHref(action.url);
1695
1730
  if (action.kind === "customShow") return `#pptx-custom-show?id=${action.id}&return=${action.returnToShow}`;
1696
1731
  if (action.kind === "endShow") return "#pptx-end-show";
1697
1732
  if (action.kind === "lastSlideViewed") return "#pptx-last-slide-viewed";
@@ -1716,6 +1751,7 @@ const DEFAULT_BODY_PT = 18;
1716
1751
  const DEFAULT_TITLE_PT = 44;
1717
1752
  const DEFAULT_FONT = "Calibri, 'Helvetica Neue', Arial, sans-serif";
1718
1753
  const browserFontFamily = (family) => family ? `${JSON.stringify(family)}, ${DEFAULT_FONT}` : DEFAULT_FONT;
1754
+ const fontFamilyStyles = (font) => font.startsWith("+") ? [] : [`font-family:${escapeXml(browserFontFamily(font))}`];
1719
1755
  const BROWSER_FAMILIES = {
1720
1756
  resolveFamily: browserFontFamily,
1721
1757
  familyIsCssList: true
@@ -2442,7 +2478,7 @@ const renderRun = (text, format, theme, effectivePt, defaultColor, _wasDefault =
2442
2478
  const styles = [];
2443
2479
  styles.push(`font-size:${(effectivePt * PX_PER_PT).toFixed(2)}px`);
2444
2480
  styles.push(suppressLineHeight ? "line-height:0;vertical-align:top" : lineHeightOverride !== void 0 ? `line-height:${lineHeightOverride}` : "line-height:1.05");
2445
- if (format?.font) styles.push(`font-family:${escapeXml(format.font)}, ${DEFAULT_FONT}`);
2481
+ if (format?.font) styles.push(...fontFamilyStyles(format.font));
2446
2482
  if (format?.bold) styles.push("font-weight:700");
2447
2483
  if (format?.italic) styles.push("font-style:italic");
2448
2484
  const underline = format?.underline;
@@ -2490,6 +2526,7 @@ const renderRun = (text, format, theme, effectivePt, defaultColor, _wasDefault =
2490
2526
  return `<span style="${styles.join(";")}">${content}</span>`;
2491
2527
  };
2492
2528
  const LINE_HEIGHT = 1.05;
2529
+ const NO_AUTOSPACE = "text-autospace:no-autospace;";
2493
2530
  const AUTOFIT_FLOOR = .25;
2494
2531
  const AUTOFIT_STEP = .05;
2495
2532
  const decimalSeparatorField = (lang) => {
@@ -2773,7 +2810,7 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2773
2810
  const defaultPt = placeholderDefaultPt(phType);
2774
2811
  const themeFonts = getPresentationFonts(pres);
2775
2812
  const themeFace = phType === "title" || phType === "ctrTitle" ? themeFonts?.majorLatin ?? null : themeFonts?.minorLatin ?? null;
2776
- const effectiveDefaultFont = themeFace ? `${escapeXml(themeFace)}, ${DEFAULT_FONT}` : DEFAULT_FONT;
2813
+ const effectiveDefaultFont = escapeXml(browserFontFamily(themeFace));
2777
2814
  let effectiveBody;
2778
2815
  try {
2779
2816
  effectiveBody = getShapeBodyPrEffective(pres, shape);
@@ -3080,10 +3117,11 @@ const renderHtmlParagraphs = (paraData, numberLabels, theme, autoFitScale, defau
3080
3117
  };
3081
3118
  }
3082
3119
  const span = renderRun(run.text, runFmt, theme, run.sizePt * autoFitScale, defaultColor, run.fmt?.size === void 0, run.text === "\n" && /[^\n]$/.test(para.runs[runIndex - 1]?.text ?? ""), para.lineSpacing?.kind === "pct" ? para.lineSpacing.value.toFixed(3) : para.lineSpacing?.kind === "pts" ? "0" : void 0);
3083
- if (!run.href) return span;
3084
- const targetAttrs = run.href.startsWith("#") ? "" : " target=\"_blank\" rel=\"noopener noreferrer\"";
3120
+ const href = run.href ? safeLinkHref(run.href) : void 0;
3121
+ if (!href) return span;
3122
+ const targetAttrs = href.startsWith("#") ? "" : " target=\"_blank\" rel=\"noopener noreferrer\"";
3085
3123
  const titleAttr = run.hrefTip ? ` title="${escapeXml(run.hrefTip)}"` : "";
3086
- return `<a href="${escapeXml(run.href)}"${targetAttrs}${titleAttr} style="color:inherit;text-decoration:inherit">${span}</a>`;
3124
+ return `<a href="${escapeXml(href)}"${targetAttrs}${titleAttr} style="color:inherit;text-decoration:inherit">${span}</a>`;
3087
3125
  });
3088
3126
  const firstRunPt = para.runs.find((run) => run.text !== "\n" && run.text !== "")?.sizePt ?? defaultPt;
3089
3127
  let lineHeightCss = para.emptySizePt !== void 0 ? `line-height:${LINE_HEIGHT}` : "";
@@ -3097,7 +3135,7 @@ const renderHtmlParagraphs = (paraData, numberLabels, theme, autoFitScale, defau
3097
3135
  const pStyles = [
3098
3136
  "margin:0",
3099
3137
  para.lineSpacing?.kind === "pts" ? `font-size:${(firstRunPt * PX_PER_PT * autoFitScale).toFixed(2)}px` : "font-size:0",
3100
- ...para.lineSpacing?.kind === "pts" && para.runs[0]?.fmt?.font ? [`font-family:${escapeXml(para.runs[0].fmt.font)}, ${DEFAULT_FONT}`] : [],
3138
+ ...para.lineSpacing?.kind === "pts" && para.runs[0]?.fmt?.font ? fontFamilyStyles(para.runs[0].fmt.font) : [],
3101
3139
  ...para.emptySizePt !== void 0 ? [`font-size:${(para.emptySizePt * PX_PER_PT * autoFitScale).toFixed(2)}px`] : [],
3102
3140
  ...para.runs.some((run) => run.text.includes(" ")) ? [`white-space:${wrap ? "pre-wrap" : "pre"}`, `tab-size:${((para.defaultTabSizeEmu ?? 914400) / EMU_PER_PX * autoFitScale).toFixed(2)}px`] : [],
3103
3141
  marginTopCss,
@@ -3132,7 +3170,7 @@ const renderHtmlParagraphs = (paraData, numberLabels, theme, autoFitScale, defau
3132
3170
  else if (para.bulletDetail.sizePts !== null) bulletStyles.push(`font-size:${(para.bulletDetail.sizePts * PX_PER_PT * autoFitScale).toFixed(2)}px`);
3133
3171
  else bulletStyles.push(`font-size:${baseBulletPx.toFixed(2)}px`);
3134
3172
  const bulletFont = para.bulletDetail.fontFollowText ? para.runs.find((r) => r.text !== "\n" && r.text !== "")?.fmt?.font ?? DEFAULT_BULLET_FONT : para.bulletDetail.font ?? DEFAULT_BULLET_FONT;
3135
- bulletStyles.push(`font-family:${escapeXml(bulletFont)}, ${DEFAULT_FONT}`);
3173
+ bulletStyles.push(...fontFamilyStyles(bulletFont));
3136
3174
  prefix = `<span style="${bulletStyles.join(";")}">${escapeXml(char)}</span>`;
3137
3175
  }
3138
3176
  paragraphs.push(`<p data-pptx-paragraph="${pi}" style="${pStyles.join(";")}">${prefix}${runHtmls.join("") || "&#8203;"}</p>`);
@@ -3235,7 +3273,7 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx, output = "body"
3235
3273
  const vertStyles = (writingMode ? `;${writingMode}${extraTransform}` : "") + colStyles;
3236
3274
  const wrapStyle = effectiveBody.wrap === "none" ? "white-space:nowrap" : "word-break:break-word";
3237
3275
  const content = paragraphs.join("");
3238
- const body = `<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;flex-direction:column;justify-content:${justify};width:100%;height:100%;box-sizing:border-box;overflow:visible;font-family:${effectiveDefaultFont};color:${defaultColor};${`translate:${model.anchorOffset.x}px ${model.anchorOffset.y}px;`}${wrapStyle}${vertStyles}">${content}</div>`;
3276
+ const body = `<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;flex-direction:column;justify-content:${justify};width:100%;height:100%;box-sizing:border-box;overflow:visible;font-family:${effectiveDefaultFont};color:${defaultColor};${NO_AUTOSPACE}${`translate:${model.anchorOffset.x}px ${model.anchorOffset.y}px;`}${wrapStyle}${vertStyles}">${content}</div>`;
3239
3277
  const foreign = `<foreignObject x="${E(innerX)}" y="${E(innerY)}" width="${E(innerW)}" height="${E(innerH)}" overflow="visible">${body}</foreignObject>`;
3240
3278
  const hasCharacterEffect = bevel !== null || paraData.some((para) => para.runs.some((run) => run.fmt?.reflection !== void 0 && run.fmt.reflection !== null || run.fmt?.innerShadow !== void 0 && run.fmt.innerShadow !== null || hasPaintedTextFill(run)));
3241
3279
  let reflectionOverlay = "";
@@ -3407,7 +3445,7 @@ const chartText = (f, x, y, attrs, content) => {
3407
3445
  }
3408
3446
  return `<text x="${px(x)}" y="${px(y)}" ${attrs}>${content}</text>`;
3409
3447
  };
3410
- const layoutChart = (xEmu, yEmu, wEmu, hEmu, hasTitle, hasAxes, titleOverlay = false, legendOverlay = false, hasLegend = true, titlePx = DEFAULT_CHART_TITLE_PT * PX_PER_PT, plotAreaLayout) => {
3448
+ const layoutChart = (xEmu, yEmu, wEmu, hEmu, hasTitle, hasAxes, titleOverlay = false, legendOverlay = false, hasLegend = true, titlePx = DEFAULT_CHART_TITLE_PT * PX_PER_PT, plotAreaLayout, yAxisGutter = hasAxes ? DEFAULT_Y_AXIS_GUTTER : 0, xAxisGutter = hasAxes ? BAR_VALUE_LABEL_ROW : 0) => {
3411
3449
  const x = xEmu / EMU_PER_PX;
3412
3450
  const y = yEmu / EMU_PER_PX;
3413
3451
  const w = wEmu / EMU_PER_PX;
@@ -3415,8 +3453,6 @@ const layoutChart = (xEmu, yEmu, wEmu, hEmu, hasTitle, hasAxes, titleOverlay = f
3415
3453
  const titleStrip = hasTitle && !titleOverlay ? Math.round(titlePx * 2.4) : 0;
3416
3454
  const legendStrip = hasLegend && !legendOverlay ? 18 : 0;
3417
3455
  const padding = 8;
3418
- const yAxisGutter = hasAxes ? 32 : 0;
3419
- const xAxisGutter = hasAxes ? 18 : 0;
3420
3456
  const inner = plotAreaLayout?.target === "inner" ? plotAreaLayout : void 0;
3421
3457
  const plotX = inner ? x + Math.max(0, Math.min(1, inner.x)) * w : x + padding + yAxisGutter;
3422
3458
  const plotY = inner ? y + Math.max(0, Math.min(1, inner.y)) * h : y + titleStrip + padding;
@@ -3529,9 +3565,10 @@ const DEFAULT_AXIS_COLOR = "#000000";
3529
3565
  const DEFAULT_GRID_COLOR = "#D9D9D9";
3530
3566
  const AXIS_TICK_LEN = 5;
3531
3567
  const AXIS_MINOR_TICK_LEN = 3;
3568
+ const AXIS_LABEL_GAP = 3;
3532
3569
  const chartFontPx = (sizePt) => (sizePt * PX_PER_PT).toFixed(1);
3533
3570
  const axisTickAttrs = (style) => {
3534
- const sz = style?.sizePt ?? 10;
3571
+ const sz = style?.sizePt ?? DEFAULT_AXIS_LABEL_PT;
3535
3572
  const fill = style?.color ?? "#6B7280";
3536
3573
  const weight = style?.bold ? " font-weight=\"600\"" : "";
3537
3574
  const italic = style?.italic ? " font-style=\"italic\"" : "";
@@ -3580,7 +3617,8 @@ const renderValueAxis = (f, axis) => {
3580
3617
  const tx2 = tickMark === "out" ? edgeX : edgeX + inward;
3581
3618
  out.push(`<line x1="${px(tx1)}" y1="${px(yp)}" x2="${px(tx2)}" y2="${px(yp)}" stroke="${axisColor}" stroke-width="1"/>`);
3582
3619
  }
3583
- const labelX = onRight ? edgeX + 4 : edgeX - 4;
3620
+ const labelOffset = (tickMark === "out" || tickMark === "cross" ? tickLen : 0) + AXIS_LABEL_GAP;
3621
+ const labelX = onRight ? edgeX + labelOffset : edgeX - labelOffset;
3584
3622
  const rot = axis.labelRotationDeg ?? 0;
3585
3623
  const transform = rot ? ` transform="rotate(${rot} ${px(labelX)} ${px(yp)})"` : "";
3586
3624
  out.push(chartText(f, labelX, yp, `text-anchor="${onRight ? "start" : "end"}" dominant-baseline="middle" ${axisTickAttrs(axis.labelStyle)}${transform}`, `${escapeXml(fmtTick(t))}`));
@@ -3601,20 +3639,81 @@ const renderValueAxis = (f, axis) => {
3601
3639
  if (axis.displayUnits) {
3602
3640
  const lbl = DISPLAY_UNIT_LABEL[axis.displayUnits];
3603
3641
  if (axis.orientation === "vertical") {
3604
- const lblX = f.plotX - 26;
3642
+ const lblX = axis.side === "right" ? f.plotX + f.plotW + 26 : f.plotX - 26;
3605
3643
  const lblY = f.plotY + f.plotH / 2;
3606
3644
  out.push(chartText(f, lblX, lblY, `text-anchor="middle" font-family="sans-serif" font-size="9" fill="#6B7280" font-style="italic" transform="rotate(-90 ${px(lblX)} ${px(lblY)})"`, `${escapeXml(lbl)}`));
3607
3645
  } else out.push(chartText(f, f.plotX + f.plotW, f.plotY + f.plotH + 22, `text-anchor="end" font-family="sans-serif" font-size="9" fill="#6B7280" font-style="italic"`, `${escapeXml(lbl)}`));
3608
3646
  }
3609
- if (!axis.lineHidden) if (axis.orientation === "vertical") out.push(`<line x1="${px(f.plotX)}" y1="${px(f.plotY)}" x2="${px(f.plotX)}" y2="${px(f.plotY + f.plotH)}" stroke="${axisColor}" stroke-width="1"/>`);
3610
- else {
3647
+ if (!axis.lineHidden) if (axis.orientation === "vertical") {
3648
+ const spineX = axis.side === "right" ? f.plotX + f.plotW : f.plotX;
3649
+ out.push(`<line x1="${px(spineX)}" y1="${px(f.plotY)}" x2="${px(spineX)}" y2="${px(f.plotY + f.plotH)}" stroke="${axisColor}" stroke-width="1"/>`);
3650
+ } else {
3611
3651
  const zeroY = f.plotY + f.plotH - (0 - axis.min) / range * f.plotH;
3612
3652
  const spineY = axis.min <= 0 && axis.max >= 0 ? zeroY : f.plotY + f.plotH;
3613
3653
  out.push(`<line x1="${px(f.plotX)}" y1="${px(spineY)}" x2="${px(f.plotX + f.plotW)}" y2="${px(spineY)}" stroke="${axisColor}" stroke-width="1"/>`);
3614
3654
  }
3615
3655
  return out.join("");
3616
3656
  };
3617
- const renderCategoryAxis = (f, orientation, cats, pointCount, skip = 1, labelStyle, labelRotationDeg, labelAlign, lineColor, lineHidden = false, majorTickMark = "out", minorTickMark = "none") => {
3657
+ const CATEGORY_LABEL_GAP = 6;
3658
+ const CATEGORY_LABEL_LINE_HEIGHT = 1.2;
3659
+ const MAX_BAR_CATEGORY_LABEL_SHARE = .5;
3660
+ const DEFAULT_Y_AXIS_GUTTER = 32;
3661
+ const BAR_VALUE_LABEL_ROW = 18;
3662
+ const DEFAULT_AXIS_TITLE_PT = 11;
3663
+ const DEFAULT_AXIS_LABEL_PT = 10;
3664
+ const categoryLabelFontPx = (style) => (style?.sizePt ?? DEFAULT_AXIS_LABEL_PT) * PX_PER_PT;
3665
+ const axisTitleRowPx = (style) => (style?.sizePt ?? DEFAULT_AXIS_TITLE_PT) * PX_PER_PT * CATEGORY_LABEL_LINE_HEIGHT;
3666
+ const NO_LINE_START = /* @__PURE__ */ new Set("、。,.,.))]]」』】〉》%%!!??::;;ー〜…・");
3667
+ const chartLabelSegments = (text) => {
3668
+ const segments = [];
3669
+ let word = "";
3670
+ for (const ch of text) if (NO_LINE_START.has(ch) && word === "" && segments.length > 0) segments[segments.length - 1] += ch;
3671
+ else if (FULLWIDTH_CHAR_PATTERN.test(ch) && !NO_LINE_START.has(ch)) {
3672
+ if (word !== "") segments.push(word);
3673
+ segments.push(ch);
3674
+ word = "";
3675
+ } else {
3676
+ word += ch;
3677
+ if (/\s/u.test(ch)) {
3678
+ segments.push(word);
3679
+ word = "";
3680
+ }
3681
+ }
3682
+ if (word !== "") segments.push(word);
3683
+ return segments;
3684
+ };
3685
+ const wrapChartLabel = (text, maxPx, widthOf) => {
3686
+ const lines = [];
3687
+ let line = "";
3688
+ for (const segment of chartLabelSegments(text)) {
3689
+ if (line !== "" && widthOf((line + segment).trimEnd()) > maxPx) {
3690
+ lines.push(line.trimEnd());
3691
+ line = "";
3692
+ }
3693
+ line += segment;
3694
+ }
3695
+ if (line.trim() !== "") lines.push(line.trimEnd());
3696
+ return lines;
3697
+ };
3698
+ const layoutBarCategoryLabels = (spec, chartWidthPx, measure) => {
3699
+ const style = spec.categoryAxisLabelStyle;
3700
+ const font = {
3701
+ family: SANS,
3702
+ sizePx: categoryLabelFontPx(style),
3703
+ bold: style?.bold === true,
3704
+ italic: style?.italic === true,
3705
+ letterSpacingPx: 0
3706
+ };
3707
+ const widthOf = (text) => measure(text, font).widthPx;
3708
+ const labels = Array.from({ length: pointCount(spec) }, (_, i) => spec.categories[i] ?? (i + 1).toString());
3709
+ const maxPx = chartWidthPx * MAX_BAR_CATEGORY_LABEL_SHARE;
3710
+ const lines = labels.map((label) => wrapChartLabel(label, maxPx, widthOf));
3711
+ return {
3712
+ widthPx: Math.max(0, ...lines.flatMap((ls) => ls.map(widthOf))),
3713
+ lines
3714
+ };
3715
+ };
3716
+ const renderCategoryAxis = (f, orientation, cats, pointCount, skip = 1, labelStyle, labelRotationDeg, labelAlign, lineColor, lineHidden = false, majorTickMark = "out", minorTickMark = "none", wrappedLabels) => {
3618
3717
  const labels = [];
3619
3718
  for (let i = 0; i < pointCount; i++) labels.push(cats[i] ?? (i + 1).toString());
3620
3719
  const out = [];
@@ -3636,6 +3735,14 @@ const renderCategoryAxis = (f, orientation, cats, pointCount, skip = 1, labelSty
3636
3735
  for (let i = 0; i < pointCount; i++) {
3637
3736
  if (skip > 1 && i % skip !== 0) continue;
3638
3737
  const cy = f.plotY + (pointCount - 1 - i + .5) * step;
3738
+ const lines = wrappedLabels?.[i];
3739
+ if (lines) {
3740
+ const lx = f.plotX - CATEGORY_LABEL_GAP;
3741
+ const lineH = categoryLabelFontPx(labelStyle) * CATEGORY_LABEL_LINE_HEIGHT;
3742
+ const content = lines.length === 1 ? escapeXml(lines[0]) : lines.map((line, k) => `<tspan x="${px(lx)}" dy="${px(k === 0 ? -(lines.length - 1) * lineH / 2 : lineH)}">${escapeXml(line)}</tspan>`).join("");
3743
+ out.push(chartText(f, lx, cy, `text-anchor="end" dominant-baseline="middle" ${axisTickAttrs(labelStyle)}`, content));
3744
+ continue;
3745
+ }
3639
3746
  const lx = f.plotX - 4;
3640
3747
  const truncated = labels[i] !== void 0 && labels[i].length > truncLen ? `${labels[i].slice(0, truncLen - 2)}…` : labels[i] ?? "";
3641
3748
  const transform = labelRotationDeg && labelRotationDeg !== 0 ? ` transform="rotate(${labelRotationDeg} ${px(lx)} ${px(cy)})"` : "";
@@ -3690,16 +3797,21 @@ const seriesMinMax = (spec) => {
3690
3797
  if (!Number.isFinite(min)) min = 0;
3691
3798
  if (!Number.isFinite(max)) max = 1;
3692
3799
  if (max === min) max = min + 1;
3693
- if (min > 0) min = 0;
3694
3800
  if (isPercent) return {
3695
3801
  min: 0,
3696
3802
  max: 1,
3697
3803
  step: .2
3698
3804
  };
3699
- const step = niceStep(max - min);
3700
- max = (Math.floor(max / step) + 1) * step;
3701
- if (spec.valueAxis?.min !== void 0) min = spec.valueAxis.min;
3702
- if (spec.valueAxis?.max !== void 0) max = spec.valueAxis.max;
3805
+ const authoredMin = spec.valueAxis?.min;
3806
+ const authoredMax = spec.valueAxis?.max;
3807
+ const lo = authoredMin ?? min;
3808
+ const hi = authoredMax ?? max;
3809
+ const pad = (hi - lo) / AUTO_AXIS_PAD_DIVISOR;
3810
+ const autoMin = lo < 0 ? lo - pad : lo >= hi * AUTO_AXIS_ZERO_THRESHOLD ? lo - pad : 0;
3811
+ const autoMax = hi > 0 ? hi + pad : hi <= lo * AUTO_AXIS_ZERO_THRESHOLD ? hi + pad : 0;
3812
+ const step = niceStep((authoredMax ?? autoMax) - (authoredMin ?? autoMin));
3813
+ min = authoredMin ?? snapToStep(autoMin, step, Math.floor);
3814
+ max = authoredMax ?? snapToStep(autoMax, step, Math.ceil);
3703
3815
  if (max === min) max = min + 1;
3704
3816
  return {
3705
3817
  min,
@@ -3707,6 +3819,10 @@ const seriesMinMax = (spec) => {
3707
3819
  step
3708
3820
  };
3709
3821
  };
3822
+ const AUTO_AXIS_PAD_DIVISOR = 20;
3823
+ const AUTO_AXIS_ZERO_THRESHOLD = 5 / 6;
3824
+ const AXIS_BOUND_PRECISION = 12;
3825
+ const snapToStep = (value, step, round) => Number((round(value / step) * step).toPrecision(AXIS_BOUND_PRECISION));
3710
3826
  const renderChartTitle = (f, title, style) => {
3711
3827
  if (!title) return "";
3712
3828
  const sz = style?.sizePt ?? DEFAULT_CHART_TITLE_PT;
@@ -4555,7 +4671,7 @@ const renderRadarChart = (f, spec, colors) => {
4555
4671
  }
4556
4672
  return out.join("");
4557
4673
  };
4558
- const renderChart = (shape, x, y, w, h, transform, theme, groupReflected) => {
4674
+ const renderChart = (shape, x, y, w, h, transform, theme, groupReflected, measure) => {
4559
4675
  let spec = null;
4560
4676
  try {
4561
4677
  spec = getShapeChartSpec(shape);
@@ -4575,10 +4691,13 @@ const renderChart = (shape, x, y, w, h, transform, theme, groupReflected) => {
4575
4691
  const isCartesian = spec.kind === "column" || spec.kind === "bar" || spec.kind === "line" || spec.kind === "area";
4576
4692
  const hasAxes = isCartesian || spec.kind === "scatter" || spec.kind === "bubble";
4577
4693
  const hasLegend = spec.legend !== void 0 && spec.legend.position !== null;
4694
+ const barLabels = spec.kind === "bar" && !spec.categoryAxisHidden && spec.categoryAxisTickLabelPos !== "none" && !spec.categoryAxisLabelRotationDeg ? layoutBarCategoryLabels(spec, w / EMU_PER_PX, measure) : void 0;
4695
+ const barValueTitleRow = spec.kind === "bar" && spec.valueAxisTitle ? axisTitleRowPx(spec.valueAxisTitleStyle) : 0;
4696
+ const barCategoryTitleRow = spec.kind === "bar" && spec.categoryAxisTitle ? axisTitleRowPx(spec.categoryAxisTitleStyle) : 0;
4578
4697
  const flip = getShapeFlip(shape);
4579
4698
  const f = {
4580
4699
  reflected: groupReflected !== Boolean(flip && flip.horizontal !== flip.vertical),
4581
- ...layoutChart(x, y, w, h, !!spec.title, hasAxes, spec.titleOverlay ?? false, spec.legend?.overlay ?? false, hasLegend, (spec.titleStyle?.sizePt ?? DEFAULT_CHART_TITLE_PT) * PX_PER_PT, spec.plotAreaLayout)
4700
+ ...layoutChart(x, y, w, h, !!spec.title, hasAxes, spec.titleOverlay ?? false, spec.legend?.overlay ?? false, hasLegend, (spec.titleStyle?.sizePt ?? DEFAULT_CHART_TITLE_PT) * PX_PER_PT, spec.plotAreaLayout, barLabels ? Math.max(DEFAULT_Y_AXIS_GUTTER, barLabels.widthPx + CATEGORY_LABEL_GAP) + barCategoryTitleRow : void 0, barValueTitleRow > 0 ? BAR_VALUE_LABEL_ROW + barValueTitleRow : void 0)
4582
4701
  };
4583
4702
  const allNamesForLegend = spec.kind === "pie" || spec.kind === "doughnut" ? Array.from(spec.categories) : spec.series.map((s) => s.name);
4584
4703
  const allColorsForLegend = spec.kind === "pie" || spec.kind === "doughnut" ? spec.categories.map((_, i) => spec.series[0]?.pointColors?.[i] ?? spec.series[0]?.color ?? colors[i % colors.length] ?? "#888") : spec.series.map((s, i) => s.color ?? colors[i % colors.length] ?? "#888");
@@ -4603,10 +4722,11 @@ const renderChart = (shape, x, y, w, h, transform, theme, groupReflected) => {
4603
4722
  ...spec,
4604
4723
  series: primarySeries
4605
4724
  });
4606
- const { valueAxis: _primaryOnlyAxis, ...specWithoutAxis } = spec;
4725
+ const secondaryAxis = spec.secondaryValueAxis;
4607
4726
  const secondaryScale = secondarySeries.length > 0 ? seriesMinMax({
4608
- ...specWithoutAxis,
4609
- series: secondarySeries
4727
+ ...spec,
4728
+ series: secondarySeries,
4729
+ valueAxis: secondaryAxis ?? {}
4610
4730
  }) : null;
4611
4731
  const N = pointCount(spec);
4612
4732
  if (!spec.valueAxisHidden) axes = renderValueAxis(f, {
@@ -4625,14 +4745,20 @@ const renderChart = (shape, x, y, w, h, transform, theme, groupReflected) => {
4625
4745
  ...spec.valueAxisMajorGridlines !== void 0 ? { majorGridlines: spec.valueAxisMajorGridlines } : {},
4626
4746
  ...spec.valueAxisLabelStyle !== void 0 ? { labelStyle: spec.valueAxisLabelStyle } : {}
4627
4747
  });
4628
- if (secondaryScale) axes += renderValueAxis(f, {
4748
+ const secondaryLabelStyle = secondaryAxis?.labelStyle ?? spec.valueAxisLabelStyle;
4749
+ if (secondaryScale && secondaryAxis?.tickLabelPos !== "none") axes += renderValueAxis(f, {
4629
4750
  orientation: "vertical",
4630
4751
  side: "right",
4631
4752
  min: secondaryScale.min,
4632
4753
  max: secondaryScale.max,
4633
- majorUnit: secondaryScale.step,
4634
- majorGridlines: false,
4635
- ...spec.valueAxisLabelStyle !== void 0 ? { labelStyle: spec.valueAxisLabelStyle } : {}
4754
+ majorUnit: secondaryAxis?.majorUnit ?? secondaryScale.step,
4755
+ majorGridlines: secondaryAxis?.majorGridlines ?? false,
4756
+ ...secondaryAxis?.numberFormat !== void 0 ? { numberFormat: secondaryAxis.numberFormat } : {},
4757
+ ...secondaryAxis?.displayUnits !== void 0 ? { displayUnits: secondaryAxis.displayUnits } : {},
4758
+ ...secondaryAxis?.majorGridlineColor !== void 0 ? { majorGridlineColor: secondaryAxis.majorGridlineColor } : {},
4759
+ ...secondaryAxis?.majorTickMark !== void 0 ? { majorTickMark: secondaryAxis.majorTickMark } : {},
4760
+ ...secondaryAxis?.lineColor !== void 0 ? { lineColor: secondaryAxis.lineColor } : {},
4761
+ ...secondaryLabelStyle !== void 0 ? { labelStyle: secondaryLabelStyle } : {}
4636
4762
  });
4637
4763
  if (N > 0 && !(spec.categoryAxisHidden || spec.categoryAxisTickLabelPos === "none")) axes += renderCategoryAxis(f, "horizontal", spec.categories, N, spec.categoryAxisTickLabelSkip ?? 1, spec.categoryAxisLabelStyle, spec.categoryAxisLabelRotationDeg, spec.categoryAxisLabelAlign, spec.categoryAxisLineColor, spec.categoryAxisLineHidden, spec.categoryAxisMajorTickMark, spec.categoryAxisMinorTickMark);
4638
4764
  const groups = /* @__PURE__ */ new Map();
@@ -4700,7 +4826,7 @@ const renderChart = (shape, x, y, w, h, transform, theme, groupReflected) => {
4700
4826
  };
4701
4827
  if (!spec.valueAxisHidden) axes = renderValueAxis(f, valueAxis);
4702
4828
  const labelsHidden = spec.categoryAxisHidden || spec.categoryAxisTickLabelPos === "none";
4703
- if (N > 0 && !labelsHidden) axes += renderCategoryAxis(f, spec.kind === "bar" ? "vertical" : "horizontal", spec.categories, N, spec.categoryAxisTickLabelSkip ?? 1, spec.categoryAxisLabelStyle, spec.categoryAxisLabelRotationDeg, spec.categoryAxisLabelAlign, spec.categoryAxisLineColor, spec.categoryAxisLineHidden, spec.categoryAxisMajorTickMark, spec.categoryAxisMinorTickMark);
4829
+ if (N > 0 && !labelsHidden) axes += renderCategoryAxis(f, spec.kind === "bar" ? "vertical" : "horizontal", spec.categories, N, spec.categoryAxisTickLabelSkip ?? 1, spec.categoryAxisLabelStyle, spec.categoryAxisLabelRotationDeg, spec.categoryAxisLabelAlign, spec.categoryAxisLineColor, spec.categoryAxisLineHidden, spec.categoryAxisMajorTickMark, spec.categoryAxisMinorTickMark, barLabels?.lines);
4704
4830
  }
4705
4831
  if (!isCombo) switch (spec.kind) {
4706
4832
  case "column":
@@ -4732,19 +4858,30 @@ const renderChart = (shape, x, y, w, h, transform, theme, groupReflected) => {
4732
4858
  }
4733
4859
  const emptyHint = finiteCount === 0 ? chartText(f, f.plotX + f.plotW / 2, f.plotY + f.plotH / 2, `text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="12" fill="#9CA3AF"`, `${escapeXml(`chart (${spec.kind}) — no data`)}`) : "";
4734
4860
  const axisTitleAttrs = (style) => {
4735
- const sz = style?.sizePt ?? 11;
4861
+ const sz = style?.sizePt ?? DEFAULT_AXIS_TITLE_PT;
4736
4862
  const fill = style?.color ?? "#374151";
4737
4863
  const weight = style?.bold === false ? "400" : "600";
4738
4864
  const italicAttr = style?.italic ? " font-style=\"italic\"" : "";
4739
4865
  return `font-family="sans-serif" font-size="${chartFontPx(sz)}" fill="${fill}" font-weight="${weight}"${italicAttr}`;
4740
4866
  };
4741
- const valueAxisTitleRot = spec.valueAxisTitleRotationDeg ?? -90;
4742
- const valueAxisTitleSvg = spec.valueAxisTitle ? chartText(f, f.plotX - 26, f.plotY + f.plotH / 2, `text-anchor="middle" ${axisTitleAttrs(spec.valueAxisTitleStyle)} transform="rotate(${valueAxisTitleRot} ${px(f.plotX - 26)} ${px(f.plotY + f.plotH / 2)})"`, `${escapeXml(spec.valueAxisTitle)}`) : "";
4743
- const catTitleRot = spec.categoryAxisTitleRotationDeg ?? 0;
4744
- const catTitleCx = f.plotX + f.plotW / 2;
4745
- const catTitleCy = f.plotY + f.plotH + 28;
4746
- const catTitleTransform = catTitleRot !== 0 ? ` transform="rotate(${catTitleRot} ${px(catTitleCx)} ${px(catTitleCy)})"` : "";
4747
- const categoryAxisTitleSvg = spec.categoryAxisTitle ? chartText(f, catTitleCx, catTitleCy, `text-anchor="middle" ${axisTitleAttrs(spec.categoryAxisTitleStyle)}${catTitleTransform}`, `${escapeXml(spec.categoryAxisTitle)}`) : "";
4867
+ const bottomTitle = {
4868
+ x: f.plotX + f.plotW / 2,
4869
+ y: f.plotY + f.plotH + 28
4870
+ };
4871
+ const sideTitle = {
4872
+ x: barLabels ? f.plotX - CATEGORY_LABEL_GAP - barLabels.widthPx - barCategoryTitleRow / 2 : f.plotX - 26,
4873
+ y: f.plotY + f.plotH / 2
4874
+ };
4875
+ const isBar = spec.kind === "bar";
4876
+ const axisTitleSvg = (title, style, rotationDeg, side) => {
4877
+ if (!title) return "";
4878
+ const at = side ? sideTitle : bottomTitle;
4879
+ const rot = rotationDeg ?? (side ? -90 : 0);
4880
+ const transform = rot !== 0 ? ` transform="rotate(${rot} ${px(at.x)} ${px(at.y)})"` : "";
4881
+ return chartText(f, at.x, at.y, `text-anchor="middle" ${axisTitleAttrs(style)}${transform}`, escapeXml(title));
4882
+ };
4883
+ const valueAxisTitleSvg = axisTitleSvg(spec.valueAxisTitle, spec.valueAxisTitleStyle, spec.valueAxisTitleRotationDeg, !isBar);
4884
+ const categoryAxisTitleSvg = axisTitleSvg(spec.categoryAxisTitle, spec.categoryAxisTitleStyle, spec.categoryAxisTitleRotationDeg, isBar);
4748
4885
  return [
4749
4886
  `<g${transform}>`,
4750
4887
  `<rect x="${px(f.x)}" y="${px(f.y)}" width="${px(f.w)}" height="${px(f.h)}" fill="${spec.chartAreaFill ?? "#FFFFFF"}" stroke="${spec.chartAreaStrokeColor ?? "none"}" stroke-width="0.6"${spec.roundedCorners ? " rx=\"6\" ry=\"6\"" : ""}/>`,
@@ -4890,9 +5027,9 @@ const renderTableCellText = (cell, paragraphs, cx, cy, cw, ch, color, pres, shap
4890
5027
  const vertical = verticalTextStyle(getTableCellTextDirection(cell));
4891
5028
  const directionStyle = `${vertical.declarations}${vertical.transform ? `;transform:${vertical.transform}` : ""}`;
4892
5029
  const justify = vAnchor === "top" ? "flex-start" : vAnchor === "bottom" ? "flex-end" : "center";
4893
- const familyFont = themeFace ? `${escapeXml(themeFace)}, ${DEFAULT_FONT}` : DEFAULT_FONT;
5030
+ const familyFont = escapeXml(browserFontFamily(themeFace));
4894
5031
  const body = renderHtmlParagraphs(paraData, numberLabels, theme, 1, DEFAULT_BODY_PT, color).join("");
4895
- return `<foreignObject x="${px(innerX)}" y="${px(innerY)}" width="${px(innerW)}" height="${px(innerH)}"><div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;flex-direction:column;justify-content:${justify};width:100%;height:100%;box-sizing:border-box;overflow:hidden;line-height:${LINE_HEIGHT};font-family:${familyFont};color:${color};word-break:break-word;${directionStyle}">${body}</div></foreignObject>${fillOverlay()}${bevelOverlay()}`;
5032
+ return `<foreignObject x="${px(innerX)}" y="${px(innerY)}" width="${px(innerW)}" height="${px(innerH)}"><div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;flex-direction:column;justify-content:${justify};width:100%;height:100%;box-sizing:border-box;overflow:hidden;line-height:${LINE_HEIGHT};font-family:${familyFont};color:${color};${NO_AUTOSPACE}word-break:break-word;${directionStyle}">${body}</div></foreignObject>${fillOverlay()}${bevelOverlay()}`;
4896
5033
  };
4897
5034
  const TWO_STOP_TABLE_GRADIENT_EXPONENT = 1.85;
4898
5035
  const TWO_STOP_TABLE_GRADIENT_SAMPLES = 16;
@@ -5479,7 +5616,7 @@ const renderShapeContent = (shape, pres, theme, ctx) => {
5479
5616
  }
5480
5617
  if (kind === "graphicFrame") {
5481
5618
  if (isChartShape(shape)) {
5482
- const chartSvg = renderChart(shape, x, y, w, h, transform, theme, ctx.groupReflected);
5619
+ const chartSvg = renderChart(shape, x, y, w, h, transform, theme, ctx.groupReflected, ctx.measure);
5483
5620
  if (chartSvg) return chartSvg;
5484
5621
  }
5485
5622
  if (isTableShape(shape)) {
@@ -5867,12 +6004,23 @@ const detectImageFormatLocal = (bytes) => {
5867
6004
  //#endregion
5868
6005
  //#region src/audit.ts
5869
6006
  const DEFAULT_TOLERANCE_PX = 1;
6007
+ const PT_PER_PX = 72 / 96;
6008
+ const SHORT_LAST_LINE_MAX_CHARS = 2;
6009
+ const LETTER_OR_DIGIT = /[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}\p{Nd}]/u;
6010
+ const KATAKANA = /[\p{Script=Katakana}\u30FC]/u;
6011
+ const isWordChar = (ch) => LETTER_OR_DIGIT.test(ch) || KATAKANA.test(ch);
6012
+ const splitsWord = (before, after) => LETTER_OR_DIGIT.test(before) && LETTER_OR_DIGIT.test(after) || KATAKANA.test(before) && KATAKANA.test(after);
6013
+ const charBefore = (s, i) => {
6014
+ return (i >= 2 ? s.codePointAt(i - 2) : 0) > 65535 ? s.slice(i - 2, i) : s.slice(i - 1, i);
6015
+ };
6016
+ const charAt = (s, i) => i < s.length ? String.fromCodePoint(s.codePointAt(i)) : "";
5870
6017
  const round2 = (n) => Math.round(n * 100) / 100;
5871
6018
  function* walkShapes(shapes) {
5872
6019
  for (const shape of shapes) if (getShapeKind(shape) !== "group") yield shape;
5873
6020
  }
5874
6021
  const passthroughFamily = (family) => family ?? "Carlito";
5875
- const auditShape = (pres, theme, shape, slideIndex, measure, tolerancePx, reportSoftWraps, issues) => {
6022
+ const auditShape = (pres, theme, shape, slideIndex, measure, options, issues) => {
6023
+ const { tolerancePx, reportSoftWraps, reportBadWraps, minFontSizePt } = options;
5876
6024
  const kind = getShapeKind(shape);
5877
6025
  if (kind !== "shape" && kind !== "graphicFrame") return;
5878
6026
  const bounds = getShapeBoundsResolved(pres, shape);
@@ -5917,51 +6065,65 @@ const auditShape = (pres, theme, shape, slideIndex, measure, tolerancePx, report
5917
6065
  resolveFamily: passthroughFamily
5918
6066
  });
5919
6067
  const core = layoutCore(input, measure);
5920
- const frame = vert === "cw90" || vert === "cw270" ? {
5921
- x: input.boxXpx + input.boxWpx / 2 - input.boxHpx / 2,
5922
- y: input.boxYpx + input.boxHpx / 2 - input.boxWpx / 2,
5923
- w: input.boxHpx,
5924
- h: input.boxWpx
6068
+ const rotated = vert === "cw90" || vert === "cw270";
6069
+ const frameOf = (x, y, w, h) => rotated ? {
6070
+ x: x + w / 2 - h / 2,
6071
+ y: y + h / 2 - w / 2,
6072
+ w: h,
6073
+ h: w
5925
6074
  } : {
5926
- x: input.boxXpx,
5927
- y: input.boxYpx,
5928
- w: input.boxWpx,
5929
- h: input.boxHpx
6075
+ x,
6076
+ y,
6077
+ w,
6078
+ h
5930
6079
  };
6080
+ const frame = frameOf(input.boxXpx, input.boxYpx, input.boxWpx, input.boxHpx);
6081
+ const shapeFrame = frameOf(bounds.x / EMU_PER_PX, bounds.y / EMU_PER_PX, bounds.w / EMU_PER_PX, bounds.h / EMU_PER_PX);
6082
+ let inkLeft = Infinity;
6083
+ let inkRight = -Infinity;
5931
6084
  let inkTop = Infinity;
5932
6085
  let inkBottom = -Infinity;
5933
- let overflowX = 0;
5934
- let anyInk = false;
6086
+ let lineHeight = 0;
5935
6087
  for (const p of core.placements) {
5936
6088
  const toks = [...p.line.tokens];
5937
6089
  while (toks.length > 0 && (toks[toks.length - 1].isSpace || toks[toks.length - 1].isBreak)) toks.pop();
5938
6090
  if (!toks.some((t) => !t.isSpace && !t.isBreak)) continue;
5939
- anyInk = true;
5940
6091
  let lineW = 0;
5941
6092
  for (const t of toks) if (!t.isBreak) lineW += t.width;
5942
- const left = p.line.textAnchor === "middle" ? p.line.anchorX - lineW / 2 : p.line.textAnchor === "end" ? p.line.anchorX - lineW : p.line.anchorX;
5943
- const over = Math.max(frame.x - (left + p.dx), left + p.dx + lineW - (frame.x + frame.w));
5944
- if (over > overflowX) overflowX = over;
6093
+ const left = (p.line.textAnchor === "middle" ? p.line.anchorX - lineW / 2 : p.line.textAnchor === "end" ? p.line.anchorX - lineW : p.line.anchorX) + p.dx;
6094
+ inkLeft = Math.min(inkLeft, left);
6095
+ inkRight = Math.max(inkRight, left + lineW);
5945
6096
  inkTop = Math.min(inkTop, p.baselineY - p.line.ascent);
5946
6097
  inkBottom = Math.max(inkBottom, p.baselineY + p.line.descent);
6098
+ lineHeight = Math.max(lineHeight, p.line.ascent + p.line.descent);
5947
6099
  }
5948
- if (!anyInk) return;
6100
+ if (inkLeft === Infinity) return;
5949
6101
  const approximate = detectApproximate(input.paragraphs, measure);
5950
6102
  const slideRef = {
5951
6103
  slideIndex,
5952
6104
  shapeName: getShapeName(shape),
5953
6105
  approximate
5954
6106
  };
6107
+ const overflowXOf = (f) => Math.max(0, f.x - inkLeft, inkRight - (f.x + f.w));
6108
+ const overflowYOf = (f) => Math.max(0, f.y - inkTop, inkBottom - (f.y + f.h));
6109
+ const shapeOverflowXOf = (f) => Math.max(0, f.x - inkLeft) + Math.max(0, inkRight - (f.x + f.w));
6110
+ const shapeOverflowYOf = (f) => Math.max(0, f.y - inkTop) + Math.max(0, inkBottom - (f.y + f.h));
6111
+ const lineHeightPx = round2(lineHeight);
6112
+ const overflowX = overflowXOf(frame);
5955
6113
  if (overflowX > tolerancePx) issues.push({
5956
6114
  ...slideRef,
5957
6115
  kind: "overflow-x",
5958
- overflowPx: round2(overflowX)
6116
+ overflowPx: round2(overflowX),
6117
+ shapeOverflowPx: round2(shapeOverflowXOf(shapeFrame)),
6118
+ lineHeightPx
5959
6119
  });
5960
- const overflowY = Math.max(frame.y - inkTop, inkBottom - (frame.y + frame.h));
6120
+ const overflowY = overflowYOf(frame);
5961
6121
  if (overflowY > tolerancePx) issues.push({
5962
6122
  ...slideRef,
5963
6123
  kind: "overflow-y",
5964
- overflowPx: round2(overflowY)
6124
+ overflowPx: round2(overflowY),
6125
+ shapeOverflowPx: round2(shapeOverflowYOf(shapeFrame)),
6126
+ lineHeightPx
5965
6127
  });
5966
6128
  if (reportSoftWraps && vert === "none") {
5967
6129
  const lineCounts = /* @__PURE__ */ new Map();
@@ -5977,6 +6139,94 @@ const auditShape = (pres, theme, shape, slideIndex, measure, tolerancePx, report
5977
6139
  });
5978
6140
  });
5979
6141
  }
6142
+ if (reportBadWraps && vert === "none") auditLineBreaks(input.paragraphs, core.placements, slideRef, issues);
6143
+ if (minFontSizePt !== void 0) {
6144
+ let smallestPx = Infinity;
6145
+ for (const para of input.paragraphs) for (const piece of para.pieces) if (!piece.isBreak && piece.text.trim() !== "" && piece.sizePx < smallestPx) smallestPx = piece.sizePx;
6146
+ const sizePt = Math.round(smallestPx * PT_PER_PX * 10) / 10;
6147
+ if (sizePt < minFontSizePt) issues.push({
6148
+ ...slideRef,
6149
+ kind: "small-font",
6150
+ sizePt
6151
+ });
6152
+ }
6153
+ };
6154
+ const auditLineBreaks = (paragraphs, placements, slideRef, issues) => {
6155
+ const linesByParagraph = /* @__PURE__ */ new Map();
6156
+ for (const { line } of placements) {
6157
+ const text = line.tokens.map((t) => t.isTab ? " " : t.text).join("");
6158
+ if (text.trim() === "") continue;
6159
+ const lines = linesByParagraph.get(line.paraIndex);
6160
+ if (lines) lines.push(text);
6161
+ else linesByParagraph.set(line.paraIndex, [text]);
6162
+ }
6163
+ for (const [paragraphIndex, lines] of linesByParagraph) auditParagraphBreaks(paragraphs[paragraphIndex].pieces.map((p) => p.isBreak ? "\n" : p.text).join(""), lines, {
6164
+ ...slideRef,
6165
+ paragraphIndex
6166
+ }, issues);
6167
+ };
6168
+ const auditParagraphBreaks = (source, lines, ref, issues) => {
6169
+ const found = [];
6170
+ const wordsByStart = /* @__PURE__ */ new Map();
6171
+ let previousEnd = -1;
6172
+ let wrappedLines = 0;
6173
+ const closeSegment = (lastLine) => {
6174
+ const visible = [...lastLine.replace(/\s/gu, "")];
6175
+ if (wrappedLines > 0 && visible.length <= SHORT_LAST_LINE_MAX_CHARS) found.push({
6176
+ ...ref,
6177
+ kind: "short-last-line",
6178
+ text: lastLine.trim()
6179
+ });
6180
+ };
6181
+ for (let i = 0; i < lines.length; i++) {
6182
+ const start = source.indexOf(lines[i], Math.max(previousEnd, 0));
6183
+ if (start < 0) return;
6184
+ if (previousEnd >= 0) {
6185
+ const gap = source.slice(previousEnd, start);
6186
+ if (gap.includes("\n")) {
6187
+ closeSegment(lines[i - 1]);
6188
+ wrappedLines = 0;
6189
+ } else {
6190
+ wrappedLines++;
6191
+ if (gap === "" && splitsWord(charBefore(source, start), charAt(source, start))) {
6192
+ let wordStart = start;
6193
+ while (wordStart > 0 && isWordChar(charBefore(source, wordStart))) wordStart -= charBefore(source, wordStart).length;
6194
+ let word = wordsByStart.get(wordStart);
6195
+ if (!word) {
6196
+ let end = start;
6197
+ while (end < source.length && isWordChar(charAt(source, end))) end += charAt(source, end).length;
6198
+ word = {
6199
+ start: wordStart,
6200
+ end,
6201
+ breaks: []
6202
+ };
6203
+ wordsByStart.set(wordStart, word);
6204
+ found.push(word);
6205
+ }
6206
+ word.breaks.push(start);
6207
+ }
6208
+ }
6209
+ }
6210
+ previousEnd = start + lines[i].length;
6211
+ }
6212
+ closeSegment(lines[lines.length - 1]);
6213
+ for (const entry of found) {
6214
+ if ("kind" in entry) {
6215
+ issues.push(entry);
6216
+ continue;
6217
+ }
6218
+ let text = "";
6219
+ let from = entry.start;
6220
+ for (const at of entry.breaks) {
6221
+ text += `${source.slice(from, at)}|`;
6222
+ from = at;
6223
+ }
6224
+ issues.push({
6225
+ ...ref,
6226
+ kind: "word-break",
6227
+ text: text + source.slice(from, entry.end)
6228
+ });
6229
+ }
5980
6230
  };
5981
6231
  const detectApproximate = (paragraphs, measure) => {
5982
6232
  const seen = /* @__PURE__ */ new Set();
@@ -6001,19 +6251,25 @@ const detectApproximate = (paragraphs, measure) => {
6001
6251
  /**
6002
6252
  * Measures every text body in the deck and reports text that escapes its box
6003
6253
  * (`overflow-x` / `overflow-y`) and, opt-in, paragraphs that soft-wrap
6004
- * (`soft-wrap`, 段落ち).
6254
+ * (`soft-wrap`, 段落ち), wraps that split a word or strand a line of one or
6255
+ * two characters (`word-break`, `short-last-line`), and text below a minimum
6256
+ * size (`small-font`).
6005
6257
  *
6006
6258
  * Table cell text is not audited (v1 covers shape text bodies, including
6007
6259
  * placeholders and shapes inside groups).
6008
6260
  */
6009
6261
  const auditTextLayout = (pres, options = {}) => {
6010
6262
  const measure = options.measureText ?? defaultMeasurer;
6011
- const tolerancePx = options.tolerancePx ?? DEFAULT_TOLERANCE_PX;
6012
- const reportSoftWraps = options.reportSoftWraps ?? false;
6263
+ const resolved = {
6264
+ tolerancePx: options.tolerancePx ?? DEFAULT_TOLERANCE_PX,
6265
+ reportSoftWraps: options.reportSoftWraps ?? false,
6266
+ reportBadWraps: options.reportBadWraps ?? false,
6267
+ minFontSizePt: options.minFontSizePt
6268
+ };
6013
6269
  const theme = getPresentationTheme(pres);
6014
6270
  const issues = [];
6015
6271
  const slides = getSlides(pres);
6016
- for (let slideIndex = 0; slideIndex < slides.length; slideIndex++) for (const shape of walkShapes(getSlideShapes(slides[slideIndex]))) auditShape(pres, theme, shape, slideIndex, measure, tolerancePx, reportSoftWraps, issues);
6272
+ for (let slideIndex = 0; slideIndex < slides.length; slideIndex++) for (const shape of walkShapes(getSlideShapes(slides[slideIndex]))) auditShape(pres, theme, shape, slideIndex, measure, resolved, issues);
6017
6273
  return issues;
6018
6274
  };
6019
6275
  //#endregion
@@ -6060,4 +6316,4 @@ function shapeTextAnchorOffset(pres, shape, options = {}) {
6060
6316
  //#endregion
6061
6317
  export { shapeTextRect as S, paragraphNumberLabels as _, renderSlideSvg as a, verticalTextStyle as b, AVG_GLYPH_W_RATIO as c, SERIF as d, TIMES as f, decimalSeparatorOf as g, substituteFamily as h, renderSlideLayoutSvg as i, MONO as l, isCjk as m, shapeTextAnchorOffset as n, renderTextEffectsSvg as o, defaultMeasurer as p, auditTextLayout as r, ARIAL as s, shapeAutoFitScale as t, SANS as u, textUnderlineStyle as v, resolveTextBodyRect as x, textColumnsStyle as y };
6062
6318
 
6063
- //# sourceMappingURL=src-u1-ERSHC.js.map
6319
+ //# sourceMappingURL=src-BGzSNWjt.js.map