slotdeck 0.1.1 → 0.2.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 +94 -8
- package/dist/deck/alternatives.d.ts +34 -0
- package/dist/deck/alternatives.js +370 -0
- package/dist/deck/alternatives.js.map +1 -0
- package/dist/deck/design.d.ts +40 -0
- package/dist/deck/design.js +112 -0
- package/dist/deck/design.js.map +1 -0
- package/dist/deck/validate.d.ts +18 -0
- package/dist/deck/validate.js +44 -6
- package/dist/deck/validate.js.map +1 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/gemini.d.ts +4 -0
- package/dist/llm/gemini.js +43 -7
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/provider.d.ts +12 -0
- package/dist/node.d.ts +7 -0
- package/dist/node.js +29 -0
- package/dist/node.js.map +1 -1
- package/dist/plan/catalog.d.ts +11 -1
- package/dist/plan/catalog.js +23 -8
- package/dist/plan/catalog.js.map +1 -1
- package/dist/plan/edit.js +14 -6
- package/dist/plan/edit.js.map +1 -1
- package/dist/plan/plan.d.ts +26 -2
- package/dist/plan/plan.js +58 -17
- package/dist/plan/plan.js.map +1 -1
- package/dist/plan/review.d.ts +43 -0
- package/dist/plan/review.js +141 -0
- package/dist/plan/review.js.map +1 -0
- package/dist/plan/write.d.ts +13 -1
- package/dist/plan/write.js +40 -7
- package/dist/plan/write.js.map +1 -1
- package/dist/preview/pagus.d.ts +30 -0
- package/dist/preview/pagus.js +45 -0
- package/dist/preview/pagus.js.map +1 -0
- package/dist/preview/patterns.d.ts +27 -0
- package/dist/preview/patterns.js +35 -0
- package/dist/preview/patterns.js.map +1 -0
- package/dist/preview/provider.d.ts +30 -0
- package/dist/preview/provider.js +11 -0
- package/dist/preview/provider.js.map +1 -0
- package/dist/render/render.d.ts +18 -1
- package/dist/render/render.js +32 -12
- package/dist/render/render.js.map +1 -1
- package/dist/render/repeat.d.ts +17 -2
- package/dist/render/repeat.js +150 -4
- package/dist/render/repeat.js.map +1 -1
- package/dist/render/text.d.ts +6 -0
- package/dist/render/text.js +24 -0
- package/dist/render/text.js.map +1 -1
- package/dist/template/budgets.d.ts +33 -0
- package/dist/template/budgets.js +38 -0
- package/dist/template/budgets.js.map +1 -0
- package/dist/template/change.d.ts +19 -1
- package/dist/template/change.js +45 -3
- package/dist/template/change.js.map +1 -1
- package/dist/template/check.d.ts +3 -0
- package/dist/template/check.js +53 -3
- package/dist/template/check.js.map +1 -1
- package/dist/template/derive.d.ts +54 -0
- package/dist/template/derive.js +402 -0
- package/dist/template/derive.js.map +1 -0
- package/dist/template/draft.d.ts +7 -0
- package/dist/template/draft.js +17 -1
- package/dist/template/draft.js.map +1 -1
- package/dist/template/instruct.js +15 -7
- package/dist/template/instruct.js.map +1 -1
- package/dist/template/manifest.d.ts +38 -0
- package/dist/template/manifest.js +43 -1
- package/dist/template/manifest.js.map +1 -1
- package/dist/template/sample.d.ts +17 -0
- package/dist/template/sample.js +31 -5
- package/dist/template/sample.js.map +1 -1
- package/dist/text/fontkit.d.ts +18 -0
- package/dist/text/fontkit.js +91 -0
- package/dist/text/fontkit.js.map +1 -0
- package/dist/text/fonts.d.ts +45 -0
- package/dist/text/fonts.js +59 -0
- package/dist/text/fonts.js.map +1 -0
- package/dist/text/layout.d.ts +12 -0
- package/dist/text/layout.js +104 -0
- package/dist/text/layout.js.map +1 -0
- package/dist/text/measure.d.ts +85 -0
- package/dist/text/measure.js +369 -0
- package/dist/text/measure.js.map +1 -0
- package/dist/text/styles.d.ts +123 -0
- package/dist/text/styles.js +417 -0
- package/dist/text/styles.js.map +1 -0
- package/dist/tools.d.ts +12 -0
- package/dist/tools.js +38 -6
- package/dist/tools.js.map +1 -1
- package/docs/README.md +16 -5
- package/docs/api-reference.md +291 -9
- package/docs/changelog.md +242 -0
- package/docs/chat-tools.md +26 -5
- package/docs/deck-format.md +6 -0
- package/docs/design-checks.md +216 -0
- package/docs/generating.md +43 -4
- package/docs/layout-freedom.md +278 -3
- package/docs/manifest-format.md +20 -3
- package/docs/preview.md +217 -0
- package/docs/providers.md +49 -4
- package/docs/rendering.md +8 -0
- package/docs/template-studio.md +121 -12
- package/docs/text-fit.md +456 -0
- package/docs/troubleshooting.md +21 -2
- package/package.json +7 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// What measuring text needs from a font library, and which typeface a character is set in.
|
|
2
|
+
/**
|
|
3
|
+
* The width of a piece of text at a font size, in points, as PowerPoint lays it out: on a grid of
|
|
4
|
+
* an eighth of a point, to which the advance of every glyph is rounded. At small sizes that
|
|
5
|
+
* rounding moves a line's width by a percent or two, enough to move a word to the next line.
|
|
6
|
+
*/
|
|
7
|
+
export function textWidth(face, text, size, kerning) {
|
|
8
|
+
let units = 0;
|
|
9
|
+
for (const advance of face.advances(text, kerning))
|
|
10
|
+
units += Math.round(advance * size * 8);
|
|
11
|
+
return units / 8;
|
|
12
|
+
}
|
|
13
|
+
const COMPLEX = /[\p{Script=Hebrew}\p{Script=Arabic}\p{Script=Syriac}\p{Script=Thaana}\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Devanagari}\p{Script=Bengali}\p{Script=Gurmukhi}\p{Script=Gujarati}\p{Script=Tamil}\p{Script=Telugu}\p{Script=Kannada}\p{Script=Malayalam}\p{Script=Sinhala}\p{Script=Tibetan}\p{Script=Myanmar}]/u;
|
|
14
|
+
const EAST_ASIAN = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Bopomofo} -〿-]/u;
|
|
15
|
+
const STRONG = /[\p{L}\p{M}]/u;
|
|
16
|
+
const classes = new Map();
|
|
17
|
+
/** The typeface class of a character, or `undefined` for one that takes its neighbour's (a space, a digit, punctuation). */
|
|
18
|
+
export function scriptClass(char) {
|
|
19
|
+
if (classes.has(char))
|
|
20
|
+
return classes.get(char);
|
|
21
|
+
const found = COMPLEX.test(char) ? 'cs' : EAST_ASIAN.test(char) ? 'ea' : STRONG.test(char) ? 'latin' : undefined;
|
|
22
|
+
if (classes.size < 20000)
|
|
23
|
+
classes.set(char, found);
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
/** Whether a line may break before and after the character without a space, as between Chinese or Japanese characters. */
|
|
27
|
+
export function breaksAnywhere(char) {
|
|
28
|
+
return /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Bopomofo}]/u.test(char);
|
|
29
|
+
}
|
|
30
|
+
/** The theme's name for the script of a complex-script or East Asian character, as in `<a:font script="Hebr">`. */
|
|
31
|
+
export function themeScript(char) {
|
|
32
|
+
for (const [tag, test] of THEME_SCRIPTS)
|
|
33
|
+
if (test.test(char))
|
|
34
|
+
return tag;
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const THEME_SCRIPTS = [
|
|
38
|
+
['Hebr', /\p{Script=Hebrew}/u],
|
|
39
|
+
['Arab', /\p{Script=Arabic}/u],
|
|
40
|
+
['Syrc', /\p{Script=Syriac}/u],
|
|
41
|
+
['Thaa', /\p{Script=Thaana}/u],
|
|
42
|
+
['Thai', /\p{Script=Thai}/u],
|
|
43
|
+
['Laoo', /\p{Script=Lao}/u],
|
|
44
|
+
['Khmr', /\p{Script=Khmer}/u],
|
|
45
|
+
['Deva', /\p{Script=Devanagari}/u],
|
|
46
|
+
['Beng', /\p{Script=Bengali}/u],
|
|
47
|
+
['Guru', /\p{Script=Gurmukhi}/u],
|
|
48
|
+
['Gujr', /\p{Script=Gujarati}/u],
|
|
49
|
+
['Taml', /\p{Script=Tamil}/u],
|
|
50
|
+
['Telu', /\p{Script=Telugu}/u],
|
|
51
|
+
['Knda', /\p{Script=Kannada}/u],
|
|
52
|
+
['Mlym', /\p{Script=Malayalam}/u],
|
|
53
|
+
['Sinh', /\p{Script=Sinhala}/u],
|
|
54
|
+
['Tibt', /\p{Script=Tibetan}/u],
|
|
55
|
+
['Hang', /\p{Script=Hangul}/u],
|
|
56
|
+
['Jpan', /[\p{Script=Hiragana}\p{Script=Katakana}]/u],
|
|
57
|
+
['Hans', /\p{Script=Han}/u],
|
|
58
|
+
];
|
|
59
|
+
//# sourceMappingURL=fonts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fonts.js","sourceRoot":"","sources":["../../src/text/fonts.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAiC3F;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,IAAc,EAAE,IAAY,EAAE,IAAY,EAAE,OAAgB;IACpF,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;QAAE,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC;IAC5F,OAAO,KAAK,GAAG,CAAC,CAAC;AACnB,CAAC;AAQD,MAAM,OAAO,GACX,gUAAgU,CAAC;AACnU,MAAM,UAAU,GAAG,mGAAmG,CAAC;AACvH,MAAM,MAAM,GAAG,eAAe,CAAC;AAE/B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAmC,CAAC;AAE3D,4HAA4H;AAC5H,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,KAAK,GAA4B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAC1I,IAAI,OAAO,CAAC,IAAI,GAAG,KAAK;QAAE,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACnD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,0HAA0H;AAC1H,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,4EAA4E,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjG,CAAC;AAED,mHAAmH;AACnH,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,aAAa;QAAE,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,GAAG,CAAC;IACzE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,aAAa,GAA4B;IAC7C,CAAC,MAAM,EAAE,oBAAoB,CAAC;IAC9B,CAAC,MAAM,EAAE,oBAAoB,CAAC;IAC9B,CAAC,MAAM,EAAE,oBAAoB,CAAC;IAC9B,CAAC,MAAM,EAAE,oBAAoB,CAAC;IAC9B,CAAC,MAAM,EAAE,kBAAkB,CAAC;IAC5B,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAC3B,CAAC,MAAM,EAAE,mBAAmB,CAAC;IAC7B,CAAC,MAAM,EAAE,wBAAwB,CAAC;IAClC,CAAC,MAAM,EAAE,qBAAqB,CAAC;IAC/B,CAAC,MAAM,EAAE,sBAAsB,CAAC;IAChC,CAAC,MAAM,EAAE,sBAAsB,CAAC;IAChC,CAAC,MAAM,EAAE,mBAAmB,CAAC;IAC7B,CAAC,MAAM,EAAE,oBAAoB,CAAC;IAC9B,CAAC,MAAM,EAAE,qBAAqB,CAAC;IAC/B,CAAC,MAAM,EAAE,uBAAuB,CAAC;IACjC,CAAC,MAAM,EAAE,qBAAqB,CAAC;IAC/B,CAAC,MAAM,EAAE,qBAAqB,CAAC;IAC/B,CAAC,MAAM,EAAE,oBAAoB,CAAC;IAC9B,CAAC,MAAM,EAAE,2CAA2C,CAAC;IACrD,CAAC,MAAM,EAAE,iBAAiB,CAAC;CAC5B,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breaks one paragraph into lines. `first` and `rest` are the widths the first and the later
|
|
3
|
+
* lines may take; `measure` gives the width of a piece of text set on one line, in the same
|
|
4
|
+
* unit. Words go on a line while they fit; a word wider than a whole line is cut where the
|
|
5
|
+
* line ends. Spaces at the end of a line hang outside it and do not count.
|
|
6
|
+
*
|
|
7
|
+
* `inside` names the exception to that: it is asked whether the space after a line that would
|
|
8
|
+
* end with the given text, with the given word to follow, stays inside the line and so needs
|
|
9
|
+
* room. PowerPoint keeps it inside when the text on both sides of that space runs against the
|
|
10
|
+
* paragraph's direction.
|
|
11
|
+
*/
|
|
12
|
+
export declare function breakLines(text: string, first: number, rest: number, measure: (text: string) => number, inside?: (line: string, next: string) => boolean): string[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Breaking a paragraph into lines the way PowerPoint does, given a way to measure a piece of text.
|
|
2
|
+
import { breaksAnywhere } from './fonts.js';
|
|
3
|
+
/** A line may break after a hyphen or a dash, as well as at a space. Not at a slash, a full stop or a colon. */
|
|
4
|
+
const BREAK_AFTER = /[-‐–—]/;
|
|
5
|
+
/** Chinese and Japanese closing punctuation, which never starts a line. */
|
|
6
|
+
const CLOSING = /[、。,.!?」』)】ー]/;
|
|
7
|
+
const SPACE = /[ \t - ]/;
|
|
8
|
+
function units(text) {
|
|
9
|
+
const out = [];
|
|
10
|
+
let word = '';
|
|
11
|
+
const close = (gap = '') => {
|
|
12
|
+
if (word !== '' || gap !== '')
|
|
13
|
+
out.push({ word, gap });
|
|
14
|
+
word = '';
|
|
15
|
+
};
|
|
16
|
+
const chars = [...text];
|
|
17
|
+
for (let index = 0; index < chars.length; index++) {
|
|
18
|
+
const char = chars[index];
|
|
19
|
+
if (SPACE.test(char)) {
|
|
20
|
+
let gap = char;
|
|
21
|
+
while (index + 1 < chars.length && SPACE.test(chars[index + 1]))
|
|
22
|
+
gap += chars[++index];
|
|
23
|
+
close(gap);
|
|
24
|
+
}
|
|
25
|
+
else if (char === '') {
|
|
26
|
+
close();
|
|
27
|
+
}
|
|
28
|
+
else if (breaksAnywhere(char)) {
|
|
29
|
+
if (CLOSING.test(char)) {
|
|
30
|
+
word += char;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
close();
|
|
34
|
+
word = char;
|
|
35
|
+
const next = chars[index + 1];
|
|
36
|
+
if (next === undefined || !CLOSING.test(next))
|
|
37
|
+
close();
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
word += char;
|
|
41
|
+
if (BREAK_AFTER.test(char) && index + 1 < chars.length && !SPACE.test(chars[index + 1]))
|
|
42
|
+
close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
close();
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Breaks one paragraph into lines. `first` and `rest` are the widths the first and the later
|
|
50
|
+
* lines may take; `measure` gives the width of a piece of text set on one line, in the same
|
|
51
|
+
* unit. Words go on a line while they fit; a word wider than a whole line is cut where the
|
|
52
|
+
* line ends. Spaces at the end of a line hang outside it and do not count.
|
|
53
|
+
*
|
|
54
|
+
* `inside` names the exception to that: it is asked whether the space after a line that would
|
|
55
|
+
* end with the given text, with the given word to follow, stays inside the line and so needs
|
|
56
|
+
* room. PowerPoint keeps it inside when the text on both sides of that space runs against the
|
|
57
|
+
* paragraph's direction.
|
|
58
|
+
*/
|
|
59
|
+
export function breakLines(text, first, rest, measure, inside) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
let line = '';
|
|
62
|
+
let width = 0;
|
|
63
|
+
let gap = '';
|
|
64
|
+
const limit = () => (lines.length === 0 ? first : rest);
|
|
65
|
+
const all = units(text);
|
|
66
|
+
for (let index = 0; index < all.length; index++) {
|
|
67
|
+
const unit = all[index];
|
|
68
|
+
let word = unit.word;
|
|
69
|
+
let wordWidth = word === '' ? 0 : measure(word);
|
|
70
|
+
const gapWidth = line !== '' && gap !== '' ? measure(gap) : 0;
|
|
71
|
+
const next = all[index + 1]?.word;
|
|
72
|
+
const tail = inside && line !== '' && unit.gap !== '' && next !== undefined && inside(line + gap + word, next) ? measure(unit.gap) : 0;
|
|
73
|
+
if (line !== '' && width + gapWidth + wordWidth + tail <= limit()) {
|
|
74
|
+
line += gap + word;
|
|
75
|
+
width += gapWidth + wordWidth;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
if (line !== '') {
|
|
79
|
+
lines.push(line + gap);
|
|
80
|
+
line = '';
|
|
81
|
+
width = 0;
|
|
82
|
+
}
|
|
83
|
+
// A word wider than a whole line is cut by characters.
|
|
84
|
+
while (wordWidth > limit()) {
|
|
85
|
+
const chars = [...word];
|
|
86
|
+
let count = 1;
|
|
87
|
+
while (count < chars.length && measure(chars.slice(0, count + 1).join('')) <= limit())
|
|
88
|
+
count++;
|
|
89
|
+
lines.push(chars.slice(0, count).join(''));
|
|
90
|
+
word = chars.slice(count).join('');
|
|
91
|
+
wordWidth = word === '' ? 0 : measure(word);
|
|
92
|
+
}
|
|
93
|
+
line = word;
|
|
94
|
+
width = wordWidth;
|
|
95
|
+
}
|
|
96
|
+
gap = unit.gap;
|
|
97
|
+
}
|
|
98
|
+
if (line !== '' || lines.length === 0)
|
|
99
|
+
lines.push(line + gap);
|
|
100
|
+
else if (gap !== '')
|
|
101
|
+
lines[lines.length - 1] += gap;
|
|
102
|
+
return lines;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/text/layout.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Template } from '../render/render.ts';
|
|
2
|
+
import type { PatternDef, TextSlotDef } from '../template/manifest.ts';
|
|
3
|
+
import type { FontMetrics } from './fonts.ts';
|
|
4
|
+
/** How far text may be reduced to make it fit. */
|
|
5
|
+
export interface ShrinkLimit {
|
|
6
|
+
/** The smallest share of its size the text may be set at, e.g. 0.85. */
|
|
7
|
+
to: number;
|
|
8
|
+
/** A size in points under which text is never set, whatever `to` allows. */
|
|
9
|
+
minSize?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface FitOptions {
|
|
12
|
+
/** The factor on body text the deck is rendered with: the template's times the deck's. Defaults to 1. */
|
|
13
|
+
textScale?: number;
|
|
14
|
+
/** Allow the text to be set smaller when it does not fit. */
|
|
15
|
+
shrink?: ShrinkLimit;
|
|
16
|
+
/** For a slot of a row of items: how many items the slide has, which can decide how wide an item is. */
|
|
17
|
+
items?: number;
|
|
18
|
+
}
|
|
19
|
+
/** How a text sits in a slot. */
|
|
20
|
+
export interface TextFit {
|
|
21
|
+
/** Whether the text stays inside its box. */
|
|
22
|
+
fits: boolean;
|
|
23
|
+
/** Lines the text takes, over all its paragraphs. */
|
|
24
|
+
lines: number;
|
|
25
|
+
/** Lines of this text the box holds. */
|
|
26
|
+
capacity: number;
|
|
27
|
+
/** Lines that do not fit; 0 when the text fits. */
|
|
28
|
+
overflow: number;
|
|
29
|
+
/** Height the text takes, and the height the box gives it, in points. */
|
|
30
|
+
height: number;
|
|
31
|
+
room: number;
|
|
32
|
+
/** For a box that does not wrap: how much wider than the box the longest line is, as a factor. 1 or less fits. */
|
|
33
|
+
widthRatio: number;
|
|
34
|
+
/** Font size of the first paragraph as measured, in points. */
|
|
35
|
+
size: number;
|
|
36
|
+
/** The font size each paragraph is set at, in points: what the renderer writes when it shrinks the text. */
|
|
37
|
+
sizes: number[];
|
|
38
|
+
/** The factor the text was reduced by to make it fit; 1 when it was not reduced. */
|
|
39
|
+
scale: number;
|
|
40
|
+
/** About how many characters of text like this the box holds. */
|
|
41
|
+
fitsChars: number;
|
|
42
|
+
/** The lines of each paragraph, as they break. */
|
|
43
|
+
paragraphs: string[][];
|
|
44
|
+
/** The width of each of those lines in points, without the space that hangs at its end. */
|
|
45
|
+
widths: number[][];
|
|
46
|
+
/** The width a line of each paragraph may take, in points. */
|
|
47
|
+
lineRoom: number[];
|
|
48
|
+
/**
|
|
49
|
+
* Lines the template's own sample text takes in this slot: how full the designer drew it.
|
|
50
|
+
* 0 when the slot has no sample to compare with (a slot that moves its box or resets its size).
|
|
51
|
+
*/
|
|
52
|
+
sampleLines: number;
|
|
53
|
+
/** A typeface or a weight was stood in for by another, so the figures are close but not exact. */
|
|
54
|
+
approximate: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** A typeface in a weight and slant that the template's text is set in. */
|
|
57
|
+
export interface TemplateFont {
|
|
58
|
+
typeface: string;
|
|
59
|
+
bold: boolean;
|
|
60
|
+
italic: boolean;
|
|
61
|
+
}
|
|
62
|
+
export interface TextMeasurer {
|
|
63
|
+
/**
|
|
64
|
+
* Measures a text in a slot of a pattern. `undefined` when it cannot be measured: the slot's
|
|
65
|
+
* shape holds no text, or a typeface it needs was not supplied.
|
|
66
|
+
*/
|
|
67
|
+
fit(pattern: string | PatternDef, slot: string | TextSlotDef, text: string | string[], options?: FitOptions): TextFit | undefined;
|
|
68
|
+
/** The typefaces the template's text slots are set in for which no font was supplied. */
|
|
69
|
+
missingFonts(): TemplateFont[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Builds a measurer for a template from font metrics. Pass the metrics here, or load the
|
|
73
|
+
* template with them (`loadTemplate(pptx, manifest, { fonts })`), which also makes validation,
|
|
74
|
+
* planning and rendering measure text.
|
|
75
|
+
*/
|
|
76
|
+
export declare function textMeasurer(template: Template, fonts?: FontMetrics | undefined): TextMeasurer;
|
|
77
|
+
/** The measurer of a template that was loaded with fonts, or `undefined` for one that was not. */
|
|
78
|
+
export declare function measurerFor(template: Template): TextMeasurer | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* The typefaces a template's text slots are set in, each with the weights and slants used: the
|
|
81
|
+
* font files to supply for measuring. `language` adds the typeface that text of that language is
|
|
82
|
+
* set in when it differs from the Latin one (Hebrew in a template whose theme sends it to
|
|
83
|
+
* another font); it defaults to the manifest's language.
|
|
84
|
+
*/
|
|
85
|
+
export declare function templateFonts(template: Template, language?: string | undefined): TemplateFont[];
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { detectDirection } from '../render/text.js';
|
|
2
|
+
import { inspectTemplate } from '../template/inspect.js';
|
|
3
|
+
import { rowTextIds } from '../render/repeat.js';
|
|
4
|
+
import { COPY_ID_STEP, effectiveSlots, itemCount, scalesWithText, widenFactor } from '../template/manifest.js';
|
|
5
|
+
import { scriptClass, textWidth, themeScript } from './fonts.js';
|
|
6
|
+
import { breakLines } from './layout.js';
|
|
7
|
+
import { TextStyles } from './styles.js';
|
|
8
|
+
/** A line that hangs under the box by less than this share of its height is counted as fitting. */
|
|
9
|
+
const SLACK = 0.3;
|
|
10
|
+
/** Width in points that a line of shrunk text must leave free: two steps of PowerPoint's layout grid. */
|
|
11
|
+
const SPARE = 0.25;
|
|
12
|
+
/** PowerPoint sets lines 1.2 times the font size apart, whatever the font's own metrics say. */
|
|
13
|
+
const LINE = 1.2;
|
|
14
|
+
/**
|
|
15
|
+
* A slot's box of the shape's own size, to within this share of the slide, moves the shape
|
|
16
|
+
* without resizing it. A manifest states boxes to four decimal places.
|
|
17
|
+
*/
|
|
18
|
+
const MOVED = 0.0005;
|
|
19
|
+
const shared = new WeakMap();
|
|
20
|
+
function sharedFor(pkg) {
|
|
21
|
+
let entry = shared.get(pkg);
|
|
22
|
+
if (!entry) {
|
|
23
|
+
entry = { styles: new TextStyles(pkg), info: inspectTemplate(pkg), shapes: new Map() };
|
|
24
|
+
shared.set(pkg, entry);
|
|
25
|
+
}
|
|
26
|
+
return entry;
|
|
27
|
+
}
|
|
28
|
+
class MissingFont extends Error {
|
|
29
|
+
font;
|
|
30
|
+
constructor(font) {
|
|
31
|
+
super(`no font was supplied for ${font.typeface}`);
|
|
32
|
+
this.font = font;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** The theme's name for the script a language is written in, for the languages whose text takes a typeface of its own. */
|
|
36
|
+
const LANGUAGE_SCRIPTS = { he: 'Hebr', iw: 'Hebr', yi: 'Hebr', ar: 'Arab', fa: 'Arab', ur: 'Arab', ps: 'Arab', ug: 'Arab', sd: 'Arab', ckb: 'Arab', syr: 'Syrc', dv: 'Thaa', th: 'Thai', hi: 'Deva' };
|
|
37
|
+
function typefaceName(run, cls, char) {
|
|
38
|
+
const face = cls === 'cs' ? run.cs : cls === 'ea' ? run.ea : run.latin;
|
|
39
|
+
if (face.name)
|
|
40
|
+
return face.name;
|
|
41
|
+
const script = cls === 'latin' ? undefined : themeScript(char);
|
|
42
|
+
return (script && face.scripts?.get(script)) || run.latin.name;
|
|
43
|
+
}
|
|
44
|
+
const RTL_LANGUAGE = /^(he|iw|yi|ar|fa|ur|ps|ug|sd|ckb|syr|dv)(-|$)/i;
|
|
45
|
+
const DIGIT = /\p{Nd}/u;
|
|
46
|
+
const LAST_LETTER = /(\p{L})[^\p{L}]*$/u;
|
|
47
|
+
/**
|
|
48
|
+
* Measures pieces of text in the typefaces of one run style, at one size. `rtl` says the run is
|
|
49
|
+
* marked with a right-to-left language, which decides the typeface of what is not a letter:
|
|
50
|
+
* PowerPoint then sets digits in the complex-script face and a space or a punctuation mark in
|
|
51
|
+
* the face of the character before it. In any other run they are all set in the Latin face.
|
|
52
|
+
*/
|
|
53
|
+
function runMeasure(fonts, run, size, rtl, flags) {
|
|
54
|
+
const style = { bold: run.bold, italic: run.italic };
|
|
55
|
+
const kerning = run.kern > 0 && size >= run.kern;
|
|
56
|
+
const faces = new Map();
|
|
57
|
+
const faceFor = (cls, char) => {
|
|
58
|
+
const name = typefaceName(run, cls, char);
|
|
59
|
+
let face = faces.get(name);
|
|
60
|
+
if (!face) {
|
|
61
|
+
face = name ? fonts.face(name, style) : undefined;
|
|
62
|
+
if (!face)
|
|
63
|
+
throw new MissingFont({ typeface: name || '(theme font)', ...style });
|
|
64
|
+
faces.set(name, face);
|
|
65
|
+
}
|
|
66
|
+
if (face.substitute)
|
|
67
|
+
flags.approximate = true;
|
|
68
|
+
return face;
|
|
69
|
+
};
|
|
70
|
+
const widths = new Map();
|
|
71
|
+
return (given) => {
|
|
72
|
+
const known = widths.get(given);
|
|
73
|
+
if (known !== undefined)
|
|
74
|
+
return known;
|
|
75
|
+
const text = run.caps ? given.toUpperCase() : given;
|
|
76
|
+
const chars = [...text];
|
|
77
|
+
let current = rtl ? 'cs' : 'latin';
|
|
78
|
+
let width = 0;
|
|
79
|
+
let piece = '';
|
|
80
|
+
let pieceFace;
|
|
81
|
+
let pieceClass = current;
|
|
82
|
+
let pieceLetters = false;
|
|
83
|
+
const flush = () => {
|
|
84
|
+
// Kerning, as PowerPoint applies it: complex-script letters are always kerned with each
|
|
85
|
+
// other, and never with the punctuation, digits or spaces next to them. Latin text is kerned
|
|
86
|
+
// when the run turns kerning on for its size, except in a run marked right-to-left.
|
|
87
|
+
const kerned = pieceClass === 'cs' ? pieceLetters : kerning && !rtl;
|
|
88
|
+
if (piece !== '' && pieceFace)
|
|
89
|
+
width += textWidth(pieceFace, piece, size, kerned);
|
|
90
|
+
piece = '';
|
|
91
|
+
};
|
|
92
|
+
for (const char of chars) {
|
|
93
|
+
const strong = scriptClass(char);
|
|
94
|
+
if (strong)
|
|
95
|
+
current = strong;
|
|
96
|
+
else if (!rtl)
|
|
97
|
+
current = 'latin';
|
|
98
|
+
else if (DIGIT.test(char))
|
|
99
|
+
current = 'cs';
|
|
100
|
+
let face = faceFor(current, char);
|
|
101
|
+
const codePoint = char.codePointAt(0);
|
|
102
|
+
if (!face.has(codePoint) && codePoint > 0x20) {
|
|
103
|
+
flags.approximate = true;
|
|
104
|
+
face = fonts.fallback?.(codePoint, style) ?? face;
|
|
105
|
+
}
|
|
106
|
+
// Complex-script letters are shaped apart from what stands between them.
|
|
107
|
+
const letters = strong !== undefined;
|
|
108
|
+
if (face !== pieceFace || current !== pieceClass || (current === 'cs' && letters !== pieceLetters))
|
|
109
|
+
flush();
|
|
110
|
+
pieceFace = face;
|
|
111
|
+
pieceClass = current;
|
|
112
|
+
pieceLetters = letters;
|
|
113
|
+
piece += char;
|
|
114
|
+
}
|
|
115
|
+
flush();
|
|
116
|
+
width += run.spacing * chars.length;
|
|
117
|
+
widths.set(given, width);
|
|
118
|
+
return width;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const spacing = (value, size) => ('points' in value ? value.points : value.percent * LINE * size);
|
|
122
|
+
/**
|
|
123
|
+
* Builds a measurer for a template from font metrics. Pass the metrics here, or load the
|
|
124
|
+
* template with them (`loadTemplate(pptx, manifest, { fonts })`), which also makes validation,
|
|
125
|
+
* planning and rendering measure text.
|
|
126
|
+
*/
|
|
127
|
+
export function textMeasurer(template, fonts = template.fonts) {
|
|
128
|
+
if (!fonts)
|
|
129
|
+
throw new Error('textMeasurer needs font metrics: pass them, or load the template with fonts');
|
|
130
|
+
const metrics = fonts;
|
|
131
|
+
const { manifest, pkg } = template;
|
|
132
|
+
const { styles, info, shapes } = sharedFor(pkg);
|
|
133
|
+
const missing = new Map();
|
|
134
|
+
const shapeOf = (slide, id) => {
|
|
135
|
+
let byId = shapes.get(slide);
|
|
136
|
+
if (!byId) {
|
|
137
|
+
byId = new Map((info.slides[slide - 1]?.shapes ?? []).map((shape) => [shape.id, shape]));
|
|
138
|
+
shapes.set(slide, byId);
|
|
139
|
+
}
|
|
140
|
+
// A copied item's shapes exist only once the row is rendered; they are set like the shape they copy.
|
|
141
|
+
return byId.get(id) ?? byId.get(String(Number(id) % COPY_ID_STEP));
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* The frame a slot's text is measured in. This is the one place that decides the box: every
|
|
145
|
+
* check (validation, shrinking, the design checks) comes through here. `items` is the number
|
|
146
|
+
* of items on the slide: a row with `widen` makes its items wider when it holds fewer of them,
|
|
147
|
+
* and the text of such an item is measured in the box the renderer gives it.
|
|
148
|
+
*/
|
|
149
|
+
const frameOf = (pattern, slot, items) => {
|
|
150
|
+
const shape = shapeOf(pattern.sourceSlide, slot.shapeId);
|
|
151
|
+
if (!shape)
|
|
152
|
+
return undefined;
|
|
153
|
+
// The shape's own size is read exactly from the slide; a slot that moves its box states another.
|
|
154
|
+
const box = slot.box;
|
|
155
|
+
const size = box && box.w > 0 && box.h > 0 ? { width: box.w * styles.slideWidth, height: box.h * styles.slideHeight } : undefined;
|
|
156
|
+
const repeat = pattern.repeat;
|
|
157
|
+
if (!size && repeat && items !== undefined) {
|
|
158
|
+
const count = itemCount(pattern, items);
|
|
159
|
+
const texts = rowTextIds(pattern);
|
|
160
|
+
if (texts?.has(slot.shapeId) && widenFactor(repeat, count) > 1) {
|
|
161
|
+
const widened = styles.rowFrame(pattern.sourceSlide, slot.shapeId, slot.styleRun ?? 'first', { key: pattern.id, repeat, count, texts });
|
|
162
|
+
if (widened)
|
|
163
|
+
return widened;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return styles.frame(pattern.sourceSlide, shape.id, slot.styleRun ?? 'first', size);
|
|
167
|
+
};
|
|
168
|
+
/** Lays text out in a frame. `size` gives the font size of a paragraph style; `gap` overrides the space after paragraphs. */
|
|
169
|
+
const lay = (frame, paragraphs, size, gap, flags, spare = 0) => {
|
|
170
|
+
const inner = (frame.vertical ? frame.height - frame.insets.top - frame.insets.bottom : frame.width - frame.insets.left - frame.insets.right) / frame.columns - spare;
|
|
171
|
+
const laid = { lines: [], widths: [], lineRoom: [], height: 0, gaps: 0, lastPitch: 0, widest: 0, firstSize: 0 };
|
|
172
|
+
paragraphs.forEach((text, index) => {
|
|
173
|
+
const style = frame.paragraphs[Math.min(index, frame.paragraphs.length - 1)];
|
|
174
|
+
const points = size(style) * frame.fontScale;
|
|
175
|
+
if (index === 0)
|
|
176
|
+
laid.firstSize = points;
|
|
177
|
+
// The renderer marks a paragraph that starts with a right-to-left letter with such a language.
|
|
178
|
+
const direction = detectDirection(text);
|
|
179
|
+
const measure = runMeasure(metrics, style.run, points, direction === 'rtl' || RTL_LANGUAGE.test(style.run.lang), flags);
|
|
180
|
+
const rest = inner - style.marginStart - style.marginEnd;
|
|
181
|
+
const first = rest - (style.bullet && style.indent < 0 ? 0 : style.indent);
|
|
182
|
+
// The space after a line hangs outside it, unless it sits inside text that runs against the paragraph.
|
|
183
|
+
// A space between two words of one direction runs with them; between words of different
|
|
184
|
+
// directions it runs with the paragraph.
|
|
185
|
+
const against = (line, next) => {
|
|
186
|
+
const last = LAST_LETTER.exec(line)?.[1];
|
|
187
|
+
if (last === undefined || direction === undefined)
|
|
188
|
+
return false;
|
|
189
|
+
const before = detectDirection(last);
|
|
190
|
+
return before !== direction && (detectDirection(next) ?? before) === before;
|
|
191
|
+
};
|
|
192
|
+
const lines = frame.wrap ? breakLines(text, Math.max(first, 1), Math.max(rest, 1), measure, against) : [text];
|
|
193
|
+
laid.lines.push(lines);
|
|
194
|
+
laid.lineRoom.push(Math.round(rest * 100) / 100);
|
|
195
|
+
const widths = lines.map((line) => measure(line.trimEnd()));
|
|
196
|
+
laid.widths.push(widths.map((width) => Math.round(width * 100) / 100));
|
|
197
|
+
for (const width of widths)
|
|
198
|
+
laid.widest = Math.max(laid.widest, width / Math.max(rest, 1));
|
|
199
|
+
const pitch = 'points' in style.lineSpacing ? style.lineSpacing.points : LINE * points * Math.max(0.1, style.lineSpacing.percent - frame.lineReduction);
|
|
200
|
+
laid.height += lines.length * pitch;
|
|
201
|
+
laid.lastPitch = pitch;
|
|
202
|
+
// The space before the first paragraph and after the last is not drawn.
|
|
203
|
+
const between = (index > 0 ? spacing(style.before, points) : 0) + (index < paragraphs.length - 1 ? (gap ?? spacing(style.after, points)) : 0);
|
|
204
|
+
laid.height += between;
|
|
205
|
+
laid.gaps += between;
|
|
206
|
+
});
|
|
207
|
+
return laid;
|
|
208
|
+
};
|
|
209
|
+
const note = (error) => {
|
|
210
|
+
if (!(error instanceof MissingFont))
|
|
211
|
+
throw error;
|
|
212
|
+
missing.set(`${error.font.typeface}|${error.font.bold}|${error.font.italic}`, error.font);
|
|
213
|
+
return undefined;
|
|
214
|
+
};
|
|
215
|
+
const fit = (patternRef, slotRef, text, options = {}) => {
|
|
216
|
+
const pattern = typeof patternRef === 'string' ? manifest.patterns.find((entry) => entry.id === patternRef) : patternRef;
|
|
217
|
+
if (!pattern)
|
|
218
|
+
return undefined;
|
|
219
|
+
let slot;
|
|
220
|
+
if (typeof slotRef === 'string') {
|
|
221
|
+
// A slot of a copied item is not in the pattern's own list, so the widest row is searched.
|
|
222
|
+
const found = effectiveSlots(pattern, pattern.repeat?.max).find((entry) => entry.name === slotRef) ?? pattern.slots.find((entry) => entry.name === slotRef);
|
|
223
|
+
if (found?.kind === 'text')
|
|
224
|
+
slot = found;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
slot = slotRef;
|
|
228
|
+
}
|
|
229
|
+
if (!slot)
|
|
230
|
+
return undefined;
|
|
231
|
+
const frame = frameOf(pattern, slot, options.items);
|
|
232
|
+
if (!frame)
|
|
233
|
+
return undefined;
|
|
234
|
+
// The box as the template draws it: the same frame unless the row widened this item.
|
|
235
|
+
const drawn = options.items === undefined ? frame : (frameOf(pattern, slot) ?? frame);
|
|
236
|
+
const paragraphs = (Array.isArray(text) ? text : text.split(/\r?\n/)).map((paragraph) => paragraph.trim()).filter((paragraph) => paragraph !== '');
|
|
237
|
+
if (paragraphs.length === 0)
|
|
238
|
+
return undefined;
|
|
239
|
+
const chosen = slot;
|
|
240
|
+
const flags = { approximate: frame.approximate };
|
|
241
|
+
// The size a paragraph is rendered at, mirroring the renderer: a deck's text size applies to
|
|
242
|
+
// body text, and only a run that states its size, or a slot that sets one, is rescaled.
|
|
243
|
+
const bodyScale = scalesWithText(chosen) ? (options.textScale ?? 1) : 1;
|
|
244
|
+
const sizeAt = (shrink) => (style) => {
|
|
245
|
+
const stated = chosen.fontSize ?? style.run.size;
|
|
246
|
+
const factor = chosen.fontSize !== undefined || style.run.ownSize || shrink !== 1 ? bodyScale * shrink : 1;
|
|
247
|
+
return factor !== 1 || chosen.fontSize !== undefined ? Math.round((stated * 100 * factor) / 50) / 2 : stated;
|
|
248
|
+
};
|
|
249
|
+
try {
|
|
250
|
+
const boxHeight = (frame.vertical ? frame.width - frame.insets.left - frame.insets.right : frame.height - frame.insets.top - frame.insets.bottom) * frame.columns;
|
|
251
|
+
let room = Math.max(0, boxHeight);
|
|
252
|
+
let roomWidth = 1;
|
|
253
|
+
let sampleLines = 0;
|
|
254
|
+
// A box drawn around its sample text holds at least what the sample takes. That does not
|
|
255
|
+
// carry over to a slot that resizes the box or resets the size: it uses the shape
|
|
256
|
+
// differently. A box that only moves the shape, as a mirrored pattern's does, keeps it.
|
|
257
|
+
const own = shapeOf(pattern.sourceSlide, chosen.shapeId)?.box;
|
|
258
|
+
const sameSize = chosen.box !== undefined && own !== undefined && Math.abs(chosen.box.w - own.w) <= MOVED && Math.abs(chosen.box.h - own.h) <= MOVED;
|
|
259
|
+
if ((chosen.box === undefined || sameSize) && chosen.fontSize === undefined) {
|
|
260
|
+
const sample = frame.sample.map((paragraph) => paragraph.trim()).filter((paragraph) => paragraph !== '');
|
|
261
|
+
if (sample.length > 0) {
|
|
262
|
+
try {
|
|
263
|
+
// An item that widens keeps its height, so the room below the first line, and how
|
|
264
|
+
// full the designer drew the box, are those of the box as drawn.
|
|
265
|
+
const laid = lay(drawn, sample, (style) => style.run.size, undefined, { approximate: false });
|
|
266
|
+
room = Math.max(room, laid.height);
|
|
267
|
+
sampleLines = laid.lines.reduce((sum, entry) => sum + entry.length, 0);
|
|
268
|
+
const wide = frame === drawn ? laid : lay(frame, sample, (style) => style.run.size, undefined, { approximate: false });
|
|
269
|
+
roomWidth = Math.max(roomWidth, wide.widest);
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
note(error);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const measured = (shrink) => {
|
|
277
|
+
// A size chosen because the text just fits at it sits on the edge by construction, so a
|
|
278
|
+
// shrunk text is asked to fit with a little width to spare.
|
|
279
|
+
const laid = lay(frame, paragraphs, sizeAt(shrink), chosen.paragraphGap, flags, shrink === 1 ? 0 : SPARE);
|
|
280
|
+
const lines = laid.lines.reduce((sum, entry) => sum + entry.length, 0);
|
|
281
|
+
const slack = SLACK * laid.lastPitch;
|
|
282
|
+
const over = laid.height - room - slack;
|
|
283
|
+
const overflow = over > 0 ? Math.max(1, Math.ceil(over / laid.lastPitch - 1e-6)) : 0;
|
|
284
|
+
const spare = over > 0 ? 0 : Math.floor(-over / laid.lastPitch + 1e-6);
|
|
285
|
+
const widthRatio = frame.wrap ? 1 : laid.widest / roomWidth;
|
|
286
|
+
const chars = paragraphs.reduce((sum, paragraph) => sum + paragraph.length, 0);
|
|
287
|
+
const textHeight = Math.max(1, laid.height - laid.gaps);
|
|
288
|
+
const share = frame.wrap ? Math.max(0, room - laid.gaps) / textHeight : 1 / Math.max(widthRatio, 0.01);
|
|
289
|
+
return {
|
|
290
|
+
fits: overflow === 0 && widthRatio <= 1.005,
|
|
291
|
+
lines,
|
|
292
|
+
capacity: Math.max(0, lines - overflow + spare),
|
|
293
|
+
overflow,
|
|
294
|
+
height: Math.round(laid.height * 100) / 100,
|
|
295
|
+
room: Math.round(room * 100) / 100,
|
|
296
|
+
widthRatio: Math.round(widthRatio * 1000) / 1000,
|
|
297
|
+
size: laid.firstSize,
|
|
298
|
+
sizes: paragraphs.map((_, index) => sizeAt(shrink)(frame.paragraphs[Math.min(index, frame.paragraphs.length - 1)])),
|
|
299
|
+
scale: shrink,
|
|
300
|
+
// Lines end at word boundaries, so a text cut to the box's share of its height needs a little margin.
|
|
301
|
+
fitsChars: Math.max(1, Math.floor(chars * share * (share < 1 ? 0.95 : 1))),
|
|
302
|
+
paragraphs: laid.lines,
|
|
303
|
+
widths: laid.widths,
|
|
304
|
+
lineRoom: laid.lineRoom,
|
|
305
|
+
sampleLines,
|
|
306
|
+
approximate: flags.approximate,
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
let result = measured(1);
|
|
310
|
+
if (!result.fits && options.shrink) {
|
|
311
|
+
const first = sizeAt(1)(frame.paragraphs[0]);
|
|
312
|
+
const floor = Math.max(options.shrink.to, options.shrink.minSize !== undefined && first > 0 ? options.shrink.minSize / first : 0);
|
|
313
|
+
// Sizes go in half points, so the scale steps down by half a point of the first paragraph at a time.
|
|
314
|
+
for (let size = Math.floor(first * 2 - 1) / 2; size > 0 && size / first >= floor - 1e-9; size -= 0.5) {
|
|
315
|
+
result = measured(size / first);
|
|
316
|
+
if (result.fits)
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
return note(error);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
return {
|
|
327
|
+
fit,
|
|
328
|
+
missingFonts() {
|
|
329
|
+
const wanted = new Map(missing);
|
|
330
|
+
for (const font of templateFonts(template)) {
|
|
331
|
+
if (!metrics.face(font.typeface, font))
|
|
332
|
+
wanted.set(`${font.typeface}|${font.bold}|${font.italic}`, font);
|
|
333
|
+
}
|
|
334
|
+
return [...wanted.values()].sort((a, b) => a.typeface.localeCompare(b.typeface) || Number(a.bold) - Number(b.bold) || Number(a.italic) - Number(b.italic));
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/** The measurer of a template that was loaded with fonts, or `undefined` for one that was not. */
|
|
339
|
+
export function measurerFor(template) {
|
|
340
|
+
return template.fonts ? textMeasurer(template, template.fonts) : undefined;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* The typefaces a template's text slots are set in, each with the weights and slants used: the
|
|
344
|
+
* font files to supply for measuring. `language` adds the typeface that text of that language is
|
|
345
|
+
* set in when it differs from the Latin one (Hebrew in a template whose theme sends it to
|
|
346
|
+
* another font); it defaults to the manifest's language.
|
|
347
|
+
*/
|
|
348
|
+
export function templateFonts(template, language = template.manifest.language) {
|
|
349
|
+
const { styles } = sharedFor(template.pkg);
|
|
350
|
+
const script = LANGUAGE_SCRIPTS[(language ?? '').toLowerCase().split('-')[0] ?? ''];
|
|
351
|
+
const found = new Map();
|
|
352
|
+
const add = (typeface, run) => {
|
|
353
|
+
if (typeface)
|
|
354
|
+
found.set(`${typeface}|${run.bold}|${run.italic}`, { typeface, bold: run.bold, italic: run.italic });
|
|
355
|
+
};
|
|
356
|
+
for (const pattern of template.manifest.patterns) {
|
|
357
|
+
for (const slot of pattern.slots) {
|
|
358
|
+
if (slot.kind !== 'text' || slot.empty === 'drop' || slot.empty === 'keep')
|
|
359
|
+
continue;
|
|
360
|
+
const frame = styles.frame(pattern.sourceSlide, slot.shapeId, slot.styleRun ?? 'first');
|
|
361
|
+
for (const paragraph of frame?.paragraphs ?? []) {
|
|
362
|
+
add(paragraph.run.latin.name, paragraph.run);
|
|
363
|
+
add(paragraph.run.cs.name || (script ? (paragraph.run.cs.scripts?.get(script) ?? '') : ''), paragraph.run);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return [...found.values()].sort((a, b) => a.typeface.localeCompare(b.typeface) || Number(a.bold) - Number(b.bold) || Number(a.italic) - Number(b.italic));
|
|
368
|
+
}
|
|
369
|
+
//# sourceMappingURL=measure.js.map
|