@liustack/pptfast 0.8.0 → 0.9.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.
@@ -2948,6 +2948,401 @@ function resolveNarrative(input) {
2948
2948
  return { strategy, pacing, audience };
2949
2949
  }
2950
2950
 
2951
+ // src/lib/svg-text-layout.ts
2952
+ var WIDE_CHAR_RE = /[\u2014\u2018-\u201f\u2e80-\u9fff\uff00-\uffef]/;
2953
+ var NO_CORRECTION = { regular: 1, bold: 1 };
2954
+ var GEORGIA = {
2955
+ space: { regular: 1, bold: 0.7254 },
2956
+ wide: NO_CORRECTION,
2957
+ upper: { regular: 1, bold: 1.1242 },
2958
+ lowerDigit: { regular: 1, bold: 1.0461 },
2959
+ other: { regular: 1, bold: 0.9159 }
2960
+ };
2961
+ var YAHEI = {
2962
+ space: { regular: 1, bold: 0.8511 },
2963
+ wide: NO_CORRECTION,
2964
+ upper: { regular: 1, bold: 1.0317 },
2965
+ lowerDigit: { regular: 1, bold: 1.0266 },
2966
+ other: { regular: 1, bold: 0.9593 }
2967
+ };
2968
+ var SIMSUN_KAITI = {
2969
+ space: { regular: 1.4286, bold: 1.4286 },
2970
+ wide: NO_CORRECTION,
2971
+ upper: { regular: 1, bold: 0.852 },
2972
+ lowerDigit: { regular: 1, bold: 1.048 },
2973
+ other: { regular: 1.2235, bold: 1.2235 }
2974
+ };
2975
+ var ENVELOPE = {
2976
+ space: { regular: 1, bold: Math.max(GEORGIA.space.bold, YAHEI.space.bold, SIMSUN_KAITI.space.bold) },
2977
+ wide: { regular: 1, bold: Math.max(GEORGIA.wide.bold, YAHEI.wide.bold, SIMSUN_KAITI.wide.bold) },
2978
+ upper: { regular: 1, bold: Math.max(GEORGIA.upper.bold, YAHEI.upper.bold, SIMSUN_KAITI.upper.bold) },
2979
+ lowerDigit: {
2980
+ regular: 1,
2981
+ bold: Math.max(GEORGIA.lowerDigit.bold, YAHEI.lowerDigit.bold, SIMSUN_KAITI.lowerDigit.bold)
2982
+ },
2983
+ other: { regular: 1, bold: Math.max(GEORGIA.other.bold, YAHEI.other.bold, SIMSUN_KAITI.other.bold) }
2984
+ };
2985
+ var GEORGIA_REGULAR_EXACT = { 32: 0.2412, 33: 0.3311, 34: 0.4116, 35: 0.6431, 36: 0.6099, 37: 0.8174, 38: 0.7104, 39: 0.2153, 40: 0.375, 41: 0.375, 42: 0.4722, 43: 0.6431, 44: 0.2695, 45: 0.374, 46: 0.2695, 47: 0.4688, 48: 0.6138, 49: 0.4297, 50: 0.5586, 51: 0.5518, 52: 0.5649, 53: 0.5283, 54: 0.5659, 55: 0.5024, 56: 0.5962, 57: 0.5659, 58: 0.3125, 59: 0.3125, 60: 0.6431, 61: 0.6431, 62: 0.6431, 63: 0.4785, 64: 0.9287, 65: 0.6709, 66: 0.6538, 67: 0.6421, 68: 0.749, 69: 0.6533, 70: 0.5991, 71: 0.7251, 72: 0.8149, 73: 0.3896, 74: 0.5176, 75: 0.6943, 76: 0.6035, 77: 0.9272, 78: 0.7671, 79: 0.7441, 80: 0.6099, 81: 0.7441, 82: 0.7017, 83: 0.561, 84: 0.6187, 85: 0.7563, 86: 0.6665, 87: 0.9756, 88: 0.7104, 89: 0.6152, 90: 0.6016, 91: 0.375, 92: 0.4688, 93: 0.375, 94: 0.6431, 95: 0.6431, 96: 0.5, 97: 0.5039, 98: 0.5601, 99: 0.4541, 100: 0.5742, 101: 0.4834, 102: 0.3252, 103: 0.5093, 104: 0.582, 105: 0.293, 106: 0.292, 107: 0.5356, 108: 0.2861, 109: 0.8809, 110: 0.5908, 111: 0.5391, 112: 0.5713, 113: 0.5596, 114: 0.4097, 115: 0.4321, 116: 0.3452, 117: 0.5752, 118: 0.4966, 119: 0.7373, 120: 0.5049, 121: 0.4922, 122: 0.4438, 123: 0.4302, 124: 0.375, 125: 0.4302, 126: 0.6431 };
2986
+ var GEORGIA_BOLD_EXACT = { 32: 0.2539, 33: 0.3765, 34: 0.5098, 35: 0.7031, 36: 0.6406, 37: 0.8794, 38: 0.7993, 39: 0.269, 40: 0.4468, 41: 0.4468, 42: 0.4819, 43: 0.7031, 44: 0.3281, 45: 0.3789, 46: 0.3281, 47: 0.4717, 48: 0.7012, 49: 0.4897, 50: 0.6265, 51: 0.6245, 52: 0.6494, 53: 0.5991, 54: 0.6479, 55: 0.5542, 56: 0.6763, 57: 0.6479, 58: 0.3672, 59: 0.3672, 60: 0.7031, 61: 0.7031, 62: 0.7031, 63: 0.5483, 64: 0.9668, 65: 0.7583, 66: 0.7573, 67: 0.7153, 68: 0.834, 69: 0.7212, 70: 0.6714, 71: 0.8071, 72: 0.9131, 73: 0.4458, 74: 0.5952, 75: 0.8169, 76: 0.6855, 77: 1.0234, 78: 0.8394, 79: 0.8198, 80: 0.7012, 81: 0.8198, 82: 0.7974, 83: 0.6489, 84: 0.6841, 85: 0.8335, 86: 0.7622, 87: 1.1265, 88: 0.8086, 89: 0.7319, 90: 0.6895, 91: 0.4468, 92: 0.4717, 93: 0.4468, 94: 0.7031, 95: 0.7031, 96: 0.5, 97: 0.5957, 98: 0.6455, 99: 0.5312, 100: 0.6631, 101: 0.5718, 102: 0.3931, 103: 0.5767, 104: 0.6797, 105: 0.3535, 106: 0.3462, 107: 0.6318, 108: 0.3442, 109: 1.0156, 110: 0.6899, 111: 0.6357, 112: 0.6577, 113: 0.6484, 114: 0.52, 115: 0.5127, 116: 0.3975, 117: 0.6768, 118: 0.5669, 119: 0.8633, 120: 0.5879, 121: 0.562, 122: 0.5254, 123: 0.5, 124: 0.3877, 125: 0.5, 126: 0.7031 };
2987
+ var YAHEI_REGULAR_EXACT = { 32: 0.2959, 33: 0.3125, 34: 0.4355, 35: 0.6382, 36: 0.5864, 37: 0.8896, 38: 0.8701, 39: 0.2563, 40: 0.334, 41: 0.334, 42: 0.4551, 43: 0.7417, 44: 0.2407, 45: 0.4326, 46: 0.2407, 47: 0.4272, 48: 0.5864, 49: 0.5864, 50: 0.5864, 51: 0.5864, 52: 0.5864, 53: 0.5864, 54: 0.5864, 55: 0.5864, 56: 0.5864, 57: 0.5864, 58: 0.2407, 59: 0.2407, 60: 0.7417, 61: 0.7417, 62: 0.7417, 63: 0.4829, 64: 1.0312, 65: 0.7036, 66: 0.6274, 67: 0.6689, 68: 0.7617, 69: 0.5498, 70: 0.5312, 71: 0.7437, 72: 0.7734, 73: 0.2939, 74: 0.396, 75: 0.6348, 76: 0.5132, 77: 0.9771, 78: 0.813, 79: 0.8149, 80: 0.6118, 81: 0.8149, 82: 0.6528, 83: 0.5771, 84: 0.5732, 85: 0.7466, 86: 0.6763, 87: 1.0176, 88: 0.645, 89: 0.6035, 90: 0.6201, 91: 0.334, 92: 0.416, 93: 0.334, 94: 0.7417, 95: 0.4482, 96: 0.2949, 97: 0.5527, 98: 0.6387, 99: 0.5015, 100: 0.6396, 101: 0.5674, 102: 0.3467, 103: 0.6396, 104: 0.6157, 105: 0.2661, 106: 0.2671, 107: 0.5444, 108: 0.2661, 109: 0.937, 110: 0.6162, 111: 0.6357, 112: 0.6387, 113: 0.6396, 114: 0.3818, 115: 0.4629, 116: 0.3726, 117: 0.6162, 118: 0.5249, 119: 0.7896, 120: 0.5068, 121: 0.5293, 122: 0.4917, 123: 0.334, 124: 0.269, 125: 0.334, 126: 0.7417 };
2988
+ var YAHEI_BOLD_EXACT = { 32: 0.2979, 33: 0.3486, 34: 0.521, 35: 0.6401, 36: 0.6167, 37: 0.9312, 38: 0.9111, 39: 0.3081, 40: 0.3896, 41: 0.3896, 42: 0.4873, 43: 0.7612, 44: 0.2856, 45: 0.4365, 46: 0.2856, 47: 0.4727, 48: 0.6167, 49: 0.6167, 50: 0.6167, 51: 0.6167, 52: 0.6167, 53: 0.6167, 54: 0.6167, 55: 0.6167, 56: 0.6167, 57: 0.6167, 58: 0.2856, 59: 0.2856, 60: 0.7612, 61: 0.7612, 62: 0.7612, 63: 0.4741, 64: 1.0298, 65: 0.752, 66: 0.6836, 67: 0.6733, 68: 0.7915, 69: 0.5718, 70: 0.5581, 71: 0.7651, 72: 0.8213, 73: 0.3354, 74: 0.4702, 75: 0.6929, 76: 0.5469, 77: 1.0283, 78: 0.8481, 79: 0.8184, 80: 0.6572, 81: 0.8184, 82: 0.6982, 83: 0.6016, 84: 0.6255, 85: 0.7764, 86: 0.7148, 87: 1.0762, 88: 0.7002, 89: 0.6484, 90: 0.6504, 91: 0.3896, 92: 0.4644, 93: 0.3896, 94: 0.7612, 95: 0.4482, 96: 0.3335, 97: 0.5776, 98: 0.666, 99: 0.5166, 100: 0.6646, 101: 0.582, 102: 0.4053, 103: 0.6646, 104: 0.6455, 105: 0.2959, 106: 0.3018, 107: 0.5962, 108: 0.2959, 109: 0.9819, 110: 0.6479, 111: 0.6572, 112: 0.666, 113: 0.6646, 114: 0.4238, 115: 0.4937, 116: 0.4141, 117: 0.6479, 118: 0.5771, 119: 0.8516, 120: 0.585, 121: 0.5742, 122: 0.5137, 123: 0.3896, 124: 0.3413, 125: 0.3896, 126: 0.7612 };
2989
+ var CLASS_TABLE_FOR = {
2990
+ georgia: GEORGIA,
2991
+ yahei: YAHEI,
2992
+ "simsun-kaiti": SIMSUN_KAITI,
2993
+ unknown: ENVELOPE
2994
+ };
2995
+ var EXACT_TABLE_FOR = {
2996
+ georgia: { regular: GEORGIA_REGULAR_EXACT, bold: GEORGIA_BOLD_EXACT },
2997
+ yahei: { regular: YAHEI_REGULAR_EXACT, bold: YAHEI_BOLD_EXACT }
2998
+ };
2999
+ function classifyFaceKey(fontFamily) {
3000
+ const first = fontFamily?.split(",")[0]?.replace(/['"]/g, "").trim().toLowerCase();
3001
+ if (!first) return "unknown";
3002
+ if (first === "georgia") return "georgia";
3003
+ if (first === "microsoft yahei" || first === "\u5FAE\u8F6F\u96C5\u9ED1") return "yahei";
3004
+ if (first === "simsun" || first === "\u5B8B\u4F53" || first === "kaiti" || first === "\u6977\u4F53") return "simsun-kaiti";
3005
+ return "unknown";
3006
+ }
3007
+ function classAverageUnits(char, table, mode) {
3008
+ if (/\s/.test(char)) return 0.35 * table.space[mode];
3009
+ if (WIDE_CHAR_RE.test(char)) return 1 * table.wide[mode];
3010
+ if (/[A-Z]/.test(char)) return 0.66 * table.upper[mode];
3011
+ if (/[a-z0-9]/.test(char)) return 0.56 * table.lowerDigit[mode];
3012
+ return 0.46 * table.other[mode];
3013
+ }
3014
+ function measureTextUnits(text, weight) {
3015
+ const mode = weight?.bold ? "bold" : "regular";
3016
+ const faceKey = classifyFaceKey(weight?.fontFamily);
3017
+ const classTable = CLASS_TABLE_FOR[faceKey];
3018
+ const exactTable = mode === "bold" ? EXACT_TABLE_FOR[faceKey]?.bold : void 0;
3019
+ return Array.from(text).reduce((sum, char) => {
3020
+ if (exactTable && !WIDE_CHAR_RE.test(char)) {
3021
+ const exact = exactTable[char.charCodeAt(0)];
3022
+ if (exact !== void 0) return sum + exact;
3023
+ }
3024
+ return sum + classAverageUnits(char, classTable, mode);
3025
+ }, 0);
3026
+ }
3027
+ var MONO_ADVANCE_EM = 1126 / 2048;
3028
+ function measureMonoTextUnits(text) {
3029
+ return Array.from(text).reduce((sum, char) => {
3030
+ if (WIDE_CHAR_RE.test(char)) return sum + 1;
3031
+ return sum + MONO_ADVANCE_EM;
3032
+ }, 0);
3033
+ }
3034
+ function splitLongToken(token, maxUnits, weight) {
3035
+ const chunks = [];
3036
+ let current = "";
3037
+ for (const char of Array.from(token)) {
3038
+ const candidate = `${current}${char}`;
3039
+ if (current && measureTextUnits(candidate, weight) > maxUnits) {
3040
+ chunks.push(current);
3041
+ current = char;
3042
+ } else {
3043
+ current = candidate;
3044
+ }
3045
+ }
3046
+ if (current) chunks.push(current);
3047
+ return chunks;
3048
+ }
3049
+ var LATIN_RUN_OR_CHAR_RE = /[A-Za-z0-9](?:[A-Za-z0-9.\-%]*[A-Za-z0-9%])?|./gu;
3050
+ function tokenize(text) {
3051
+ const normalized = text.trim().replace(/\s+/g, " ");
3052
+ if (!normalized) return { tokens: [], spaceDelimited: false };
3053
+ const spaceDelimited = normalized.includes(" ");
3054
+ return {
3055
+ tokens: spaceDelimited ? normalized.split(" ") : normalized.match(LATIN_RUN_OR_CHAR_RE) ?? [],
3056
+ spaceDelimited
3057
+ };
3058
+ }
3059
+ function wrapWithUnits(text, maxUnits, weight) {
3060
+ const lines = [];
3061
+ let hadSplit = false;
3062
+ let minSplitFreeUnits = 0;
3063
+ for (const paragraph2 of text.split(/\n+/)) {
3064
+ const { tokens, spaceDelimited } = tokenize(paragraph2);
3065
+ let current = "";
3066
+ for (const token of tokens) {
3067
+ const tokenUnits = measureTextUnits(token, weight);
3068
+ const isRunToken = !spaceDelimited && Array.from(token).length > 1;
3069
+ if (isRunToken) minSplitFreeUnits = Math.max(minSplitFreeUnits, tokenUnits);
3070
+ const tokenChunks = tokenUnits > maxUnits ? splitLongToken(token, maxUnits, weight) : [token];
3071
+ if (isRunToken && tokenChunks.length > 1) hadSplit = true;
3072
+ for (const [chunkIndex, chunk] of tokenChunks.entries()) {
3073
+ const prefix = current && spaceDelimited && chunkIndex === 0 ? " " : "";
3074
+ const candidate = `${current}${prefix}${chunk}`;
3075
+ if (current && measureTextUnits(candidate, weight) > maxUnits) {
3076
+ lines.push(current);
3077
+ current = chunk;
3078
+ } else {
3079
+ current = candidate;
3080
+ }
3081
+ }
3082
+ }
3083
+ if (current) lines.push(current);
3084
+ }
3085
+ return { lines, hadSplit, minSplitFreeUnits };
3086
+ }
3087
+ function balanceWrappedLines(content, lines, weight) {
3088
+ if (lines.length < 2 || content.includes("\n")) return lines;
3089
+ const units = lines.map((l) => measureTextUnits(l, weight));
3090
+ const widest = Math.max(...units);
3091
+ if (units[units.length - 1] >= widest * 0.5) return lines;
3092
+ const total = units.reduce((sum, u) => sum + u, 0);
3093
+ const { tokens } = tokenize(content);
3094
+ const longestToken = Math.max(...tokens.map((t) => measureTextUnits(t, weight)), 0);
3095
+ let target = Math.max(total / lines.length, longestToken);
3096
+ for (let i = 0; i < 8; i += 1) {
3097
+ const candidate = wrapWithUnits(content, target, weight);
3098
+ if (candidate.lines.length === lines.length) return candidate.lines;
3099
+ if (candidate.lines.length < lines.length) return lines;
3100
+ target *= 1.06;
3101
+ }
3102
+ return lines;
3103
+ }
3104
+ function truncateToUnits(text, maxUnits, weight) {
3105
+ if (measureTextUnits(text, weight) <= maxUnits) return text;
3106
+ const budget = maxUnits - 1;
3107
+ let out = "";
3108
+ for (const ch of Array.from(text)) {
3109
+ if (measureTextUnits(out + ch, weight) > budget) break;
3110
+ out += ch;
3111
+ }
3112
+ if (out === "") {
3113
+ return measureTextUnits("\u2026", weight) > maxUnits ? "" : "\u2026";
3114
+ }
3115
+ return `${out}\u2026`;
3116
+ }
3117
+ function truncateToMonoUnits(text, maxUnits) {
3118
+ if (measureMonoTextUnits(text) <= maxUnits) return text;
3119
+ const budget = maxUnits - 1;
3120
+ let out = "";
3121
+ for (const ch of Array.from(text)) {
3122
+ if (measureMonoTextUnits(out + ch) > budget) break;
3123
+ out += ch;
3124
+ }
3125
+ if (out === "") {
3126
+ return measureMonoTextUnits("\u2026") > maxUnits ? "" : "\u2026";
3127
+ }
3128
+ return `${out}\u2026`;
3129
+ }
3130
+ function fitSvgLine(text, opts) {
3131
+ const minFontSize = opts.minFontSize ?? 12;
3132
+ const letterSpacing = opts.letterSpacing ?? 0;
3133
+ const weight = { bold: opts.bold, fontFamily: opts.fontFamily };
3134
+ const units = measureTextUnits(text, weight);
3135
+ if (units <= 0) return { text, fontSize: opts.fontSize, truncated: false };
3136
+ const charCount = Array.from(text).length;
3137
+ const spacingBudget = Math.max(0, charCount - 1) * letterSpacing;
3138
+ const availableWidth = Math.max(0, opts.maxWidth - spacingBudget);
3139
+ const fitted = Math.min(opts.fontSize, Math.floor(availableWidth / units));
3140
+ if (fitted >= minFontSize) return { text, fontSize: fitted, truncated: false };
3141
+ return {
3142
+ text: truncateToUnits(text, availableWidth / minFontSize, weight),
3143
+ fontSize: minFontSize,
3144
+ truncated: true
3145
+ };
3146
+ }
3147
+ function layoutSvgText(text, options) {
3148
+ const content = text?.trim() ?? "";
3149
+ const maxLines = options.maxLines ?? 2;
3150
+ const lineHeightRatio = options.lineHeightRatio ?? 1.08;
3151
+ const weight = { bold: options.bold, fontFamily: options.fontFamily };
3152
+ const minPt = options.minPt;
3153
+ if (!content) {
3154
+ return { lines: [], fontSize: options.fontSize, lineHeight: 0, truncated: false };
3155
+ }
3156
+ const fontSizeFor = (ls) => {
3157
+ const longest = Math.max(...ls.map((l) => measureTextUnits(l, weight)), 1);
3158
+ return Math.max(1, Math.min(options.fontSize, Math.floor(options.maxWidth / longest)));
3159
+ };
3160
+ const baseUnits = options.maxWidth / options.fontSize;
3161
+ let maxUnits = baseUnits;
3162
+ let attempt = wrapWithUnits(content, maxUnits, weight);
3163
+ const minSplitFreeUnits = attempt.minSplitFreeUnits;
3164
+ let legacyLines = attempt.lines;
3165
+ for (let i = 0; legacyLines.length > maxLines && i < 8; i += 1) {
3166
+ maxUnits *= 1.14;
3167
+ attempt = wrapWithUnits(content, maxUnits, weight);
3168
+ legacyLines = attempt.lines;
3169
+ }
3170
+ const legacyHadSplit = attempt.hadSplit;
3171
+ if (legacyLines.length > maxLines) {
3172
+ legacyLines = [
3173
+ ...legacyLines.slice(0, maxLines - 1),
3174
+ legacyLines.slice(maxLines - 1).join("")
3175
+ ];
3176
+ }
3177
+ let lines = legacyLines;
3178
+ if (legacyHadSplit) {
3179
+ const reachCeilingUnits = minPt === void 0 ? baseUnits * 1.14 ** 8 : Infinity;
3180
+ if (minSplitFreeUnits <= reachCeilingUnits) {
3181
+ let splitFreeUnits = minSplitFreeUnits;
3182
+ let splitFreeAttempt = wrapWithUnits(content, splitFreeUnits, weight);
3183
+ let admissible = splitFreeAttempt.lines.length <= maxLines;
3184
+ if (admissible && minPt !== void 0) {
3185
+ admissible = fontSizeFor(splitFreeAttempt.lines) >= minPt;
3186
+ }
3187
+ const stepCap = minPt === void 0 ? 8 : 32;
3188
+ for (let i = 0; !admissible && splitFreeAttempt.lines.length > maxLines && splitFreeUnits < reachCeilingUnits && i < stepCap; i += 1) {
3189
+ splitFreeUnits = Math.min(splitFreeUnits * 1.14, reachCeilingUnits);
3190
+ splitFreeAttempt = wrapWithUnits(content, splitFreeUnits, weight);
3191
+ admissible = splitFreeAttempt.lines.length <= maxLines;
3192
+ if (admissible && minPt !== void 0) {
3193
+ admissible = fontSizeFor(splitFreeAttempt.lines) >= minPt;
3194
+ }
3195
+ }
3196
+ if (admissible) lines = splitFreeAttempt.lines;
3197
+ }
3198
+ }
3199
+ if (options.balanceLines) {
3200
+ lines = balanceWrappedLines(content, lines, weight);
3201
+ }
3202
+ const fontSize = fontSizeFor(lines);
3203
+ return {
3204
+ lines,
3205
+ fontSize,
3206
+ lineHeight: Math.round(fontSize * lineHeightRatio),
3207
+ truncated: false
3208
+ };
3209
+ }
3210
+ function hasExactWidthTable(fontFamily) {
3211
+ return EXACT_TABLE_FOR[classifyFaceKey(fontFamily)] !== void 0;
3212
+ }
3213
+
3214
+ // src/svg/fonts.ts
3215
+ var SAFE_FONTS = new Set(
3216
+ [
3217
+ // Latin sans
3218
+ "Arial",
3219
+ "Calibri",
3220
+ "Tahoma",
3221
+ "Verdana",
3222
+ "Segoe UI",
3223
+ // Latin serif
3224
+ "Georgia",
3225
+ "Times New Roman",
3226
+ "Cambria",
3227
+ // Latin mono
3228
+ "Consolas",
3229
+ "Courier New",
3230
+ "Lucida Console",
3231
+ // CJK (Windows)
3232
+ "Microsoft YaHei",
3233
+ "\u5FAE\u8F6F\u96C5\u9ED1",
3234
+ "SimSun",
3235
+ "\u5B8B\u4F53",
3236
+ "SimHei",
3237
+ "\u9ED1\u4F53",
3238
+ "KaiTi",
3239
+ "\u6977\u4F53",
3240
+ "FangSong",
3241
+ "\u4EFF\u5B8B"
3242
+ ].map((f) => f.toLowerCase())
3243
+ );
3244
+ var ROLE_DEFAULT = {
3245
+ heading: "Microsoft YaHei",
3246
+ body: "Microsoft YaHei",
3247
+ mono: "Consolas"
3248
+ };
3249
+ function resolveFontFace(stack, role) {
3250
+ for (const raw of stack) {
3251
+ const name = raw.replace(/['"]/g, "").trim();
3252
+ if (SAFE_FONTS.has(name.toLowerCase())) return name;
3253
+ }
3254
+ return ROLE_DEFAULT[role];
3255
+ }
3256
+ var CJK_SAFE_FACES = new Set(
3257
+ ["Microsoft YaHei", "\u5FAE\u8F6F\u96C5\u9ED1", "SimSun", "\u5B8B\u4F53", "SimHei", "\u9ED1\u4F53", "KaiTi", "\u6977\u4F53", "FangSong", "\u4EFF\u5B8B"].map(
3258
+ (f) => f.toLowerCase()
3259
+ )
3260
+ );
3261
+ var EA_FALLBACK_FACE = ROLE_DEFAULT.heading;
3262
+ function eaFontFaceFor(latinFace) {
3263
+ const name = latinFace.replace(/['"]/g, "").trim();
3264
+ return CJK_SAFE_FACES.has(name.toLowerCase()) ? name : EA_FALLBACK_FACE;
3265
+ }
3266
+ var SERIF_SAFE_FACES = new Set(
3267
+ ["Georgia", "Times New Roman", "Cambria", "SimSun", "\u5B8B\u4F53", "FangSong", "\u4EFF\u5B8B", "KaiTi", "\u6977\u4F53"].map(
3268
+ (f) => f.toLowerCase()
3269
+ )
3270
+ );
3271
+ var PREVIEW_FALLBACK = {
3272
+ serif: "Songti SC, STSong, serif",
3273
+ sans: "PingFang SC, Helvetica Neue, sans-serif",
3274
+ mono: "Menlo, monospace"
3275
+ };
3276
+ function resolveFontStack(stack, role) {
3277
+ const face = resolveFontFace(stack, role);
3278
+ const fallback = role === "mono" ? PREVIEW_FALLBACK.mono : SERIF_SAFE_FACES.has(face.toLowerCase()) ? PREVIEW_FALLBACK.serif : PREVIEW_FALLBACK.sans;
3279
+ return `${face}, ${fallback}`;
3280
+ }
3281
+ function isMonoFontFamily(fontFamily) {
3282
+ return fontFamily.endsWith(`, ${PREVIEW_FALLBACK.mono}`);
3283
+ }
3284
+ function isBold(weight) {
3285
+ if (!weight) return false;
3286
+ if (weight === "bold" || weight === "bolder") return true;
3287
+ return parseInt(weight, 10) >= 600;
3288
+ }
3289
+
3290
+ // src/svg/ink.ts
3291
+ var LUMINANCE_INK_THRESHOLD = 0.4;
3292
+ var LARGE_TEXT_MIN_PX = 24;
3293
+ var CONTRAST_RATIO_LARGE = 3;
3294
+ var CONTRAST_RATIO_BODY = 4.5;
3295
+ function relativeLuminance(hex) {
3296
+ let h = hex.trim().replace(/^#/, "");
3297
+ if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join("");
3298
+ if (h.length === 8) h = h.slice(0, 6);
3299
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) return 0;
3300
+ const n = parseInt(h, 16);
3301
+ const chan = [n >> 16 & 255, n >> 8 & 255, n & 255].map((v) => {
3302
+ const c = v / 255;
3303
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
3304
+ });
3305
+ return 0.2126 * chan[0] + 0.7152 * chan[1] + 0.0722 * chan[2];
3306
+ }
3307
+ function contrastRatio(hexA, hexB) {
3308
+ const la = relativeLuminance(hexA);
3309
+ const lb = relativeLuminance(hexB);
3310
+ const lighter = Math.max(la, lb);
3311
+ const darker = Math.min(la, lb);
3312
+ return (lighter + 0.05) / (darker + 0.05);
3313
+ }
3314
+ function requiredContrastRatio(fontSizePx) {
3315
+ return fontSizePx >= LARGE_TEXT_MIN_PX ? CONTRAST_RATIO_LARGE : CONTRAST_RATIO_BODY;
3316
+ }
3317
+ var DARK_INK = "#0A0E14";
3318
+ var LIGHT_INK = "#FFFFFF";
3319
+ function readableOn(bgHex) {
3320
+ const darkContrast = contrastRatio(DARK_INK, bgHex);
3321
+ const lightContrast = contrastRatio(LIGHT_INK, bgHex);
3322
+ if (darkContrast === lightContrast) {
3323
+ return relativeLuminance(bgHex) > LUMINANCE_INK_THRESHOLD ? DARK_INK : LIGHT_INK;
3324
+ }
3325
+ return darkContrast > lightContrast ? DARK_INK : LIGHT_INK;
3326
+ }
3327
+ function accessibleInk(preferredFill, bgHex, fontSizePx) {
3328
+ return contrastRatio(preferredFill, bgHex) >= requiredContrastRatio(fontSizePx) ? preferredFill : readableOn(bgHex);
3329
+ }
3330
+ function blendOver(fg, bg, alpha2) {
3331
+ const toRgb = (hex) => {
3332
+ const n = parseInt(hex.replace("#", ""), 16);
3333
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
3334
+ };
3335
+ const [fr, fgc, fb] = toRgb(fg);
3336
+ const [br, bgc, bb] = toRgb(bg);
3337
+ const mix = (f, b) => Math.round(f * alpha2 + b * (1 - alpha2));
3338
+ const toHex2 = (v) => v.toString(16).padStart(2, "0");
3339
+ return `#${toHex2(mix(fr, br))}${toHex2(mix(fgc, bgc))}${toHex2(mix(fb, bb))}`;
3340
+ }
3341
+ function accessibleOpacity(inkHex, bgHex, fontSizePx, preferredOpacity) {
3342
+ const blended = blendOver(inkHex, bgHex, preferredOpacity);
3343
+ return contrastRatio(blended, bgHex) >= requiredContrastRatio(fontSizePx) ? preferredOpacity : 1;
3344
+ }
3345
+
2951
3346
  // src/svg/layouts/registry.ts
2952
3347
  function filterByNarrativesOnly(defs, strategy) {
2953
3348
  return defs.filter((def) => def.narrativesOnly === void 0 || def.narrativesOnly.includes(strategy));
@@ -4204,6 +4599,34 @@ function resolveBrand(id, override) {
4204
4599
  return override ? { ...base2, ...override } : base2;
4205
4600
  }
4206
4601
  var REGISTERABLE_SLIDE_TYPES = ["cover", "chapter", "content", "ending"];
4602
+ function resolveBackgroundHex(spec, surfaceFallback) {
4603
+ if (spec.kind === "color") return spec.value;
4604
+ if (spec.kind === "gradient") return spec.from;
4605
+ return surfaceFallback;
4606
+ }
4607
+ var CONTRAST_FLOOR = 3;
4608
+ var CONTRAST_CHECKED_SLIDE_TYPES = ["cover", "content", "ending"];
4609
+ function assertContrastFloor(id, style) {
4610
+ for (const slideType of CONTRAST_CHECKED_SLIDE_TYPES) {
4611
+ const bg = resolveBackgroundHex(style.defaultBackgrounds[slideType], style.colors.surface);
4612
+ for (const token of ["text", "muted"]) {
4613
+ const ratio = contrastRatio(style.colors[token], bg);
4614
+ if (ratio < CONTRAST_FLOOR) {
4615
+ throw new PptfastError(
4616
+ `theme "${id}" colors.${token} has a contrast ratio of ${ratio.toFixed(2)}:1 against its "${slideType}" background (${bg}) \u2014 must be at least ${CONTRAST_FLOOR.toFixed(1)}:1`
4617
+ );
4618
+ }
4619
+ }
4620
+ }
4621
+ }
4622
+ function warnUnmeasuredFace(id, role, stack) {
4623
+ const face = resolveFontFace(stack, role);
4624
+ if (!hasExactWidthTable(face)) {
4625
+ console.warn(
4626
+ `theme "${id}" ${role} font "${face}" has no exact width table \u2014 text width estimation falls back to a conservative class-average envelope and may overflow on long text; see measureTextUnits in src/lib/svg-text-layout.ts`
4627
+ );
4628
+ }
4629
+ }
4207
4630
  function registerTheme(def) {
4208
4631
  if (CANONICAL_THEME_IDS.includes(def.id) || REGISTERED_THEMES.has(def.id)) {
4209
4632
  throw new PptfastError(`theme "${def.id}" is already installed`);
@@ -4211,6 +4634,7 @@ function registerTheme(def) {
4211
4634
  if (!def.style) {
4212
4635
  throw new PptfastError(`theme "${def.id}" is missing style tokens`);
4213
4636
  }
4637
+ assertContrastFloor(def.id, def.style);
4214
4638
  const layouts = {};
4215
4639
  for (const slideType of REGISTERABLE_SLIDE_TYPES) {
4216
4640
  const ids = def.layouts?.[slideType] ?? FULL_LAYOUTS[slideType];
@@ -4235,6 +4659,8 @@ function registerTheme(def) {
4235
4659
  }
4236
4660
  layouts[slideType] = ids;
4237
4661
  }
4662
+ warnUnmeasuredFace(def.id, "heading", def.style.fonts.heading);
4663
+ warnUnmeasuredFace(def.id, "body", def.style.fonts.body);
4238
4664
  REGISTERED_THEMES.set(def.id, { ...def, layouts });
4239
4665
  }
4240
4666
  function getInstalledThemeIds() {
@@ -4367,7 +4793,7 @@ var COMPONENT_FIELD_ALIASES = {
4367
4793
  }
4368
4794
  };
4369
4795
  var COMPONENT_ITEM_FIELD_ALIASES = {
4370
- kpi_cards: { itemsKey: "items", aliases: { title: "label", name: "label" } },
4796
+ kpi_cards: [{ itemsKey: "items", aliases: { title: "label", name: "label" } }],
4371
4797
  // Numeric-axis family (structure-components wave task 2, decision 8):
4372
4798
  // waterfall's per-item signed delta is commonly reached for as "amount" in
4373
4799
  // finance-deck vocabulary (a waterfall/bridge chart is itself a finance-
@@ -4375,30 +4801,38 @@ var COMPONENT_ITEM_FIELD_ALIASES = {
4375
4801
  // model that knows "Gantt chart" but not this schema's numeric-axis-only
4376
4802
  // shape (decision 6: no date parsing) reaches for by analogy to a
4377
4803
  // calendar's own "from"/"to" range vocabulary.
4378
- waterfall: { itemsKey: "items", aliases: { amount: "value" } },
4379
- gantt: { itemsKey: "items", aliases: { from: "start", to: "end" } },
4380
- // Sankey (structure-components wave 2 task 4): `source`/`target` is the
4381
- // exact field-name convention D3-sankey and Plotly's own Sankey trace
4382
- // both use for a link's two endpoints a model that has ever produced a
4383
- // sankey spec for either of those two (the dominant JS/Python charting
4384
- // libraries for this diagram type) reaches for that vocabulary over this
4385
- // schema's `from`/`to`. `nodes[].label` carries no alias here: D3's own
4386
- // classic convention there is `name`, but rescuing it would need a
4387
- // *second* item-array entry for the same component type (`nodes`
4388
- // alongside `links`), which `ItemFieldAliasSpec`'s one-`itemsKey`-per-
4389
- // component-type shape does not support — a real, scoped-out gap, not a
4390
- // silent drop, left for a future mechanism change rather than a data
4391
- // addition.
4392
- sankey: { itemsKey: "links", aliases: { source: "from", target: "to" } },
4804
+ waterfall: [{ itemsKey: "items", aliases: { amount: "value" } }],
4805
+ gantt: [{ itemsKey: "items", aliases: { from: "start", to: "end" } }],
4806
+ // Sankey (structure-components wave 2 task 4, `links`; field-alias sweep
4807
+ // task I1, `nodes`): `links`' `source`/`target` is the exact field-name
4808
+ // convention D3-sankey and Plotly's own Sankey trace both use for a
4809
+ // link's two endpoints a model that has ever produced a sankey spec for
4810
+ // either of those two (the dominant JS/Python charting libraries for this
4811
+ // diagram type) reaches for that vocabulary over this schema's
4812
+ // `from`/`to`. `nodes[].label` carried no alias until task I1 widened this
4813
+ // table's value type to a list of specs — rescuing it needed a *second*
4814
+ // item-array entry for the same component type (`nodes` alongside
4815
+ // `links`), which the original one-`itemsKey`-per-component-type shape
4816
+ // did not support (a real, scoped-out gap, not a silent drop). Now that a
4817
+ // component type can list one spec per item array, `nodes` gets its own:
4818
+ // `name` is D3's own classic node-label convention (the same
4819
+ // "reaches for the charting library's vocabulary" logic as `links`'
4820
+ // `source`/`target` above), `title` mirrors kpi_cards' own
4821
+ // `title`→`label` alias above — the same generic title-for-label slip on
4822
+ // any labeled-card-like item shape.
4823
+ sankey: [
4824
+ { itemsKey: "links", aliases: { source: "from", target: "to" } },
4825
+ { itemsKey: "nodes", aliases: { name: "label", title: "label" } }
4826
+ ],
4393
4827
  // Real-world tech-deck mental model: layers have a "name" and hold
4394
4828
  // "components" or "nodes" — pptfast's own top-level components array
4395
4829
  // shares the word "components" by coincidence only; this alias is scoped
4396
4830
  // to one architecture layer's own item shape, never the deck-level array.
4397
- architecture: { itemsKey: "layers", aliases: { name: "title", components: "items", nodes: "items" } },
4398
- steps: { itemsKey: "items", aliases: { description: "text", desc: "text" } },
4399
- timeline: { itemsKey: "milestones", aliases: { year: "date", text: "desc", description: "desc" } },
4400
- numbered_cards: { itemsKey: "items", aliases: { description: "text", desc: "text" } },
4401
- row_cards: { itemsKey: "items", aliases: { description: "text", desc: "text" } }
4831
+ architecture: [{ itemsKey: "layers", aliases: { name: "title", components: "items", nodes: "items" } }],
4832
+ steps: [{ itemsKey: "items", aliases: { description: "text", desc: "text" } }],
4833
+ timeline: [{ itemsKey: "milestones", aliases: { year: "date", text: "desc", description: "desc" } }],
4834
+ numbered_cards: [{ itemsKey: "items", aliases: { description: "text", desc: "text" } }],
4835
+ row_cards: [{ itemsKey: "items", aliases: { description: "text", desc: "text" } }]
4402
4836
  };
4403
4837
  var SLIDE_FIELD_ALIASES = {
4404
4838
  note: "notes",
@@ -4425,18 +4859,20 @@ function normalizeComponent(component, si, ci, normalized) {
4425
4859
  let next = component;
4426
4860
  const blockAliases = COMPONENT_FIELD_ALIASES[component.type];
4427
4861
  if (blockAliases) next = renameAliases(next, blockAliases, path, normalized);
4428
- const itemSpec = COMPONENT_ITEM_FIELD_ALIASES[component.type];
4429
- if (itemSpec) {
4430
- const items = next[itemSpec.itemsKey];
4431
- if (Array.isArray(items)) {
4432
- let itemsChanged = false;
4433
- const nextItems = items.map((item, ii) => {
4434
- if (!isPlainObject(item)) return item;
4435
- const renamed = renameAliases(item, itemSpec.aliases, `${path}.${itemSpec.itemsKey}[${ii}]`, normalized);
4436
- if (renamed !== item) itemsChanged = true;
4437
- return renamed;
4438
- });
4439
- if (itemsChanged) next = { ...next, [itemSpec.itemsKey]: nextItems };
4862
+ const itemSpecs = COMPONENT_ITEM_FIELD_ALIASES[component.type];
4863
+ if (itemSpecs) {
4864
+ for (const itemSpec of itemSpecs) {
4865
+ const items = next[itemSpec.itemsKey];
4866
+ if (Array.isArray(items)) {
4867
+ let itemsChanged = false;
4868
+ const nextItems = items.map((item, ii) => {
4869
+ if (!isPlainObject(item)) return item;
4870
+ const renamed = renameAliases(item, itemSpec.aliases, `${path}.${itemSpec.itemsKey}[${ii}]`, normalized);
4871
+ if (renamed !== item) itemsChanged = true;
4872
+ return renamed;
4873
+ });
4874
+ if (itemsChanged) next = { ...next, [itemSpec.itemsKey]: nextItems };
4875
+ }
4440
4876
  }
4441
4877
  }
4442
4878
  return next;
@@ -5016,288 +5452,61 @@ function resolveArchetypeId(slideType, layouts, seed, pageKey, requestedLayout,
5016
5452
  return Math.max(strategyWeight, beatWeight);
5017
5453
  };
5018
5454
  const salt = `${slideType}-archetype:${pageKey}`;
5019
- const picked = weightedPickBySeed(seed, salt, pool, weightOf);
5020
- if (picked === previousEffectiveLayoutId && pool.length > 1) {
5021
- const remainder = pool.filter((id) => id !== previousEffectiveLayoutId);
5022
- return weightedPickBySeed(seed, salt, remainder, weightOf);
5023
- }
5024
- return picked;
5025
- }
5026
- function resolveIrStrategy(ir) {
5027
- return resolveNarrative(ir.narrative).strategy;
5028
- }
5029
- function resolveOneEffectiveLayoutId(ir, slide, seed, strategy, pageKey, previousEffectiveLayoutId) {
5030
- const tokens = resolveStyle(ir.theme.id, ir.theme.style);
5031
- const bgSpec = slide.background ?? tokens.defaultBackgrounds[slide.type];
5032
- const imageCoverTakeover = bgSpec.kind === "asset" && (slide.type === "cover" || slide.type === "chapter");
5033
- if (imageCoverTakeover) return null;
5034
- if (slide.layout) {
5035
- const requestedLayoutDef = getLayout(slide.layout);
5036
- if (requestedLayoutDef?.kind === "takeover" && findImageComponent(slide) != null) {
5037
- return slide.layout;
5038
- }
5039
- }
5040
- const themeDef = getThemeDefinition(ir.theme.id);
5041
- return resolveArchetypeId(
5042
- slide.type,
5043
- themeDef.layouts,
5044
- seed,
5045
- pageKey,
5046
- slide.layout,
5047
- strategy,
5048
- previousEffectiveLayoutId,
5049
- slide.beat
5050
- );
5051
- }
5052
- var deckEffectiveLayoutIdsCache = /* @__PURE__ */ new WeakMap();
5053
- function resolveDeckEffectiveLayoutIds(ir) {
5054
- const cached = deckEffectiveLayoutIdsCache.get(ir);
5055
- if (cached) return cached;
5056
- const seed = cachedDeckSeed(ir);
5057
- const strategy = resolveIrStrategy(ir);
5058
- const deckIds = [];
5059
- for (let i = 0; i < ir.slides.length; i++) {
5060
- const slide = ir.slides[i];
5061
- const previous = i > 0 ? deckIds[i - 1] : null;
5062
- deckIds.push(resolveOneEffectiveLayoutId(ir, slide, seed, strategy, slide.id ?? String(i), previous));
5063
- }
5064
- deckEffectiveLayoutIdsCache.set(ir, deckIds);
5065
- return deckIds;
5066
- }
5067
- function resolveEffectiveLayoutId(ir, slide, index) {
5068
- return resolveDeckEffectiveLayoutIds(ir)[index] ?? null;
5069
- }
5070
- function resolveEffectiveLayoutBodyCapacity(ir, slide, index) {
5071
- const layoutId = resolveEffectiveLayoutId(ir, slide, index);
5072
- const capacity = layoutId === null ? void 0 : getLayout(layoutId)?.slots.find((s) => s.name === "body")?.capacity;
5073
- return { layoutId, capacity };
5074
- }
5075
-
5076
- // src/lib/svg-text-layout.ts
5077
- var WIDE_CHAR_RE = /[\u2014\u2018-\u201f\u2e80-\u9fff\uff00-\uffef]/;
5078
- var NO_CORRECTION = { regular: 1, bold: 1 };
5079
- var GEORGIA = {
5080
- space: { regular: 1, bold: 0.7254 },
5081
- wide: NO_CORRECTION,
5082
- upper: { regular: 1, bold: 1.1242 },
5083
- lowerDigit: { regular: 1, bold: 1.0461 },
5084
- other: { regular: 1, bold: 0.9159 }
5085
- };
5086
- var YAHEI = {
5087
- space: { regular: 1, bold: 0.8511 },
5088
- wide: NO_CORRECTION,
5089
- upper: { regular: 1, bold: 1.0317 },
5090
- lowerDigit: { regular: 1, bold: 1.0266 },
5091
- other: { regular: 1, bold: 0.9593 }
5092
- };
5093
- var SIMSUN_KAITI = {
5094
- space: { regular: 1.4286, bold: 1.4286 },
5095
- wide: NO_CORRECTION,
5096
- upper: { regular: 1, bold: 0.852 },
5097
- lowerDigit: { regular: 1, bold: 1.048 },
5098
- other: { regular: 1.2235, bold: 1.2235 }
5099
- };
5100
- var ENVELOPE = {
5101
- space: { regular: 1, bold: Math.max(GEORGIA.space.bold, YAHEI.space.bold, SIMSUN_KAITI.space.bold) },
5102
- wide: { regular: 1, bold: Math.max(GEORGIA.wide.bold, YAHEI.wide.bold, SIMSUN_KAITI.wide.bold) },
5103
- upper: { regular: 1, bold: Math.max(GEORGIA.upper.bold, YAHEI.upper.bold, SIMSUN_KAITI.upper.bold) },
5104
- lowerDigit: {
5105
- regular: 1,
5106
- bold: Math.max(GEORGIA.lowerDigit.bold, YAHEI.lowerDigit.bold, SIMSUN_KAITI.lowerDigit.bold)
5107
- },
5108
- other: { regular: 1, bold: Math.max(GEORGIA.other.bold, YAHEI.other.bold, SIMSUN_KAITI.other.bold) }
5109
- };
5110
- var GEORGIA_REGULAR_EXACT = { 32: 0.2412, 33: 0.3311, 34: 0.4116, 35: 0.6431, 36: 0.6099, 37: 0.8174, 38: 0.7104, 39: 0.2153, 40: 0.375, 41: 0.375, 42: 0.4722, 43: 0.6431, 44: 0.2695, 45: 0.374, 46: 0.2695, 47: 0.4688, 48: 0.6138, 49: 0.4297, 50: 0.5586, 51: 0.5518, 52: 0.5649, 53: 0.5283, 54: 0.5659, 55: 0.5024, 56: 0.5962, 57: 0.5659, 58: 0.3125, 59: 0.3125, 60: 0.6431, 61: 0.6431, 62: 0.6431, 63: 0.4785, 64: 0.9287, 65: 0.6709, 66: 0.6538, 67: 0.6421, 68: 0.749, 69: 0.6533, 70: 0.5991, 71: 0.7251, 72: 0.8149, 73: 0.3896, 74: 0.5176, 75: 0.6943, 76: 0.6035, 77: 0.9272, 78: 0.7671, 79: 0.7441, 80: 0.6099, 81: 0.7441, 82: 0.7017, 83: 0.561, 84: 0.6187, 85: 0.7563, 86: 0.6665, 87: 0.9756, 88: 0.7104, 89: 0.6152, 90: 0.6016, 91: 0.375, 92: 0.4688, 93: 0.375, 94: 0.6431, 95: 0.6431, 96: 0.5, 97: 0.5039, 98: 0.5601, 99: 0.4541, 100: 0.5742, 101: 0.4834, 102: 0.3252, 103: 0.5093, 104: 0.582, 105: 0.293, 106: 0.292, 107: 0.5356, 108: 0.2861, 109: 0.8809, 110: 0.5908, 111: 0.5391, 112: 0.5713, 113: 0.5596, 114: 0.4097, 115: 0.4321, 116: 0.3452, 117: 0.5752, 118: 0.4966, 119: 0.7373, 120: 0.5049, 121: 0.4922, 122: 0.4438, 123: 0.4302, 124: 0.375, 125: 0.4302, 126: 0.6431 };
5111
- var GEORGIA_BOLD_EXACT = { 32: 0.2539, 33: 0.3765, 34: 0.5098, 35: 0.7031, 36: 0.6406, 37: 0.8794, 38: 0.7993, 39: 0.269, 40: 0.4468, 41: 0.4468, 42: 0.4819, 43: 0.7031, 44: 0.3281, 45: 0.3789, 46: 0.3281, 47: 0.4717, 48: 0.7012, 49: 0.4897, 50: 0.6265, 51: 0.6245, 52: 0.6494, 53: 0.5991, 54: 0.6479, 55: 0.5542, 56: 0.6763, 57: 0.6479, 58: 0.3672, 59: 0.3672, 60: 0.7031, 61: 0.7031, 62: 0.7031, 63: 0.5483, 64: 0.9668, 65: 0.7583, 66: 0.7573, 67: 0.7153, 68: 0.834, 69: 0.7212, 70: 0.6714, 71: 0.8071, 72: 0.9131, 73: 0.4458, 74: 0.5952, 75: 0.8169, 76: 0.6855, 77: 1.0234, 78: 0.8394, 79: 0.8198, 80: 0.7012, 81: 0.8198, 82: 0.7974, 83: 0.6489, 84: 0.6841, 85: 0.8335, 86: 0.7622, 87: 1.1265, 88: 0.8086, 89: 0.7319, 90: 0.6895, 91: 0.4468, 92: 0.4717, 93: 0.4468, 94: 0.7031, 95: 0.7031, 96: 0.5, 97: 0.5957, 98: 0.6455, 99: 0.5312, 100: 0.6631, 101: 0.5718, 102: 0.3931, 103: 0.5767, 104: 0.6797, 105: 0.3535, 106: 0.3462, 107: 0.6318, 108: 0.3442, 109: 1.0156, 110: 0.6899, 111: 0.6357, 112: 0.6577, 113: 0.6484, 114: 0.52, 115: 0.5127, 116: 0.3975, 117: 0.6768, 118: 0.5669, 119: 0.8633, 120: 0.5879, 121: 0.562, 122: 0.5254, 123: 0.5, 124: 0.3877, 125: 0.5, 126: 0.7031 };
5112
- var YAHEI_REGULAR_EXACT = { 32: 0.2959, 33: 0.3125, 34: 0.4355, 35: 0.6382, 36: 0.5864, 37: 0.8896, 38: 0.8701, 39: 0.2563, 40: 0.334, 41: 0.334, 42: 0.4551, 43: 0.7417, 44: 0.2407, 45: 0.4326, 46: 0.2407, 47: 0.4272, 48: 0.5864, 49: 0.5864, 50: 0.5864, 51: 0.5864, 52: 0.5864, 53: 0.5864, 54: 0.5864, 55: 0.5864, 56: 0.5864, 57: 0.5864, 58: 0.2407, 59: 0.2407, 60: 0.7417, 61: 0.7417, 62: 0.7417, 63: 0.4829, 64: 1.0312, 65: 0.7036, 66: 0.6274, 67: 0.6689, 68: 0.7617, 69: 0.5498, 70: 0.5312, 71: 0.7437, 72: 0.7734, 73: 0.2939, 74: 0.396, 75: 0.6348, 76: 0.5132, 77: 0.9771, 78: 0.813, 79: 0.8149, 80: 0.6118, 81: 0.8149, 82: 0.6528, 83: 0.5771, 84: 0.5732, 85: 0.7466, 86: 0.6763, 87: 1.0176, 88: 0.645, 89: 0.6035, 90: 0.6201, 91: 0.334, 92: 0.416, 93: 0.334, 94: 0.7417, 95: 0.4482, 96: 0.2949, 97: 0.5527, 98: 0.6387, 99: 0.5015, 100: 0.6396, 101: 0.5674, 102: 0.3467, 103: 0.6396, 104: 0.6157, 105: 0.2661, 106: 0.2671, 107: 0.5444, 108: 0.2661, 109: 0.937, 110: 0.6162, 111: 0.6357, 112: 0.6387, 113: 0.6396, 114: 0.3818, 115: 0.4629, 116: 0.3726, 117: 0.6162, 118: 0.5249, 119: 0.7896, 120: 0.5068, 121: 0.5293, 122: 0.4917, 123: 0.334, 124: 0.269, 125: 0.334, 126: 0.7417 };
5113
- var YAHEI_BOLD_EXACT = { 32: 0.2979, 33: 0.3486, 34: 0.521, 35: 0.6401, 36: 0.6167, 37: 0.9312, 38: 0.9111, 39: 0.3081, 40: 0.3896, 41: 0.3896, 42: 0.4873, 43: 0.7612, 44: 0.2856, 45: 0.4365, 46: 0.2856, 47: 0.4727, 48: 0.6167, 49: 0.6167, 50: 0.6167, 51: 0.6167, 52: 0.6167, 53: 0.6167, 54: 0.6167, 55: 0.6167, 56: 0.6167, 57: 0.6167, 58: 0.2856, 59: 0.2856, 60: 0.7612, 61: 0.7612, 62: 0.7612, 63: 0.4741, 64: 1.0298, 65: 0.752, 66: 0.6836, 67: 0.6733, 68: 0.7915, 69: 0.5718, 70: 0.5581, 71: 0.7651, 72: 0.8213, 73: 0.3354, 74: 0.4702, 75: 0.6929, 76: 0.5469, 77: 1.0283, 78: 0.8481, 79: 0.8184, 80: 0.6572, 81: 0.8184, 82: 0.6982, 83: 0.6016, 84: 0.6255, 85: 0.7764, 86: 0.7148, 87: 1.0762, 88: 0.7002, 89: 0.6484, 90: 0.6504, 91: 0.3896, 92: 0.4644, 93: 0.3896, 94: 0.7612, 95: 0.4482, 96: 0.3335, 97: 0.5776, 98: 0.666, 99: 0.5166, 100: 0.6646, 101: 0.582, 102: 0.4053, 103: 0.6646, 104: 0.6455, 105: 0.2959, 106: 0.3018, 107: 0.5962, 108: 0.2959, 109: 0.9819, 110: 0.6479, 111: 0.6572, 112: 0.666, 113: 0.6646, 114: 0.4238, 115: 0.4937, 116: 0.4141, 117: 0.6479, 118: 0.5771, 119: 0.8516, 120: 0.585, 121: 0.5742, 122: 0.5137, 123: 0.3896, 124: 0.3413, 125: 0.3896, 126: 0.7612 };
5114
- var CLASS_TABLE_FOR = {
5115
- georgia: GEORGIA,
5116
- yahei: YAHEI,
5117
- "simsun-kaiti": SIMSUN_KAITI,
5118
- unknown: ENVELOPE
5119
- };
5120
- var EXACT_TABLE_FOR = {
5121
- georgia: { regular: GEORGIA_REGULAR_EXACT, bold: GEORGIA_BOLD_EXACT },
5122
- yahei: { regular: YAHEI_REGULAR_EXACT, bold: YAHEI_BOLD_EXACT }
5123
- };
5124
- function classifyFaceKey(fontFamily) {
5125
- const first = fontFamily?.split(",")[0]?.replace(/['"]/g, "").trim().toLowerCase();
5126
- if (!first) return "unknown";
5127
- if (first === "georgia") return "georgia";
5128
- if (first === "microsoft yahei" || first === "\u5FAE\u8F6F\u96C5\u9ED1") return "yahei";
5129
- if (first === "simsun" || first === "\u5B8B\u4F53" || first === "kaiti" || first === "\u6977\u4F53") return "simsun-kaiti";
5130
- return "unknown";
5131
- }
5132
- function classAverageUnits(char, table, mode) {
5133
- if (/\s/.test(char)) return 0.35 * table.space[mode];
5134
- if (WIDE_CHAR_RE.test(char)) return 1 * table.wide[mode];
5135
- if (/[A-Z]/.test(char)) return 0.66 * table.upper[mode];
5136
- if (/[a-z0-9]/.test(char)) return 0.56 * table.lowerDigit[mode];
5137
- return 0.46 * table.other[mode];
5138
- }
5139
- function measureTextUnits(text, weight) {
5140
- const mode = weight?.bold ? "bold" : "regular";
5141
- const faceKey = classifyFaceKey(weight?.fontFamily);
5142
- const classTable = CLASS_TABLE_FOR[faceKey];
5143
- const exactTable = mode === "bold" ? EXACT_TABLE_FOR[faceKey]?.bold : void 0;
5144
- return Array.from(text).reduce((sum, char) => {
5145
- if (exactTable && !WIDE_CHAR_RE.test(char)) {
5146
- const exact = exactTable[char.charCodeAt(0)];
5147
- if (exact !== void 0) return sum + exact;
5148
- }
5149
- return sum + classAverageUnits(char, classTable, mode);
5150
- }, 0);
5151
- }
5152
- var MONO_ADVANCE_EM = 1126 / 2048;
5153
- function measureMonoTextUnits(text) {
5154
- return Array.from(text).reduce((sum, char) => {
5155
- if (WIDE_CHAR_RE.test(char)) return sum + 1;
5156
- return sum + MONO_ADVANCE_EM;
5157
- }, 0);
5158
- }
5159
- function splitLongToken(token, maxUnits, weight) {
5160
- const chunks = [];
5161
- let current = "";
5162
- for (const char of Array.from(token)) {
5163
- const candidate = `${current}${char}`;
5164
- if (current && measureTextUnits(candidate, weight) > maxUnits) {
5165
- chunks.push(current);
5166
- current = char;
5167
- } else {
5168
- current = candidate;
5169
- }
5170
- }
5171
- if (current) chunks.push(current);
5172
- return chunks;
5173
- }
5174
- function tokenize(text) {
5175
- const normalized = text.trim().replace(/\s+/g, " ");
5176
- if (!normalized) return { tokens: [], spaceDelimited: false };
5177
- const spaceDelimited = normalized.includes(" ");
5178
- return {
5179
- tokens: spaceDelimited ? normalized.split(" ") : Array.from(normalized),
5180
- spaceDelimited
5181
- };
5182
- }
5183
- function wrapWithUnits(text, maxUnits, weight) {
5184
- const lines = [];
5185
- for (const paragraph2 of text.split(/\n+/)) {
5186
- const { tokens, spaceDelimited } = tokenize(paragraph2);
5187
- let current = "";
5188
- for (const token of tokens) {
5189
- const tokenChunks = measureTextUnits(token, weight) > maxUnits ? splitLongToken(token, maxUnits, weight) : [token];
5190
- for (const [chunkIndex, chunk] of tokenChunks.entries()) {
5191
- const prefix = current && spaceDelimited && chunkIndex === 0 ? " " : "";
5192
- const candidate = `${current}${prefix}${chunk}`;
5193
- if (current && measureTextUnits(candidate, weight) > maxUnits) {
5194
- lines.push(current);
5195
- current = chunk;
5196
- } else {
5197
- current = candidate;
5198
- }
5199
- }
5200
- }
5201
- if (current) lines.push(current);
5202
- }
5203
- return lines;
5204
- }
5205
- function balanceWrappedLines(content, lines, weight) {
5206
- if (lines.length < 2 || content.includes("\n")) return lines;
5207
- const units = lines.map((l) => measureTextUnits(l, weight));
5208
- const widest = Math.max(...units);
5209
- if (units[units.length - 1] >= widest * 0.5) return lines;
5210
- const total = units.reduce((sum, u) => sum + u, 0);
5211
- const { tokens } = tokenize(content);
5212
- const longestToken = Math.max(...tokens.map((t) => measureTextUnits(t, weight)), 0);
5213
- let target = Math.max(total / lines.length, longestToken);
5214
- for (let i = 0; i < 8; i += 1) {
5215
- const candidate = wrapWithUnits(content, target, weight);
5216
- if (candidate.length === lines.length) return candidate;
5217
- if (candidate.length < lines.length) return lines;
5218
- target *= 1.06;
5455
+ const picked = weightedPickBySeed(seed, salt, pool, weightOf);
5456
+ if (picked === previousEffectiveLayoutId && pool.length > 1) {
5457
+ const remainder = pool.filter((id) => id !== previousEffectiveLayoutId);
5458
+ return weightedPickBySeed(seed, salt, remainder, weightOf);
5219
5459
  }
5220
- return lines;
5460
+ return picked;
5221
5461
  }
5222
- function truncateToUnits(text, maxUnits, weight) {
5223
- if (measureTextUnits(text, weight) <= maxUnits) return text;
5224
- const budget = maxUnits - 1;
5225
- let out = "";
5226
- for (const ch of Array.from(text)) {
5227
- if (measureTextUnits(out + ch, weight) > budget) break;
5228
- out += ch;
5229
- }
5230
- if (out === "") {
5231
- return measureTextUnits("\u2026", weight) > maxUnits ? "" : "\u2026";
5232
- }
5233
- return `${out}\u2026`;
5462
+ function resolveIrStrategy(ir) {
5463
+ return resolveNarrative(ir.narrative).strategy;
5234
5464
  }
5235
- function truncateToMonoUnits(text, maxUnits) {
5236
- if (measureMonoTextUnits(text) <= maxUnits) return text;
5237
- const budget = maxUnits - 1;
5238
- let out = "";
5239
- for (const ch of Array.from(text)) {
5240
- if (measureMonoTextUnits(out + ch) > budget) break;
5241
- out += ch;
5465
+ function resolveOneEffectiveLayoutId(ir, slide, seed, strategy, pageKey, previousEffectiveLayoutId) {
5466
+ const tokens = resolveStyle(ir.theme.id, ir.theme.style);
5467
+ const bgSpec = slide.background ?? tokens.defaultBackgrounds[slide.type];
5468
+ const imageCoverTakeover = bgSpec.kind === "asset" && (slide.type === "cover" || slide.type === "chapter");
5469
+ if (imageCoverTakeover) return null;
5470
+ if (slide.layout) {
5471
+ const requestedLayoutDef = getLayout(slide.layout);
5472
+ if (requestedLayoutDef?.kind === "takeover" && findImageComponent(slide) != null) {
5473
+ return slide.layout;
5474
+ }
5242
5475
  }
5243
- if (out === "") {
5244
- return measureMonoTextUnits("\u2026") > maxUnits ? "" : "\u2026";
5476
+ const themeDef = getThemeDefinition(ir.theme.id);
5477
+ return resolveArchetypeId(
5478
+ slide.type,
5479
+ themeDef.layouts,
5480
+ seed,
5481
+ pageKey,
5482
+ slide.layout,
5483
+ strategy,
5484
+ previousEffectiveLayoutId,
5485
+ slide.beat
5486
+ );
5487
+ }
5488
+ var deckEffectiveLayoutIdsCache = /* @__PURE__ */ new WeakMap();
5489
+ function resolveDeckEffectiveLayoutIds(ir) {
5490
+ const cached = deckEffectiveLayoutIdsCache.get(ir);
5491
+ if (cached) return cached;
5492
+ const seed = cachedDeckSeed(ir);
5493
+ const strategy = resolveIrStrategy(ir);
5494
+ const deckIds = [];
5495
+ for (let i = 0; i < ir.slides.length; i++) {
5496
+ const slide = ir.slides[i];
5497
+ const previous = i > 0 ? deckIds[i - 1] : null;
5498
+ deckIds.push(resolveOneEffectiveLayoutId(ir, slide, seed, strategy, slide.id ?? String(i), previous));
5245
5499
  }
5246
- return `${out}\u2026`;
5500
+ deckEffectiveLayoutIdsCache.set(ir, deckIds);
5501
+ return deckIds;
5247
5502
  }
5248
- function fitSvgLine(text, opts) {
5249
- const minFontSize = opts.minFontSize ?? 12;
5250
- const letterSpacing = opts.letterSpacing ?? 0;
5251
- const weight = { bold: opts.bold, fontFamily: opts.fontFamily };
5252
- const units = measureTextUnits(text, weight);
5253
- if (units <= 0) return { text, fontSize: opts.fontSize, truncated: false };
5254
- const charCount = Array.from(text).length;
5255
- const spacingBudget = Math.max(0, charCount - 1) * letterSpacing;
5256
- const availableWidth = Math.max(0, opts.maxWidth - spacingBudget);
5257
- const fitted = Math.min(opts.fontSize, Math.floor(availableWidth / units));
5258
- if (fitted >= minFontSize) return { text, fontSize: fitted, truncated: false };
5259
- return {
5260
- text: truncateToUnits(text, availableWidth / minFontSize, weight),
5261
- fontSize: minFontSize,
5262
- truncated: true
5263
- };
5503
+ function resolveEffectiveLayoutId(ir, slide, index) {
5504
+ return resolveDeckEffectiveLayoutIds(ir)[index] ?? null;
5264
5505
  }
5265
- function layoutSvgText(text, options) {
5266
- const content = text?.trim() ?? "";
5267
- const maxLines = options.maxLines ?? 2;
5268
- const lineHeightRatio = options.lineHeightRatio ?? 1.08;
5269
- const weight = { bold: options.bold, fontFamily: options.fontFamily };
5270
- if (!content) {
5271
- return { lines: [], fontSize: options.fontSize, lineHeight: 0, truncated: false };
5272
- }
5273
- const baseUnits = options.maxWidth / options.fontSize;
5274
- let maxUnits = baseUnits;
5275
- let lines = wrapWithUnits(content, maxUnits, weight);
5276
- for (let i = 0; lines.length > maxLines && i < 8; i += 1) {
5277
- maxUnits *= 1.14;
5278
- lines = wrapWithUnits(content, maxUnits, weight);
5279
- }
5280
- if (lines.length > maxLines) {
5281
- lines = [
5282
- ...lines.slice(0, maxLines - 1),
5283
- lines.slice(maxLines - 1).join("")
5284
- ];
5285
- }
5286
- if (options.balanceLines) {
5287
- lines = balanceWrappedLines(content, lines, weight);
5288
- }
5289
- const longest = Math.max(...lines.map((l) => measureTextUnits(l, weight)), 1);
5290
- const fittedFontSize2 = Math.min(
5291
- options.fontSize,
5292
- Math.floor(options.maxWidth / longest)
5293
- );
5294
- const fontSize = Math.max(1, fittedFontSize2);
5295
- return {
5296
- lines,
5297
- fontSize,
5298
- lineHeight: Math.round(fontSize * lineHeightRatio),
5299
- truncated: false
5300
- };
5506
+ function resolveEffectiveLayoutBodyCapacity(ir, slide, index) {
5507
+ const layoutId = resolveEffectiveLayoutId(ir, slide, index);
5508
+ const capacity = layoutId === null ? void 0 : getLayout(layoutId)?.slots.find((s) => s.name === "body")?.capacity;
5509
+ return { layoutId, capacity };
5301
5510
  }
5302
5511
 
5303
5512
  // src/svg/ir-quality.ts
@@ -6600,82 +6809,6 @@ function ellipseToOp(el, gradients) {
6600
6809
  return op;
6601
6810
  }
6602
6811
 
6603
- // src/svg/fonts.ts
6604
- var SAFE_FONTS = new Set(
6605
- [
6606
- // Latin sans
6607
- "Arial",
6608
- "Calibri",
6609
- "Tahoma",
6610
- "Verdana",
6611
- "Segoe UI",
6612
- // Latin serif
6613
- "Georgia",
6614
- "Times New Roman",
6615
- "Cambria",
6616
- // Latin mono
6617
- "Consolas",
6618
- "Courier New",
6619
- "Lucida Console",
6620
- // CJK (Windows)
6621
- "Microsoft YaHei",
6622
- "\u5FAE\u8F6F\u96C5\u9ED1",
6623
- "SimSun",
6624
- "\u5B8B\u4F53",
6625
- "SimHei",
6626
- "\u9ED1\u4F53",
6627
- "KaiTi",
6628
- "\u6977\u4F53",
6629
- "FangSong",
6630
- "\u4EFF\u5B8B"
6631
- ].map((f) => f.toLowerCase())
6632
- );
6633
- var ROLE_DEFAULT = {
6634
- heading: "Microsoft YaHei",
6635
- body: "Microsoft YaHei",
6636
- mono: "Consolas"
6637
- };
6638
- function resolveFontFace(stack, role) {
6639
- for (const raw of stack) {
6640
- const name = raw.replace(/['"]/g, "").trim();
6641
- if (SAFE_FONTS.has(name.toLowerCase())) return name;
6642
- }
6643
- return ROLE_DEFAULT[role];
6644
- }
6645
- var CJK_SAFE_FACES = new Set(
6646
- ["Microsoft YaHei", "\u5FAE\u8F6F\u96C5\u9ED1", "SimSun", "\u5B8B\u4F53", "SimHei", "\u9ED1\u4F53", "KaiTi", "\u6977\u4F53", "FangSong", "\u4EFF\u5B8B"].map(
6647
- (f) => f.toLowerCase()
6648
- )
6649
- );
6650
- var EA_FALLBACK_FACE = ROLE_DEFAULT.heading;
6651
- function eaFontFaceFor(latinFace) {
6652
- const name = latinFace.replace(/['"]/g, "").trim();
6653
- return CJK_SAFE_FACES.has(name.toLowerCase()) ? name : EA_FALLBACK_FACE;
6654
- }
6655
- var SERIF_SAFE_FACES = new Set(
6656
- ["Georgia", "Times New Roman", "Cambria", "SimSun", "\u5B8B\u4F53", "FangSong", "\u4EFF\u5B8B", "KaiTi", "\u6977\u4F53"].map(
6657
- (f) => f.toLowerCase()
6658
- )
6659
- );
6660
- var PREVIEW_FALLBACK = {
6661
- serif: "Songti SC, STSong, serif",
6662
- sans: "PingFang SC, Helvetica Neue, sans-serif",
6663
- mono: "Menlo, monospace"
6664
- };
6665
- function resolveFontStack(stack, role) {
6666
- const face = resolveFontFace(stack, role);
6667
- const fallback = role === "mono" ? PREVIEW_FALLBACK.mono : SERIF_SAFE_FACES.has(face.toLowerCase()) ? PREVIEW_FALLBACK.serif : PREVIEW_FALLBACK.sans;
6668
- return `${face}, ${fallback}`;
6669
- }
6670
- function isMonoFontFamily(fontFamily) {
6671
- return fontFamily.endsWith(`, ${PREVIEW_FALLBACK.mono}`);
6672
- }
6673
- function isBold(weight) {
6674
- if (!weight) return false;
6675
- if (weight === "bold" || weight === "bolder") return true;
6676
- return parseInt(weight, 10) >= 600;
6677
- }
6678
-
6679
6812
  // src/pptx/svg2pptx/text.ts
6680
6813
  var ASCENT_RATIO = 0.8;
6681
6814
  function num5(el, name, fallback = 0) {
@@ -6770,6 +6903,10 @@ function textToOp(el) {
6770
6903
  }
6771
6904
 
6772
6905
  // src/pptx/svg2pptx/line.ts
6906
+ var EMU_PER_IN = 914400;
6907
+ function roundsToZeroEmu(px) {
6908
+ return Math.round(Math.abs(px) / PX_PER_IN * EMU_PER_IN) === 0;
6909
+ }
6773
6910
  function num6(el, name, fallback = 0) {
6774
6911
  const v = el.getAttribute(name);
6775
6912
  if (v == null) return fallback;
@@ -6789,7 +6926,7 @@ function lineToOp(el) {
6789
6926
  const y2 = num6(el, "y2");
6790
6927
  const dx = x2 - x1;
6791
6928
  const dy = y2 - y1;
6792
- const isPoint = dx === 0 && dy === 0;
6929
+ const isPoint = roundsToZeroEmu(dx) && roundsToZeroEmu(dy);
6793
6930
  const w = isPoint ? 0.75 : Math.abs(dx);
6794
6931
  const h = isPoint ? 0.75 : Math.abs(dy);
6795
6932
  const strokeColor = el.getAttribute("stroke") || "#000000";
@@ -8019,62 +8156,6 @@ var bullets = {
8019
8156
  }
8020
8157
  };
8021
8158
 
8022
- // src/svg/ink.ts
8023
- var LUMINANCE_INK_THRESHOLD = 0.4;
8024
- var LARGE_TEXT_MIN_PX = 24;
8025
- var CONTRAST_RATIO_LARGE = 3;
8026
- var CONTRAST_RATIO_BODY = 4.5;
8027
- function relativeLuminance(hex) {
8028
- let h = hex.trim().replace(/^#/, "");
8029
- if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join("");
8030
- if (h.length === 8) h = h.slice(0, 6);
8031
- if (!/^[0-9a-fA-F]{6}$/.test(h)) return 0;
8032
- const n = parseInt(h, 16);
8033
- const chan = [n >> 16 & 255, n >> 8 & 255, n & 255].map((v) => {
8034
- const c = v / 255;
8035
- return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
8036
- });
8037
- return 0.2126 * chan[0] + 0.7152 * chan[1] + 0.0722 * chan[2];
8038
- }
8039
- function contrastRatio(hexA, hexB) {
8040
- const la = relativeLuminance(hexA);
8041
- const lb = relativeLuminance(hexB);
8042
- const lighter = Math.max(la, lb);
8043
- const darker = Math.min(la, lb);
8044
- return (lighter + 0.05) / (darker + 0.05);
8045
- }
8046
- function requiredContrastRatio(fontSizePx) {
8047
- return fontSizePx >= LARGE_TEXT_MIN_PX ? CONTRAST_RATIO_LARGE : CONTRAST_RATIO_BODY;
8048
- }
8049
- var DARK_INK = "#0A0E14";
8050
- var LIGHT_INK = "#FFFFFF";
8051
- function readableOn(bgHex) {
8052
- const darkContrast = contrastRatio(DARK_INK, bgHex);
8053
- const lightContrast = contrastRatio(LIGHT_INK, bgHex);
8054
- if (darkContrast === lightContrast) {
8055
- return relativeLuminance(bgHex) > LUMINANCE_INK_THRESHOLD ? DARK_INK : LIGHT_INK;
8056
- }
8057
- return darkContrast > lightContrast ? DARK_INK : LIGHT_INK;
8058
- }
8059
- function accessibleInk(preferredFill, bgHex, fontSizePx) {
8060
- return contrastRatio(preferredFill, bgHex) >= requiredContrastRatio(fontSizePx) ? preferredFill : readableOn(bgHex);
8061
- }
8062
- function blendOver(fg, bg, alpha2) {
8063
- const toRgb = (hex) => {
8064
- const n = parseInt(hex.replace("#", ""), 16);
8065
- return [n >> 16 & 255, n >> 8 & 255, n & 255];
8066
- };
8067
- const [fr, fgc, fb] = toRgb(fg);
8068
- const [br, bgc, bb] = toRgb(bg);
8069
- const mix = (f, b) => Math.round(f * alpha2 + b * (1 - alpha2));
8070
- const toHex2 = (v) => v.toString(16).padStart(2, "0");
8071
- return `#${toHex2(mix(fr, br))}${toHex2(mix(fgc, bgc))}${toHex2(mix(fb, bb))}`;
8072
- }
8073
- function accessibleOpacity(inkHex, bgHex, fontSizePx, preferredOpacity) {
8074
- const blended = blendOver(inkHex, bgHex, preferredOpacity);
8075
- return contrastRatio(blended, bgHex) >= requiredContrastRatio(fontSizePx) ? preferredOpacity : 1;
8076
- }
8077
-
8078
8159
  // src/svg/components/quote.tsx
8079
8160
  import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
8080
8161
  var QUOTE_ZONE = 60;
@@ -9265,6 +9346,7 @@ function renderFunnel(series, palette, x0, y0, w, h, _mutedColor, _textColor, _a
9265
9346
  }
9266
9347
  var DUMBBELL_LABEL_W = 96;
9267
9348
  var DUMBBELL_DOT_R = 5;
9349
+ var DUMBBELL_VALUE_LABEL_W = 56;
9268
9350
  function renderDumbbell(series, _palette, x0, y0, w, h, mutedColor, textColor, accentColor2, _showGrid) {
9269
9351
  const fromData = series[0]?.data ?? [];
9270
9352
  const toData = series[1]?.data ?? [];
@@ -9274,7 +9356,7 @@ function renderDumbbell(series, _palette, x0, y0, w, h, mutedColor, textColor, a
9274
9356
  const min = Math.min(0, ...all);
9275
9357
  const max = Math.max(...all, 1);
9276
9358
  const plotX = x0 + DUMBBELL_LABEL_W + 12;
9277
- const plotW = Math.max(1, w - DUMBBELL_LABEL_W - 12 - 56);
9359
+ const plotW = Math.max(1, w - DUMBBELL_LABEL_W - 12 - DUMBBELL_VALUE_LABEL_W);
9278
9360
  const rowH = h / rows;
9279
9361
  const vx = (v) => plotX + (v - min) / (max - min) * plotW;
9280
9362
  return /* @__PURE__ */ jsx14(Fragment7, { children: Array.from({ length: rows }, (_, i) => {
@@ -9286,6 +9368,16 @@ function renderDumbbell(series, _palette, x0, y0, w, h, mutedColor, textColor, a
9286
9368
  fontSize: 13,
9287
9369
  minFontSize: 10
9288
9370
  });
9371
+ const fromValueLabel = fitSvgLine(String(from.y), {
9372
+ maxWidth: DUMBBELL_VALUE_LABEL_W,
9373
+ fontSize: LABEL_FONT_SIZE,
9374
+ minFontSize: 10
9375
+ });
9376
+ const toValueLabel = fitSvgLine(String(to.y), {
9377
+ maxWidth: DUMBBELL_VALUE_LABEL_W,
9378
+ fontSize: 12.5,
9379
+ minFontSize: 10
9380
+ });
9289
9381
  const x1 = vx(from.y);
9290
9382
  const x2 = vx(to.y);
9291
9383
  return /* @__PURE__ */ jsxs12("g", { children: [
@@ -9309,25 +9401,27 @@ function renderDumbbell(series, _palette, x0, y0, w, h, mutedColor, textColor, a
9309
9401
  /* @__PURE__ */ jsx14(
9310
9402
  "text",
9311
9403
  {
9404
+ "data-truncated": fromValueLabel.truncated ? "1" : void 0,
9312
9405
  x: x1,
9313
9406
  y: cy - 11,
9314
9407
  textAnchor: "middle",
9315
- fontSize: LABEL_FONT_SIZE,
9408
+ fontSize: fromValueLabel.fontSize,
9316
9409
  fill: mutedColor,
9317
9410
  dominantBaseline: "alphabetic",
9318
- children: from.y
9411
+ children: fromValueLabel.text
9319
9412
  }
9320
9413
  ),
9321
9414
  /* @__PURE__ */ jsx14(
9322
9415
  "text",
9323
9416
  {
9417
+ "data-truncated": toValueLabel.truncated ? "1" : void 0,
9324
9418
  x: x2 + DUMBBELL_DOT_R + 8,
9325
9419
  y: cy + 4,
9326
- fontSize: 12.5,
9420
+ fontSize: toValueLabel.fontSize,
9327
9421
  fontWeight: "bold",
9328
9422
  fill: accentColor2,
9329
9423
  dominantBaseline: "alphabetic",
9330
- children: to.y
9424
+ children: toValueLabel.text
9331
9425
  }
9332
9426
  )
9333
9427
  ] }, i);
@@ -11592,9 +11686,15 @@ function fitYTitleStack(text, availH) {
11592
11686
  const kept = chars.slice(0, Math.max(0, maxChars - 1));
11593
11687
  return { chars: [...kept, "\u2026"], truncated: true };
11594
11688
  }
11595
- function cellLayout2(item, cardW) {
11689
+ function cellLayout2(item, cardW, fontFamily) {
11596
11690
  const contentW = cardW - PAD_X5 * 2;
11597
- const title = fitSvgLine(item.title, { maxWidth: contentW, fontSize: TITLE_SIZE5, minFontSize: 12 });
11691
+ const title = fitSvgLine(item.title, {
11692
+ maxWidth: contentW,
11693
+ fontSize: TITLE_SIZE5,
11694
+ minFontSize: 12,
11695
+ bold: true,
11696
+ fontFamily
11697
+ });
11598
11698
  const tag = item.tag ? fitSvgLine(item.tag, { maxWidth: contentW, fontSize: TAG_SIZE, minFontSize: 10 }) : null;
11599
11699
  const contentH = TITLE_LH3 + (tag ? GAP_TITLE_TAG + TAG_LH : 0);
11600
11700
  return { title, tag, contentH };
@@ -11666,7 +11766,7 @@ var matrix = {
11666
11766
  const row = Math.floor(i / cols);
11667
11767
  const x = box.x + gridX0 + col * (cardW + CARD_GAP2);
11668
11768
  const y = gridTop + row * (rowH + CARD_GAP2);
11669
- const cell = cellLayout2(item, cardW);
11769
+ const cell = cellLayout2(item, cardW, ctx.fonts.heading);
11670
11770
  const titleBaseline = y + PAD_TOP3 + TITLE_SIZE5;
11671
11771
  return /* @__PURE__ */ jsxs24("g", { children: [
11672
11772
  /* @__PURE__ */ jsx26(
@@ -14851,6 +14951,7 @@ function fitHeadingLines(text, opts) {
14851
14951
  maxLines,
14852
14952
  lineHeightRatio,
14853
14953
  balanceLines: true,
14954
+ minPt,
14854
14955
  ...weight
14855
14956
  });
14856
14957
  if (!content.trim() || first.fontSize >= minPt) return first;
@@ -14862,6 +14963,7 @@ function fitHeadingLines(text, opts) {
14862
14963
  maxLines,
14863
14964
  lineHeightRatio,
14864
14965
  balanceLines: true,
14966
+ minPt,
14865
14967
  ...weight
14866
14968
  });
14867
14969
  return { ...result, truncated: truncatedContent !== content };
@@ -20726,7 +20828,7 @@ function resolveMotifId(ir, slide, index) {
20726
20828
 
20727
20829
  // src/svg/FullSlideSvg.tsx
20728
20830
  import { jsx as jsx89, jsxs as jsxs85 } from "react/jsx-runtime";
20729
- function resolveBackgroundHex(spec, surfaceFallback) {
20831
+ function resolveBackgroundHex2(spec, surfaceFallback) {
20730
20832
  if (spec.kind === "color") return spec.value;
20731
20833
  if (spec.kind === "gradient") return spec.from;
20732
20834
  return surfaceFallback;
@@ -20734,7 +20836,7 @@ function resolveBackgroundHex(spec, surfaceFallback) {
20734
20836
  function resolveOverrideBackgroundHex(spec, surfaceFallback, paintedFallback) {
20735
20837
  if (spec.kind === "gradient") return gradientBands(spec.from, spec.to, 3)[1];
20736
20838
  if (spec.kind === "asset") return paintedFallback;
20737
- return resolveBackgroundHex(spec, surfaceFallback);
20839
+ return resolveBackgroundHex2(spec, surfaceFallback);
20738
20840
  }
20739
20841
  function buildCtx(tokens, images, components, defaultBg, bodyFontPx, chartPaletteOffset) {
20740
20842
  return {
@@ -20779,7 +20881,7 @@ function FullSlideSvg({
20779
20881
  preserveAspectRatio
20780
20882
  }) {
20781
20883
  const tokens = resolveStyle(ir.theme.id, ir.theme.style);
20782
- const themeDefaultBg = resolveBackgroundHex(tokens.defaultBackgrounds[slide.type], tokens.colors.surface);
20884
+ const themeDefaultBg = resolveBackgroundHex2(tokens.defaultBackgrounds[slide.type], tokens.colors.surface);
20783
20885
  const defaultBg = slide.background ? resolveOverrideBackgroundHex(slide.background, tokens.colors.surface, themeDefaultBg) : themeDefaultBg;
20784
20886
  const bodyFontPx = PACING_BUDGETS[resolveNarrative(ir.narrative).pacing].bodyBaselinePx;
20785
20887
  const chartPaletteOffset = resolveChartPaletteOffset(cachedDeckSeed(ir), tokens.colors.chartPalette.length);
@@ -22065,6 +22167,11 @@ function findContrastIssues(markup) {
22065
22167
  function __collectImageBackedTextRuns(markup) {
22066
22168
  return runContrastWalk(markup).imageBackedRuns;
22067
22169
  }
22170
+ function resolveCandidateFill(rawFill) {
22171
+ if (rawFill?.startsWith("#")) return rawFill;
22172
+ if (rawFill?.startsWith("url(")) return null;
22173
+ return void 0;
22174
+ }
22068
22175
  function runContrastWalk(markup) {
22069
22176
  const root = parseSvg(markup);
22070
22177
  const issues = [];
@@ -22132,7 +22239,8 @@ function runContrastWalk(markup) {
22132
22239
  } else {
22133
22240
  const shapeFill = el.getAttribute("fill");
22134
22241
  const opaqueEnough = currentFillOpacity * currentOpacityProduct >= MIN_BG_OPACITY;
22135
- if (shapeFill?.startsWith("#") && opaqueEnough) {
22242
+ const resolvedFill = resolveCandidateFill(shapeFill);
22243
+ if (resolvedFill !== void 0 && opaqueEnough) {
22136
22244
  if (localWedge) {
22137
22245
  paintedShapes.push(
22138
22246
  sectorShape(
@@ -22144,14 +22252,14 @@ function runContrastWalk(markup) {
22144
22252
  startA: localWedge.startA,
22145
22253
  span: localWedge.span
22146
22254
  },
22147
- shapeFill
22255
+ resolvedFill
22148
22256
  )
22149
22257
  );
22150
22258
  } else {
22151
- paintedShapes.push(rectShape(absX, absY, w, h, shapeFill));
22259
+ paintedShapes.push(rectShape(absX, absY, w, h, resolvedFill));
22152
22260
  }
22153
22261
  if (w * h >= MIN_BG_REGION_AREA) {
22154
- regions.push({ x: absX, y: absY, w, h, fill: shapeFill });
22262
+ regions.push({ x: absX, y: absY, w, h, fill: resolvedFill });
22155
22263
  }
22156
22264
  }
22157
22265
  }
@@ -22163,9 +22271,10 @@ function runContrastWalk(markup) {
22163
22271
  const localRy = Number(el.getAttribute(tag === "circle" ? "r" : "ry") ?? 0);
22164
22272
  const shapeFill = el.getAttribute("fill");
22165
22273
  const opaqueEnough = currentFillOpacity * currentOpacityProduct >= MIN_BG_OPACITY;
22166
- if (!inDecorSubtree && shapeFill?.startsWith("#") && opaqueEnough) {
22274
+ const resolvedFill = resolveCandidateFill(shapeFill);
22275
+ if (!inDecorSubtree && resolvedFill !== void 0 && opaqueEnough) {
22167
22276
  paintedShapes.push(
22168
- ellipseShape(ax + cx * as, ay + cy * as, localRx * as, localRy * as, shapeFill)
22277
+ ellipseShape(ax + cx * as, ay + cy * as, localRx * as, localRy * as, resolvedFill)
22169
22278
  );
22170
22279
  }
22171
22280
  } else if (tag === "text" || tag === "tspan") {
@@ -22432,7 +22541,7 @@ function auditDeck(ir, opts = {}) {
22432
22541
  return runPixelPass(ir, report);
22433
22542
  }
22434
22543
  async function runPixelPass(ir, report) {
22435
- const { runPixelContrastAudit } = await import("./pixel-audit-HJEJ36WB.js");
22544
+ const { runPixelContrastAudit } = await import("./pixel-audit-ZB4RSPHG.js");
22436
22545
  const pixelFindings = await runPixelContrastAudit(ir);
22437
22546
  return {
22438
22547
  ...report,
@@ -22487,4 +22596,4 @@ export {
22487
22596
  __collectImageBackedTextRuns,
22488
22597
  auditDeck
22489
22598
  };
22490
- //# sourceMappingURL=chunk-4NKLJ7OK.js.map
22599
+ //# sourceMappingURL=chunk-6JJGXB5S.js.map