@office-kit/pptx-preview 0.16.0 → 0.17.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/README.md +22 -6
- package/dist/{index-D1r4NkUr.d.ts → index-C8dvep3H.d.ts} +26 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +1 -1
- package/dist/{src-u1-ERSHC.js → src-ayE1F9lH.js} +195 -39
- package/dist/src-ayE1F9lH.js.map +1 -0
- package/package.json +3 -3
- package/dist/src-u1-ERSHC.js.map +0 -1
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ const measureText = buildFontkitMeasurer({
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
const issues = auditTextLayout(pres, { measureText, reportSoftWraps: true });
|
|
120
|
-
// → [{ kind: 'overflow-y', slideIndex: 2, shapeName: 'Title 1', overflowPx: 14.3, approximate: false }, …]
|
|
120
|
+
// → [{ kind: 'overflow-y', slideIndex: 2, shapeName: 'Title 1', overflowPx: 14.3, shapeOverflowPx: 9.5, lineHeightPx: 52.8, approximate: false }, …]
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
`auditTextLayout` measures every shape's text body — placeholders, text boxes,
|
|
@@ -125,11 +125,20 @@ autoshapes, shapes inside groups — with the same engine the preview renders
|
|
|
125
125
|
with, honouring the effective bodyPr cascade (insets, wrap, anchor, vertical
|
|
126
126
|
text, columns) and `normAutofit` shrinking. It returns one issue per finding:
|
|
127
127
|
|
|
128
|
-
| `kind`
|
|
129
|
-
|
|
|
130
|
-
| `overflow-x`
|
|
131
|
-
| `overflow-y`
|
|
132
|
-
| `soft-wrap`
|
|
128
|
+
| `kind` | Meaning | Extra fields |
|
|
129
|
+
| ----------------- | ----------------------------------------------------------------------------------- | ------------------------------ |
|
|
130
|
+
| `overflow-x` | Text ink escapes the box horizontally (typically `wrap="none"`) | `overflowPx` and the two below |
|
|
131
|
+
| `overflow-y` | Wrapped text is taller than the box (or escapes the top when bottom-anchored) | `overflowPx` and the two below |
|
|
132
|
+
| `soft-wrap` | A paragraph wrapped onto more lines than its authored `<a:br>` count (段落ち) | `paragraphIndex`, `extraLines` |
|
|
133
|
+
| `word-break` | A wrap split a word (Latin / Greek / Cyrillic letters or digits, or a katakana run) | `paragraphIndex`, `text` |
|
|
134
|
+
| `short-last-line` | A wrapped paragraph ends on a line of one or two characters (泣き別れ) | `paragraphIndex`, `text` |
|
|
135
|
+
| `small-font` | The shape draws text below `minFontSizePt`, after autofit shrinking | `sizePt` |
|
|
136
|
+
|
|
137
|
+
`overflowPx` is measured against the text area (the box minus its insets);
|
|
138
|
+
`shapeOverflowPx` is how much ink lies outside the shape itself, both sides
|
|
139
|
+
added (a centre-anchored body spills above and below; 0 while the spill stays
|
|
140
|
+
inside the insets, where nobody sees it); `lineHeightPx` is the
|
|
141
|
+
tallest line's height, about how far one misjudged wrap moves the text.
|
|
133
142
|
|
|
134
143
|
Every issue also carries `slideIndex`, `shapeName`, and `approximate` —
|
|
135
144
|
`true` when a width had to be estimated (the heuristic measurer, or glyphs the
|
|
@@ -146,6 +155,13 @@ Options:
|
|
|
146
155
|
- `reportSoftWraps` — opt into `soft-wrap` issues. Off by default because
|
|
147
156
|
wrapping is normal for body text; turn it on when auditing titles or labels
|
|
148
157
|
meant to stay on one line.
|
|
158
|
+
- `reportBadWraps` — opt into `word-break` and `short-last-line` issues
|
|
159
|
+
(horizontal text only). A word inside a kanji compound cannot be told apart
|
|
160
|
+
from its neighbours, so only the scripts listed above are checked.
|
|
161
|
+
A `word-break` issue is reported once per word; its `text` is the word with
|
|
162
|
+
`|` at each line break. `short-last-line` counts characters, not words, so a
|
|
163
|
+
Latin last line of `to` is reported and one of `the` is not.
|
|
164
|
+
- `minFontSizePt` — report `small-font` for shapes with text below this size.
|
|
149
165
|
|
|
150
166
|
Accuracy: the bundled fonts are metric-compatible with the common default fonts, so
|
|
151
167
|
verdicts for Calibri / Cambria / Arial / Times / Courier decks match real glyph
|
|
@@ -108,14 +108,29 @@ interface IssueBase {
|
|
|
108
108
|
readonly approximate: boolean;
|
|
109
109
|
}
|
|
110
110
|
type TextAuditIssue = (IssueBase & {
|
|
111
|
-
readonly kind: 'overflow-x' | 'overflow-y'; /** How far the text ink escapes the box, in px at 96 DPI. */
|
|
111
|
+
readonly kind: 'overflow-x' | 'overflow-y'; /** How far the text ink escapes the text area (the box minus its insets), in px at 96 DPI. */
|
|
112
112
|
readonly overflowPx: number;
|
|
113
|
+
/** How much ink lies outside the shape itself along that axis, both sides added
|
|
114
|
+
* (a centred body spills half above, half below); 0 while it stays within the insets. */
|
|
115
|
+
readonly shapeOverflowPx: number; /** The tallest line's height in px: about how far one misjudged wrap moves the text. */
|
|
116
|
+
readonly lineHeightPx: number;
|
|
113
117
|
}) | (IssueBase & {
|
|
114
118
|
readonly kind: 'soft-wrap';
|
|
115
119
|
readonly paragraphIndex: number;
|
|
116
120
|
/** Lines beyond the paragraph's authored line count (1 + explicit
|
|
117
121
|
* breaks) — i.e. how many times the text wrapped on its own. */
|
|
118
122
|
readonly extraLines: number;
|
|
123
|
+
}) | (IssueBase & {
|
|
124
|
+
/** A wrap split a word: Latin / Greek / Cyrillic letters or digits, or a katakana run. */readonly kind: 'word-break';
|
|
125
|
+
readonly paragraphIndex: number; /** The split word, with `|` where the line breaks. */
|
|
126
|
+
readonly text: string;
|
|
127
|
+
}) | (IssueBase & {
|
|
128
|
+
/** A wrapped paragraph whose last line holds only a character or two (泣き別れ). */readonly kind: 'short-last-line';
|
|
129
|
+
readonly paragraphIndex: number; /** The text of that last line. */
|
|
130
|
+
readonly text: string;
|
|
131
|
+
}) | (IssueBase & {
|
|
132
|
+
readonly kind: 'small-font'; /** The smallest font size drawn in the shape, after autofit shrinking. */
|
|
133
|
+
readonly sizePt: number;
|
|
119
134
|
});
|
|
120
135
|
interface AuditTextLayoutOptions {
|
|
121
136
|
/** Measurer for run widths / vertical metrics. Defaults to the heuristic
|
|
@@ -129,11 +144,19 @@ interface AuditTextLayoutOptions {
|
|
|
129
144
|
* breaks author (段落ち). Off by default: wrapping is normal for body
|
|
130
145
|
* text, so this is opt-in for single-line-intent content like titles. */
|
|
131
146
|
readonly reportSoftWraps?: boolean;
|
|
147
|
+
/** Also report wraps that split a word (`word-break`) and wrapped
|
|
148
|
+
* paragraphs that end on a line of one or two characters
|
|
149
|
+
* (`short-last-line`). Horizontal text only. */
|
|
150
|
+
readonly reportBadWraps?: boolean;
|
|
151
|
+
/** Report shapes drawing text below this size in points (`small-font`). */
|
|
152
|
+
readonly minFontSizePt?: number;
|
|
132
153
|
}
|
|
133
154
|
/**
|
|
134
155
|
* Measures every text body in the deck and reports text that escapes its box
|
|
135
156
|
* (`overflow-x` / `overflow-y`) and, opt-in, paragraphs that soft-wrap
|
|
136
|
-
* (`soft-wrap`, 段落ち)
|
|
157
|
+
* (`soft-wrap`, 段落ち), wraps that split a word or strand a line of one or
|
|
158
|
+
* two characters (`word-break`, `short-last-line`), and text below a minimum
|
|
159
|
+
* size (`small-font`).
|
|
137
160
|
*
|
|
138
161
|
* Table cell text is not audited (v1 covers shape text bodies, including
|
|
139
162
|
* placeholders and shapes inside groups).
|
|
@@ -272,4 +295,4 @@ declare function shapeTextAnchorOffset(pres: PresentationData, shape: SlideShape
|
|
|
272
295
|
declare function textUnderlineStyle(underline: TextFormat['underline'], color: string, includeDecorationColor?: boolean): string;
|
|
273
296
|
//#endregion
|
|
274
297
|
export { substituteFamily as A, RenderSlideOptions as C, TextLayoutMode as D, TIMES as E, TextMeasurer as O, MeasureResult as S, SERIF as T, renderSlideSvg as _, VerticalTextStyle as a, FontSpec as b, TextRectFractions as c, decimalSeparatorOf as d, paragraphNumberLabels as f, renderSlideLayoutSvg as g, auditTextLayout as h, shapeTextAnchorOffset as i, defaultMeasurer as k, resolveTextBodyRect as l, TextAuditIssue as m, ShapeAutoFitScaleOptions as n, textColumnsStyle as o, AuditTextLayoutOptions as p, shapeAutoFitScale as r, verticalTextStyle as s, textUnderlineStyle as t, shapeTextRect as u, renderTextEffectsSvg as v, SANS as w, MONO as x, ARIAL as y };
|
|
275
|
-
//# sourceMappingURL=index-
|
|
298
|
+
//# sourceMappingURL=index-C8dvep3H.d.ts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as substituteFamily, C as RenderSlideOptions, D as TextLayoutMode, E as TIMES, O as TextMeasurer, S as MeasureResult, T as SERIF, _ as renderSlideSvg, a as VerticalTextStyle, b as FontSpec, c as TextRectFractions, d as decimalSeparatorOf, f as paragraphNumberLabels, g as renderSlideLayoutSvg, h as auditTextLayout, i as shapeTextAnchorOffset, k as defaultMeasurer, l as resolveTextBodyRect, m as TextAuditIssue, n as ShapeAutoFitScaleOptions, o as textColumnsStyle, p as AuditTextLayoutOptions, r as shapeAutoFitScale, s as verticalTextStyle, t as textUnderlineStyle, u as shapeTextRect, v as renderTextEffectsSvg, w as SANS, x as MONO, y as ARIAL } from "./index-
|
|
1
|
+
import { A as substituteFamily, C as RenderSlideOptions, D as TextLayoutMode, E as TIMES, O as TextMeasurer, S as MeasureResult, T as SERIF, _ as renderSlideSvg, a as VerticalTextStyle, b as FontSpec, c as TextRectFractions, d as decimalSeparatorOf, f as paragraphNumberLabels, g as renderSlideLayoutSvg, h as auditTextLayout, i as shapeTextAnchorOffset, k as defaultMeasurer, l as resolveTextBodyRect, m as TextAuditIssue, n as ShapeAutoFitScaleOptions, o as textColumnsStyle, p as AuditTextLayoutOptions, r as shapeAutoFitScale, s as verticalTextStyle, t as textUnderlineStyle, u as shapeTextRect, v as renderTextEffectsSvg, w as SANS, x as MONO, y as ARIAL } from "./index-C8dvep3H.js";
|
|
2
2
|
export { ARIAL, type AuditTextLayoutOptions, type FontSpec, MONO, type MeasureResult, type RenderSlideOptions, SANS, SERIF, type ShapeAutoFitScaleOptions, TIMES, type TextAuditIssue, type TextLayoutMode, type TextMeasurer, type TextRectFractions, type VerticalTextStyle, auditTextLayout, decimalSeparatorOf, defaultMeasurer, paragraphNumberLabels, renderSlideLayoutSvg as renderSlideLayoutToSvg, renderSlideSvg as renderSlideToSvg, renderTextEffectsSvg, resolveTextBodyRect, shapeAutoFitScale, shapeTextAnchorOffset, shapeTextRect, substituteFamily, textColumnsStyle, textUnderlineStyle, verticalTextStyle };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { S as shapeTextRect, _ as paragraphNumberLabels, a as renderSlideSvg, b as verticalTextStyle, d as SERIF, f as TIMES, g as decimalSeparatorOf, h as substituteFamily, i as renderSlideLayoutSvg, l as MONO, n as shapeTextAnchorOffset, o as renderTextEffectsSvg, p as defaultMeasurer, r as auditTextLayout, s as ARIAL, t as shapeAutoFitScale, u as SANS, v as textUnderlineStyle, x as resolveTextBodyRect, y as textColumnsStyle } from "./src-
|
|
1
|
+
import { S as shapeTextRect, _ as paragraphNumberLabels, a as renderSlideSvg, b as verticalTextStyle, d as SERIF, f as TIMES, g as decimalSeparatorOf, h as substituteFamily, i as renderSlideLayoutSvg, l as MONO, n as shapeTextAnchorOffset, o as renderTextEffectsSvg, p as defaultMeasurer, r as auditTextLayout, s as ARIAL, t as shapeAutoFitScale, u as SANS, v as textUnderlineStyle, x as resolveTextBodyRect, y as textColumnsStyle } from "./src-ayE1F9lH.js";
|
|
2
2
|
export { ARIAL, MONO, SANS, SERIF, TIMES, auditTextLayout, decimalSeparatorOf, defaultMeasurer, paragraphNumberLabels, renderSlideLayoutSvg as renderSlideLayoutToSvg, renderSlideSvg as renderSlideToSvg, renderTextEffectsSvg, resolveTextBodyRect, shapeAutoFitScale, shapeTextAnchorOffset, shapeTextRect, substituteFamily, textColumnsStyle, textUnderlineStyle, verticalTextStyle };
|
package/dist/node.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as substituteFamily, C as RenderSlideOptions, D as TextLayoutMode, E as TIMES, O as TextMeasurer, S as MeasureResult, T as SERIF, _ as renderSlideSvg, a as VerticalTextStyle, b as FontSpec, c as TextRectFractions, d as decimalSeparatorOf, f as paragraphNumberLabels, g as renderSlideLayoutSvg, h as auditTextLayout, i as shapeTextAnchorOffset, k as defaultMeasurer, l as resolveTextBodyRect, m as TextAuditIssue, n as ShapeAutoFitScaleOptions, o as textColumnsStyle, p as AuditTextLayoutOptions, r as shapeAutoFitScale, s as verticalTextStyle, t as textUnderlineStyle, u as shapeTextRect, v as renderTextEffectsSvg, w as SANS, x as MONO, y as ARIAL } from "./index-
|
|
1
|
+
import { A as substituteFamily, C as RenderSlideOptions, D as TextLayoutMode, E as TIMES, O as TextMeasurer, S as MeasureResult, T as SERIF, _ as renderSlideSvg, a as VerticalTextStyle, b as FontSpec, c as TextRectFractions, d as decimalSeparatorOf, f as paragraphNumberLabels, g as renderSlideLayoutSvg, h as auditTextLayout, i as shapeTextAnchorOffset, k as defaultMeasurer, l as resolveTextBodyRect, m as TextAuditIssue, n as ShapeAutoFitScaleOptions, o as textColumnsStyle, p as AuditTextLayoutOptions, r as shapeAutoFitScale, s as verticalTextStyle, t as textUnderlineStyle, u as shapeTextRect, v as renderTextEffectsSvg, w as SANS, x as MONO, y as ARIAL } from "./index-C8dvep3H.js";
|
|
2
2
|
import { PresentationData, SlideData } from "@office-kit/pptx";
|
|
3
3
|
|
|
4
4
|
//#region src/measure.d.ts
|
package/dist/node.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as shapeTextRect, _ as paragraphNumberLabels, a as renderSlideSvg, b as verticalTextStyle, c as AVG_GLYPH_W_RATIO, d as SERIF, f as TIMES, g as decimalSeparatorOf, h as substituteFamily, i as renderSlideLayoutSvg, l as MONO, m as isCjk, n as shapeTextAnchorOffset, o as renderTextEffectsSvg, p as defaultMeasurer, r as auditTextLayout, s as ARIAL, t as shapeAutoFitScale, u as SANS, v as textUnderlineStyle, x as resolveTextBodyRect, y as textColumnsStyle } from "./src-
|
|
1
|
+
import { S as shapeTextRect, _ as paragraphNumberLabels, a as renderSlideSvg, b as verticalTextStyle, c as AVG_GLYPH_W_RATIO, d as SERIF, f as TIMES, g as decimalSeparatorOf, h as substituteFamily, i as renderSlideLayoutSvg, l as MONO, m as isCjk, n as shapeTextAnchorOffset, o as renderTextEffectsSvg, p as defaultMeasurer, r as auditTextLayout, s as ARIAL, t as shapeAutoFitScale, u as SANS, v as textUnderlineStyle, x as resolveTextBodyRect, y as textColumnsStyle } from "./src-ayE1F9lH.js";
|
|
2
2
|
import { getSlideSize } from "@office-kit/pptx";
|
|
3
3
|
import { Resvg } from "@resvg/resvg-js";
|
|
4
4
|
import * as fontkit from "fontkit";
|
|
@@ -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)
|
|
655
|
-
|
|
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
|
-
|
|
1048
|
-
|
|
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(
|
|
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(
|
|
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;
|
|
@@ -2773,7 +2809,7 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
|
|
|
2773
2809
|
const defaultPt = placeholderDefaultPt(phType);
|
|
2774
2810
|
const themeFonts = getPresentationFonts(pres);
|
|
2775
2811
|
const themeFace = phType === "title" || phType === "ctrTitle" ? themeFonts?.majorLatin ?? null : themeFonts?.minorLatin ?? null;
|
|
2776
|
-
const effectiveDefaultFont =
|
|
2812
|
+
const effectiveDefaultFont = escapeXml(browserFontFamily(themeFace));
|
|
2777
2813
|
let effectiveBody;
|
|
2778
2814
|
try {
|
|
2779
2815
|
effectiveBody = getShapeBodyPrEffective(pres, shape);
|
|
@@ -3080,10 +3116,11 @@ const renderHtmlParagraphs = (paraData, numberLabels, theme, autoFitScale, defau
|
|
|
3080
3116
|
};
|
|
3081
3117
|
}
|
|
3082
3118
|
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
|
-
|
|
3084
|
-
|
|
3119
|
+
const href = run.href ? safeLinkHref(run.href) : void 0;
|
|
3120
|
+
if (!href) return span;
|
|
3121
|
+
const targetAttrs = href.startsWith("#") ? "" : " target=\"_blank\" rel=\"noopener noreferrer\"";
|
|
3085
3122
|
const titleAttr = run.hrefTip ? ` title="${escapeXml(run.hrefTip)}"` : "";
|
|
3086
|
-
return `<a href="${escapeXml(
|
|
3123
|
+
return `<a href="${escapeXml(href)}"${targetAttrs}${titleAttr} style="color:inherit;text-decoration:inherit">${span}</a>`;
|
|
3087
3124
|
});
|
|
3088
3125
|
const firstRunPt = para.runs.find((run) => run.text !== "\n" && run.text !== "")?.sizePt ?? defaultPt;
|
|
3089
3126
|
let lineHeightCss = para.emptySizePt !== void 0 ? `line-height:${LINE_HEIGHT}` : "";
|
|
@@ -3097,7 +3134,7 @@ const renderHtmlParagraphs = (paraData, numberLabels, theme, autoFitScale, defau
|
|
|
3097
3134
|
const pStyles = [
|
|
3098
3135
|
"margin:0",
|
|
3099
3136
|
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 ?
|
|
3137
|
+
...para.lineSpacing?.kind === "pts" && para.runs[0]?.fmt?.font ? fontFamilyStyles(para.runs[0].fmt.font) : [],
|
|
3101
3138
|
...para.emptySizePt !== void 0 ? [`font-size:${(para.emptySizePt * PX_PER_PT * autoFitScale).toFixed(2)}px`] : [],
|
|
3102
3139
|
...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
3140
|
marginTopCss,
|
|
@@ -3132,7 +3169,7 @@ const renderHtmlParagraphs = (paraData, numberLabels, theme, autoFitScale, defau
|
|
|
3132
3169
|
else if (para.bulletDetail.sizePts !== null) bulletStyles.push(`font-size:${(para.bulletDetail.sizePts * PX_PER_PT * autoFitScale).toFixed(2)}px`);
|
|
3133
3170
|
else bulletStyles.push(`font-size:${baseBulletPx.toFixed(2)}px`);
|
|
3134
3171
|
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(
|
|
3172
|
+
bulletStyles.push(...fontFamilyStyles(bulletFont));
|
|
3136
3173
|
prefix = `<span style="${bulletStyles.join(";")}">${escapeXml(char)}</span>`;
|
|
3137
3174
|
}
|
|
3138
3175
|
paragraphs.push(`<p data-pptx-paragraph="${pi}" style="${pStyles.join(";")}">${prefix}${runHtmls.join("") || "​"}</p>`);
|
|
@@ -4890,7 +4927,7 @@ const renderTableCellText = (cell, paragraphs, cx, cy, cw, ch, color, pres, shap
|
|
|
4890
4927
|
const vertical = verticalTextStyle(getTableCellTextDirection(cell));
|
|
4891
4928
|
const directionStyle = `${vertical.declarations}${vertical.transform ? `;transform:${vertical.transform}` : ""}`;
|
|
4892
4929
|
const justify = vAnchor === "top" ? "flex-start" : vAnchor === "bottom" ? "flex-end" : "center";
|
|
4893
|
-
const familyFont =
|
|
4930
|
+
const familyFont = escapeXml(browserFontFamily(themeFace));
|
|
4894
4931
|
const body = renderHtmlParagraphs(paraData, numberLabels, theme, 1, DEFAULT_BODY_PT, color).join("");
|
|
4895
4932
|
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()}`;
|
|
4896
4933
|
};
|
|
@@ -5867,12 +5904,23 @@ const detectImageFormatLocal = (bytes) => {
|
|
|
5867
5904
|
//#endregion
|
|
5868
5905
|
//#region src/audit.ts
|
|
5869
5906
|
const DEFAULT_TOLERANCE_PX = 1;
|
|
5907
|
+
const PT_PER_PX = 72 / 96;
|
|
5908
|
+
const SHORT_LAST_LINE_MAX_CHARS = 2;
|
|
5909
|
+
const LETTER_OR_DIGIT = /[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}\p{Nd}]/u;
|
|
5910
|
+
const KATAKANA = /[\p{Script=Katakana}\u30FC]/u;
|
|
5911
|
+
const isWordChar = (ch) => LETTER_OR_DIGIT.test(ch) || KATAKANA.test(ch);
|
|
5912
|
+
const splitsWord = (before, after) => LETTER_OR_DIGIT.test(before) && LETTER_OR_DIGIT.test(after) || KATAKANA.test(before) && KATAKANA.test(after);
|
|
5913
|
+
const charBefore = (s, i) => {
|
|
5914
|
+
return (i >= 2 ? s.codePointAt(i - 2) : 0) > 65535 ? s.slice(i - 2, i) : s.slice(i - 1, i);
|
|
5915
|
+
};
|
|
5916
|
+
const charAt = (s, i) => i < s.length ? String.fromCodePoint(s.codePointAt(i)) : "";
|
|
5870
5917
|
const round2 = (n) => Math.round(n * 100) / 100;
|
|
5871
5918
|
function* walkShapes(shapes) {
|
|
5872
5919
|
for (const shape of shapes) if (getShapeKind(shape) !== "group") yield shape;
|
|
5873
5920
|
}
|
|
5874
5921
|
const passthroughFamily = (family) => family ?? "Carlito";
|
|
5875
|
-
const auditShape = (pres, theme, shape, slideIndex, measure,
|
|
5922
|
+
const auditShape = (pres, theme, shape, slideIndex, measure, options, issues) => {
|
|
5923
|
+
const { tolerancePx, reportSoftWraps, reportBadWraps, minFontSizePt } = options;
|
|
5876
5924
|
const kind = getShapeKind(shape);
|
|
5877
5925
|
if (kind !== "shape" && kind !== "graphicFrame") return;
|
|
5878
5926
|
const bounds = getShapeBoundsResolved(pres, shape);
|
|
@@ -5917,51 +5965,65 @@ const auditShape = (pres, theme, shape, slideIndex, measure, tolerancePx, report
|
|
|
5917
5965
|
resolveFamily: passthroughFamily
|
|
5918
5966
|
});
|
|
5919
5967
|
const core = layoutCore(input, measure);
|
|
5920
|
-
const
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5968
|
+
const rotated = vert === "cw90" || vert === "cw270";
|
|
5969
|
+
const frameOf = (x, y, w, h) => rotated ? {
|
|
5970
|
+
x: x + w / 2 - h / 2,
|
|
5971
|
+
y: y + h / 2 - w / 2,
|
|
5972
|
+
w: h,
|
|
5973
|
+
h: w
|
|
5925
5974
|
} : {
|
|
5926
|
-
x
|
|
5927
|
-
y
|
|
5928
|
-
w
|
|
5929
|
-
h
|
|
5975
|
+
x,
|
|
5976
|
+
y,
|
|
5977
|
+
w,
|
|
5978
|
+
h
|
|
5930
5979
|
};
|
|
5980
|
+
const frame = frameOf(input.boxXpx, input.boxYpx, input.boxWpx, input.boxHpx);
|
|
5981
|
+
const shapeFrame = frameOf(bounds.x / EMU_PER_PX, bounds.y / EMU_PER_PX, bounds.w / EMU_PER_PX, bounds.h / EMU_PER_PX);
|
|
5982
|
+
let inkLeft = Infinity;
|
|
5983
|
+
let inkRight = -Infinity;
|
|
5931
5984
|
let inkTop = Infinity;
|
|
5932
5985
|
let inkBottom = -Infinity;
|
|
5933
|
-
let
|
|
5934
|
-
let anyInk = false;
|
|
5986
|
+
let lineHeight = 0;
|
|
5935
5987
|
for (const p of core.placements) {
|
|
5936
5988
|
const toks = [...p.line.tokens];
|
|
5937
5989
|
while (toks.length > 0 && (toks[toks.length - 1].isSpace || toks[toks.length - 1].isBreak)) toks.pop();
|
|
5938
5990
|
if (!toks.some((t) => !t.isSpace && !t.isBreak)) continue;
|
|
5939
|
-
anyInk = true;
|
|
5940
5991
|
let lineW = 0;
|
|
5941
5992
|
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
|
-
|
|
5944
|
-
|
|
5993
|
+
const left = (p.line.textAnchor === "middle" ? p.line.anchorX - lineW / 2 : p.line.textAnchor === "end" ? p.line.anchorX - lineW : p.line.anchorX) + p.dx;
|
|
5994
|
+
inkLeft = Math.min(inkLeft, left);
|
|
5995
|
+
inkRight = Math.max(inkRight, left + lineW);
|
|
5945
5996
|
inkTop = Math.min(inkTop, p.baselineY - p.line.ascent);
|
|
5946
5997
|
inkBottom = Math.max(inkBottom, p.baselineY + p.line.descent);
|
|
5998
|
+
lineHeight = Math.max(lineHeight, p.line.ascent + p.line.descent);
|
|
5947
5999
|
}
|
|
5948
|
-
if (
|
|
6000
|
+
if (inkLeft === Infinity) return;
|
|
5949
6001
|
const approximate = detectApproximate(input.paragraphs, measure);
|
|
5950
6002
|
const slideRef = {
|
|
5951
6003
|
slideIndex,
|
|
5952
6004
|
shapeName: getShapeName(shape),
|
|
5953
6005
|
approximate
|
|
5954
6006
|
};
|
|
6007
|
+
const overflowXOf = (f) => Math.max(0, f.x - inkLeft, inkRight - (f.x + f.w));
|
|
6008
|
+
const overflowYOf = (f) => Math.max(0, f.y - inkTop, inkBottom - (f.y + f.h));
|
|
6009
|
+
const shapeOverflowXOf = (f) => Math.max(0, f.x - inkLeft) + Math.max(0, inkRight - (f.x + f.w));
|
|
6010
|
+
const shapeOverflowYOf = (f) => Math.max(0, f.y - inkTop) + Math.max(0, inkBottom - (f.y + f.h));
|
|
6011
|
+
const lineHeightPx = round2(lineHeight);
|
|
6012
|
+
const overflowX = overflowXOf(frame);
|
|
5955
6013
|
if (overflowX > tolerancePx) issues.push({
|
|
5956
6014
|
...slideRef,
|
|
5957
6015
|
kind: "overflow-x",
|
|
5958
|
-
overflowPx: round2(overflowX)
|
|
6016
|
+
overflowPx: round2(overflowX),
|
|
6017
|
+
shapeOverflowPx: round2(shapeOverflowXOf(shapeFrame)),
|
|
6018
|
+
lineHeightPx
|
|
5959
6019
|
});
|
|
5960
|
-
const overflowY =
|
|
6020
|
+
const overflowY = overflowYOf(frame);
|
|
5961
6021
|
if (overflowY > tolerancePx) issues.push({
|
|
5962
6022
|
...slideRef,
|
|
5963
6023
|
kind: "overflow-y",
|
|
5964
|
-
overflowPx: round2(overflowY)
|
|
6024
|
+
overflowPx: round2(overflowY),
|
|
6025
|
+
shapeOverflowPx: round2(shapeOverflowYOf(shapeFrame)),
|
|
6026
|
+
lineHeightPx
|
|
5965
6027
|
});
|
|
5966
6028
|
if (reportSoftWraps && vert === "none") {
|
|
5967
6029
|
const lineCounts = /* @__PURE__ */ new Map();
|
|
@@ -5977,6 +6039,94 @@ const auditShape = (pres, theme, shape, slideIndex, measure, tolerancePx, report
|
|
|
5977
6039
|
});
|
|
5978
6040
|
});
|
|
5979
6041
|
}
|
|
6042
|
+
if (reportBadWraps && vert === "none") auditLineBreaks(input.paragraphs, core.placements, slideRef, issues);
|
|
6043
|
+
if (minFontSizePt !== void 0) {
|
|
6044
|
+
let smallestPx = Infinity;
|
|
6045
|
+
for (const para of input.paragraphs) for (const piece of para.pieces) if (!piece.isBreak && piece.text.trim() !== "" && piece.sizePx < smallestPx) smallestPx = piece.sizePx;
|
|
6046
|
+
const sizePt = Math.round(smallestPx * PT_PER_PX * 10) / 10;
|
|
6047
|
+
if (sizePt < minFontSizePt) issues.push({
|
|
6048
|
+
...slideRef,
|
|
6049
|
+
kind: "small-font",
|
|
6050
|
+
sizePt
|
|
6051
|
+
});
|
|
6052
|
+
}
|
|
6053
|
+
};
|
|
6054
|
+
const auditLineBreaks = (paragraphs, placements, slideRef, issues) => {
|
|
6055
|
+
const linesByParagraph = /* @__PURE__ */ new Map();
|
|
6056
|
+
for (const { line } of placements) {
|
|
6057
|
+
const text = line.tokens.map((t) => t.isTab ? " " : t.text).join("");
|
|
6058
|
+
if (text.trim() === "") continue;
|
|
6059
|
+
const lines = linesByParagraph.get(line.paraIndex);
|
|
6060
|
+
if (lines) lines.push(text);
|
|
6061
|
+
else linesByParagraph.set(line.paraIndex, [text]);
|
|
6062
|
+
}
|
|
6063
|
+
for (const [paragraphIndex, lines] of linesByParagraph) auditParagraphBreaks(paragraphs[paragraphIndex].pieces.map((p) => p.isBreak ? "\n" : p.text).join(""), lines, {
|
|
6064
|
+
...slideRef,
|
|
6065
|
+
paragraphIndex
|
|
6066
|
+
}, issues);
|
|
6067
|
+
};
|
|
6068
|
+
const auditParagraphBreaks = (source, lines, ref, issues) => {
|
|
6069
|
+
const found = [];
|
|
6070
|
+
const wordsByStart = /* @__PURE__ */ new Map();
|
|
6071
|
+
let previousEnd = -1;
|
|
6072
|
+
let wrappedLines = 0;
|
|
6073
|
+
const closeSegment = (lastLine) => {
|
|
6074
|
+
const visible = [...lastLine.replace(/\s/gu, "")];
|
|
6075
|
+
if (wrappedLines > 0 && visible.length <= SHORT_LAST_LINE_MAX_CHARS) found.push({
|
|
6076
|
+
...ref,
|
|
6077
|
+
kind: "short-last-line",
|
|
6078
|
+
text: lastLine.trim()
|
|
6079
|
+
});
|
|
6080
|
+
};
|
|
6081
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6082
|
+
const start = source.indexOf(lines[i], Math.max(previousEnd, 0));
|
|
6083
|
+
if (start < 0) return;
|
|
6084
|
+
if (previousEnd >= 0) {
|
|
6085
|
+
const gap = source.slice(previousEnd, start);
|
|
6086
|
+
if (gap.includes("\n")) {
|
|
6087
|
+
closeSegment(lines[i - 1]);
|
|
6088
|
+
wrappedLines = 0;
|
|
6089
|
+
} else {
|
|
6090
|
+
wrappedLines++;
|
|
6091
|
+
if (gap === "" && splitsWord(charBefore(source, start), charAt(source, start))) {
|
|
6092
|
+
let wordStart = start;
|
|
6093
|
+
while (wordStart > 0 && isWordChar(charBefore(source, wordStart))) wordStart -= charBefore(source, wordStart).length;
|
|
6094
|
+
let word = wordsByStart.get(wordStart);
|
|
6095
|
+
if (!word) {
|
|
6096
|
+
let end = start;
|
|
6097
|
+
while (end < source.length && isWordChar(charAt(source, end))) end += charAt(source, end).length;
|
|
6098
|
+
word = {
|
|
6099
|
+
start: wordStart,
|
|
6100
|
+
end,
|
|
6101
|
+
breaks: []
|
|
6102
|
+
};
|
|
6103
|
+
wordsByStart.set(wordStart, word);
|
|
6104
|
+
found.push(word);
|
|
6105
|
+
}
|
|
6106
|
+
word.breaks.push(start);
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
previousEnd = start + lines[i].length;
|
|
6111
|
+
}
|
|
6112
|
+
closeSegment(lines[lines.length - 1]);
|
|
6113
|
+
for (const entry of found) {
|
|
6114
|
+
if ("kind" in entry) {
|
|
6115
|
+
issues.push(entry);
|
|
6116
|
+
continue;
|
|
6117
|
+
}
|
|
6118
|
+
let text = "";
|
|
6119
|
+
let from = entry.start;
|
|
6120
|
+
for (const at of entry.breaks) {
|
|
6121
|
+
text += `${source.slice(from, at)}|`;
|
|
6122
|
+
from = at;
|
|
6123
|
+
}
|
|
6124
|
+
issues.push({
|
|
6125
|
+
...ref,
|
|
6126
|
+
kind: "word-break",
|
|
6127
|
+
text: text + source.slice(from, entry.end)
|
|
6128
|
+
});
|
|
6129
|
+
}
|
|
5980
6130
|
};
|
|
5981
6131
|
const detectApproximate = (paragraphs, measure) => {
|
|
5982
6132
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -6001,19 +6151,25 @@ const detectApproximate = (paragraphs, measure) => {
|
|
|
6001
6151
|
/**
|
|
6002
6152
|
* Measures every text body in the deck and reports text that escapes its box
|
|
6003
6153
|
* (`overflow-x` / `overflow-y`) and, opt-in, paragraphs that soft-wrap
|
|
6004
|
-
* (`soft-wrap`, 段落ち)
|
|
6154
|
+
* (`soft-wrap`, 段落ち), wraps that split a word or strand a line of one or
|
|
6155
|
+
* two characters (`word-break`, `short-last-line`), and text below a minimum
|
|
6156
|
+
* size (`small-font`).
|
|
6005
6157
|
*
|
|
6006
6158
|
* Table cell text is not audited (v1 covers shape text bodies, including
|
|
6007
6159
|
* placeholders and shapes inside groups).
|
|
6008
6160
|
*/
|
|
6009
6161
|
const auditTextLayout = (pres, options = {}) => {
|
|
6010
6162
|
const measure = options.measureText ?? defaultMeasurer;
|
|
6011
|
-
const
|
|
6012
|
-
|
|
6163
|
+
const resolved = {
|
|
6164
|
+
tolerancePx: options.tolerancePx ?? DEFAULT_TOLERANCE_PX,
|
|
6165
|
+
reportSoftWraps: options.reportSoftWraps ?? false,
|
|
6166
|
+
reportBadWraps: options.reportBadWraps ?? false,
|
|
6167
|
+
minFontSizePt: options.minFontSizePt
|
|
6168
|
+
};
|
|
6013
6169
|
const theme = getPresentationTheme(pres);
|
|
6014
6170
|
const issues = [];
|
|
6015
6171
|
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,
|
|
6172
|
+
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
6173
|
return issues;
|
|
6018
6174
|
};
|
|
6019
6175
|
//#endregion
|
|
@@ -6060,4 +6216,4 @@ function shapeTextAnchorOffset(pres, shape, options = {}) {
|
|
|
6060
6216
|
//#endregion
|
|
6061
6217
|
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
6218
|
|
|
6063
|
-
//# sourceMappingURL=src-
|
|
6219
|
+
//# sourceMappingURL=src-ayE1F9lH.js.map
|