@hajimism/dek 0.2.1 → 0.3.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/CHANGELOG.md +46 -0
- package/README.ja.md +3 -1
- package/README.md +3 -1
- package/package.json +4 -1
- package/skills/dek/SKILL.md +44 -0
- package/src/cli/annotations.ts +26 -0
- package/src/cli/commands.ts +66 -0
- package/src/cli/contract.ts +75 -4
- package/src/cli/env-file.ts +78 -0
- package/src/cli/files.ts +3 -3
- package/src/cli/flags.ts +1 -1
- package/src/cli/format.ts +3 -0
- package/src/cli/pptx.ts +20 -0
- package/src/cli/show.ts +17 -2
- package/src/cli/text.ts +43 -0
- package/src/cli/theme.ts +7 -1
- package/src/cli.ts +5 -1
- package/src/core/annotation-rows.ts +176 -0
- package/src/core/annotations.ts +362 -0
- package/src/core/diagnostic.ts +1 -0
- package/src/core/github.ts +27 -24
- package/src/core/html-scan.ts +54 -0
- package/src/core/html.ts +53 -8
- package/src/core/lint/slide-html.ts +92 -2
- package/src/core/lint.ts +8 -1
- package/src/core/live-protocol.ts +4 -1
- package/src/core/page.ts +6 -0
- package/src/core/playwright-module.d.ts +7 -3
- package/src/core/playwright-visual.ts +76 -1
- package/src/core/playwright.ts +65 -2
- package/src/core/pptx-package.ts +312 -0
- package/src/core/pptx-text.ts +363 -0
- package/src/core/pptx.ts +75 -0
- package/src/core/slide-measure.ts +1 -0
- package/src/core/slide-script-eval.ts +2 -1
- package/src/core/spawn.ts +8 -1
- package/src/core/sync.ts +33 -6
- package/src/core/theme-excerpt.ts +61 -9
- package/src/core/theme-facts.ts +12 -2
- package/src/core/zip.ts +77 -0
- package/src/runtime/annotate-pick.ts +2 -6
- package/src/runtime/annotate-view.ts +115 -101
- package/src/runtime/annotate.ts +5 -1
- package/src/runtime/browser.ts +5 -1
- package/src/runtime/live.ts +1 -0
- package/src/runtime/routes.ts +3 -0
- package/src/server/auth.ts +3 -1
- package/src/server/routes.ts +68 -1
- package/src/video/worker.ts +2 -0
- package/tsconfig.json +26 -0
- package/src/runtime/annotate-notes.ts +0 -207
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { zipStored } from "./zip.ts";
|
|
2
|
+
|
|
3
|
+
// A PowerPoint file as dek writes it: the fewest OOXML parts PowerPoint opens without a repair,
|
|
4
|
+
// as strings. Each slide is a picture of what the browser drew, with the text taken out of it,
|
|
5
|
+
// and that text laid back over it in text boxes, line by line, where the browser set it.
|
|
6
|
+
|
|
7
|
+
/** One run of text on one line, as the browser drew it. Sizes are in the slide's own pixels. */
|
|
8
|
+
export type PptxRun = {
|
|
9
|
+
text: string;
|
|
10
|
+
size: number;
|
|
11
|
+
bold: boolean;
|
|
12
|
+
italic: boolean;
|
|
13
|
+
underline: boolean;
|
|
14
|
+
strike: boolean;
|
|
15
|
+
color: [number, number, number];
|
|
16
|
+
/** 0..1: the text's own alpha times the opacity of everything it is in. */
|
|
17
|
+
alpha: number;
|
|
18
|
+
letterSpacing: number;
|
|
19
|
+
/** The font the browser drew it with; PowerPoint's default when none is known. */
|
|
20
|
+
font?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** One line of text, or the part of one that sits together, at its box on the slide. */
|
|
24
|
+
export type PptxTextBox = {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
/** The distance from one line to the next, which places the line in its box. */
|
|
30
|
+
lineHeight: number;
|
|
31
|
+
runs: PptxRun[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type PptxSlide = {
|
|
35
|
+
/** The slide drawn without the text the boxes carry, as a PNG. */
|
|
36
|
+
picture: Uint8Array;
|
|
37
|
+
/** What the picture shows, for a screen reader: the slide's own alternative texts. */
|
|
38
|
+
description: string;
|
|
39
|
+
texts: PptxTextBox[];
|
|
40
|
+
/** The script for this slide, a paragraph a line. */
|
|
41
|
+
notes: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type PptxDeck = {
|
|
45
|
+
title: string;
|
|
46
|
+
lang: string;
|
|
47
|
+
/** The slide's logical size in pixels; one pixel is 1/96 inch, as CSS has it. */
|
|
48
|
+
size: { width: number; height: number };
|
|
49
|
+
slides: PptxSlide[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** English Metric Units in one CSS pixel: 914400 to the inch, 96 pixels to the inch. */
|
|
53
|
+
const EMU_PER_PX = 9525;
|
|
54
|
+
/** Points in one CSS pixel. */
|
|
55
|
+
const PT_PER_PX = 0.75;
|
|
56
|
+
|
|
57
|
+
const NS = {
|
|
58
|
+
a: "http://schemas.openxmlformats.org/drawingml/2006/main",
|
|
59
|
+
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
|
60
|
+
p: "http://schemas.openxmlformats.org/presentationml/2006/main",
|
|
61
|
+
rel: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
|
62
|
+
pkg: "http://schemas.openxmlformats.org/package/2006/relationships",
|
|
63
|
+
};
|
|
64
|
+
const PML = `xmlns:a="${NS.a}" xmlns:r="${NS.r}" xmlns:p="${NS.p}"`;
|
|
65
|
+
const DECL = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
|
|
66
|
+
const CT = "application/vnd.openxmlformats-officedocument";
|
|
67
|
+
|
|
68
|
+
/** The whole `.pptx`, ready to write. */
|
|
69
|
+
export function pptxPackage(deck: PptxDeck): Uint8Array {
|
|
70
|
+
const encoder = new TextEncoder();
|
|
71
|
+
const parts: Array<{ path: string; data: Uint8Array }> = [];
|
|
72
|
+
const add = (path: string, xml: string) => parts.push({ path, data: encoder.encode(xml) });
|
|
73
|
+
const count = deck.slides.length;
|
|
74
|
+
const numbers = deck.slides.map((_, index) => index + 1);
|
|
75
|
+
|
|
76
|
+
add("[Content_Types].xml", contentTypes(numbers));
|
|
77
|
+
add(
|
|
78
|
+
"_rels/.rels",
|
|
79
|
+
relationships([
|
|
80
|
+
[`${NS.rel}/officeDocument`, "ppt/presentation.xml"],
|
|
81
|
+
[`${NS.pkg}/metadata/core-properties`, "docProps/core.xml"],
|
|
82
|
+
[`${NS.rel}/extended-properties`, "docProps/app.xml"],
|
|
83
|
+
]),
|
|
84
|
+
);
|
|
85
|
+
add("docProps/core.xml", coreProperties(deck));
|
|
86
|
+
add("docProps/app.xml", appProperties(count));
|
|
87
|
+
add("ppt/presentation.xml", presentation(deck, numbers));
|
|
88
|
+
add(
|
|
89
|
+
"ppt/_rels/presentation.xml.rels",
|
|
90
|
+
relationships([
|
|
91
|
+
[`${NS.rel}/slideMaster`, "slideMasters/slideMaster1.xml"],
|
|
92
|
+
[`${NS.rel}/notesMaster`, "notesMasters/notesMaster1.xml"],
|
|
93
|
+
[`${NS.rel}/theme`, "theme/theme1.xml"],
|
|
94
|
+
[`${NS.rel}/presProps`, "presProps.xml"],
|
|
95
|
+
[`${NS.rel}/viewProps`, "viewProps.xml"],
|
|
96
|
+
[`${NS.rel}/tableStyles`, "tableStyles.xml"],
|
|
97
|
+
...numbers.map((n): [string, string] => [`${NS.rel}/slide`, `slides/slide${n}.xml`]),
|
|
98
|
+
]),
|
|
99
|
+
);
|
|
100
|
+
add("ppt/presProps.xml", `${DECL}<p:presentationPr ${PML}/>`);
|
|
101
|
+
add(
|
|
102
|
+
"ppt/viewProps.xml",
|
|
103
|
+
`${DECL}<p:viewPr ${PML}><p:normalViewPr><p:restoredLeft sz="15620"/><p:restoredTop sz="75000"/></p:normalViewPr><p:gridSpacing cx="76200" cy="76200"/></p:viewPr>`,
|
|
104
|
+
);
|
|
105
|
+
add(
|
|
106
|
+
"ppt/tableStyles.xml",
|
|
107
|
+
`${DECL}<a:tblStyleLst xmlns:a="${NS.a}" def="{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"/>`,
|
|
108
|
+
);
|
|
109
|
+
add("ppt/theme/theme1.xml", theme("dek"));
|
|
110
|
+
add("ppt/theme/theme2.xml", theme("dek notes"));
|
|
111
|
+
add("ppt/slideMasters/slideMaster1.xml", slideMaster());
|
|
112
|
+
add(
|
|
113
|
+
"ppt/slideMasters/_rels/slideMaster1.xml.rels",
|
|
114
|
+
relationships([
|
|
115
|
+
[`${NS.rel}/slideLayout`, "../slideLayouts/slideLayout1.xml"],
|
|
116
|
+
[`${NS.rel}/theme`, "../theme/theme1.xml"],
|
|
117
|
+
]),
|
|
118
|
+
);
|
|
119
|
+
add("ppt/slideLayouts/slideLayout1.xml", slideLayout());
|
|
120
|
+
add(
|
|
121
|
+
"ppt/slideLayouts/_rels/slideLayout1.xml.rels",
|
|
122
|
+
relationships([[`${NS.rel}/slideMaster`, "../slideMasters/slideMaster1.xml"]]),
|
|
123
|
+
);
|
|
124
|
+
add("ppt/notesMasters/notesMaster1.xml", notesMaster());
|
|
125
|
+
add(
|
|
126
|
+
"ppt/notesMasters/_rels/notesMaster1.xml.rels",
|
|
127
|
+
relationships([[`${NS.rel}/theme`, "../theme/theme2.xml"]]),
|
|
128
|
+
);
|
|
129
|
+
deck.slides.forEach((slide, index) => {
|
|
130
|
+
const n = index + 1;
|
|
131
|
+
add(`ppt/slides/slide${n}.xml`, slideXml(slide, deck));
|
|
132
|
+
add(
|
|
133
|
+
`ppt/slides/_rels/slide${n}.xml.rels`,
|
|
134
|
+
relationships([
|
|
135
|
+
[`${NS.rel}/slideLayout`, "../slideLayouts/slideLayout1.xml"],
|
|
136
|
+
[`${NS.rel}/image`, `../media/image${n}.png`],
|
|
137
|
+
[`${NS.rel}/notesSlide`, `../notesSlides/notesSlide${n}.xml`],
|
|
138
|
+
]),
|
|
139
|
+
);
|
|
140
|
+
parts.push({ path: `ppt/media/image${n}.png`, data: slide.picture });
|
|
141
|
+
add(`ppt/notesSlides/notesSlide${n}.xml`, notesSlide(slide.notes, deck.lang));
|
|
142
|
+
add(
|
|
143
|
+
`ppt/notesSlides/_rels/notesSlide${n}.xml.rels`,
|
|
144
|
+
relationships([
|
|
145
|
+
[`${NS.rel}/notesMaster`, "../notesMasters/notesMaster1.xml"],
|
|
146
|
+
[`${NS.rel}/slide`, `../slides/slide${n}.xml`],
|
|
147
|
+
]),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
return zipStored(parts);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function contentTypes(numbers: number[]): string {
|
|
154
|
+
const override = (part: string, type: string) =>
|
|
155
|
+
`<Override PartName="/${part}" ContentType="${type}"/>`;
|
|
156
|
+
return `${DECL}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Default Extension="png" ContentType="image/png"/>${[
|
|
157
|
+
override("ppt/presentation.xml", `${CT}.presentationml.presentation.main+xml`),
|
|
158
|
+
override("ppt/presProps.xml", `${CT}.presentationml.presProps+xml`),
|
|
159
|
+
override("ppt/viewProps.xml", `${CT}.presentationml.viewProps+xml`),
|
|
160
|
+
override("ppt/tableStyles.xml", `${CT}.presentationml.tableStyles+xml`),
|
|
161
|
+
override("ppt/theme/theme1.xml", `${CT}.theme+xml`),
|
|
162
|
+
override("ppt/theme/theme2.xml", `${CT}.theme+xml`),
|
|
163
|
+
override("ppt/slideMasters/slideMaster1.xml", `${CT}.presentationml.slideMaster+xml`),
|
|
164
|
+
override("ppt/slideLayouts/slideLayout1.xml", `${CT}.presentationml.slideLayout+xml`),
|
|
165
|
+
override("ppt/notesMasters/notesMaster1.xml", `${CT}.presentationml.notesMaster+xml`),
|
|
166
|
+
...numbers.flatMap((n) => [
|
|
167
|
+
override(`ppt/slides/slide${n}.xml`, `${CT}.presentationml.slide+xml`),
|
|
168
|
+
override(`ppt/notesSlides/notesSlide${n}.xml`, `${CT}.presentationml.notesSlide+xml`),
|
|
169
|
+
]),
|
|
170
|
+
override("docProps/core.xml", "application/vnd.openxmlformats-package.core-properties+xml"),
|
|
171
|
+
override("docProps/app.xml", `${CT}.extended-properties+xml`),
|
|
172
|
+
].join("")}</Types>`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A part's relationships, numbered `rId1` on in the order given. */
|
|
176
|
+
function relationships(targets: Array<[type: string, target: string]>): string {
|
|
177
|
+
return `${DECL}<Relationships xmlns="${NS.pkg}">${targets
|
|
178
|
+
.map(
|
|
179
|
+
([type, target], index) =>
|
|
180
|
+
`<Relationship Id="rId${index + 1}" Type="${type}" Target="${escapeXml(target)}"/>`,
|
|
181
|
+
)
|
|
182
|
+
.join("")}</Relationships>`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function coreProperties(deck: PptxDeck): string {
|
|
186
|
+
return `${DECL}<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title>${escapeXml(deck.title)}</dc:title><dc:language>${escapeXml(deck.lang)}</dc:language></cp:coreProperties>`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function appProperties(count: number): string {
|
|
190
|
+
return `${DECL}<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"><Application>dek</Application><Slides>${count}</Slides><Notes>${count}</Notes></Properties>`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function presentation(deck: PptxDeck, numbers: number[]): string {
|
|
194
|
+
// The relationships list the master, notes master, theme, and three properties first.
|
|
195
|
+
const slides = numbers.map((n) => `<p:sldId id="${255 + n}" r:id="rId${6 + n}"/>`).join("");
|
|
196
|
+
return `${DECL}<p:presentation ${PML} saveSubsetFonts="1"><p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst><p:notesMasterIdLst><p:notesMasterId r:id="rId2"/></p:notesMasterIdLst><p:sldIdLst>${slides}</p:sldIdLst><p:sldSz cx="${emu(deck.size.width)}" cy="${emu(deck.size.height)}"/><p:notesSz cx="6858000" cy="9144000"/></p:presentation>`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** An empty group: the shape tree every slide, layout, and master starts from. */
|
|
200
|
+
const GROUP =
|
|
201
|
+
'<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>';
|
|
202
|
+
const CLR_MAP =
|
|
203
|
+
'<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>';
|
|
204
|
+
const LEVEL =
|
|
205
|
+
'<a:lvl1pPr><a:defRPr sz="1800"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl1pPr>';
|
|
206
|
+
|
|
207
|
+
function slideMaster(): string {
|
|
208
|
+
return `${DECL}<p:sldMaster ${PML}><p:cSld><p:bg><p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef></p:bg><p:spTree>${GROUP}</p:spTree></p:cSld>${CLR_MAP}<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst><p:txStyles><p:titleStyle>${LEVEL}</p:titleStyle><p:bodyStyle>${LEVEL}</p:bodyStyle><p:otherStyle>${LEVEL}</p:otherStyle></p:txStyles></p:sldMaster>`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function slideLayout(): string {
|
|
212
|
+
return `${DECL}<p:sldLayout ${PML} type="blank" preserve="1"><p:cSld name="Blank"><p:spTree>${GROUP}</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function notesMaster(): string {
|
|
216
|
+
const image = `<p:sp><p:nvSpPr><p:cNvPr id="2" name="Slide Image"/><p:cNvSpPr><a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldImg" idx="2"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="381000" y="685800"/><a:ext cx="6096000" cy="3429000"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/><a:ln w="12700"><a:solidFill><a:prstClr val="black"/></a:solidFill></a:ln></p:spPr></p:sp>`;
|
|
217
|
+
const body = `<p:sp><p:nvSpPr><p:cNvPr id="3" name="Notes"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="body" sz="quarter" idx="3"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="685800" y="4343400"/><a:ext cx="5486400" cy="4114800"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr><p:txBody><a:bodyPr vert="horz" lIns="91440" tIns="45720" rIns="91440" bIns="45720" rtlCol="0"/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody></p:sp>`;
|
|
218
|
+
return `${DECL}<p:notesMaster ${PML}><p:cSld><p:bg><p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef></p:bg><p:spTree>${GROUP}${image}${body}</p:spTree></p:cSld>${CLR_MAP}<p:notesStyle><a:lvl1pPr marL="0" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1"><a:defRPr sz="1200" kern="1200"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/><a:cs typeface="+mn-cs"/></a:defRPr></a:lvl1pPr></p:notesStyle></p:notesMaster>`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function notesSlide(notes: string, lang: string): string {
|
|
222
|
+
const paragraphs = notes
|
|
223
|
+
.split(/\r?\n/)
|
|
224
|
+
.map((line) =>
|
|
225
|
+
line === ""
|
|
226
|
+
? `<a:p><a:endParaRPr lang="${escapeXml(lang)}" dirty="0"/></a:p>`
|
|
227
|
+
: `<a:p><a:r><a:rPr lang="${escapeXml(lang)}" dirty="0"/><a:t>${escapeXml(line)}</a:t></a:r></a:p>`,
|
|
228
|
+
)
|
|
229
|
+
.join("");
|
|
230
|
+
return `${DECL}<p:notes ${PML}><p:cSld><p:spTree>${GROUP}<p:sp><p:nvSpPr><p:cNvPr id="2" name="Slide Image"/><p:cNvSpPr><a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldImg"/></p:nvPr></p:nvSpPr><p:spPr/></p:sp><p:sp><p:nvSpPr><p:cNvPr id="3" name="Notes"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="body" idx="1"/></p:nvPr></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/>${paragraphs || `<a:p><a:endParaRPr lang="${escapeXml(lang)}" dirty="0"/></a:p>`}</p:txBody></p:sp></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:notes>`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function slideXml(slide: PptxSlide, deck: PptxDeck): string {
|
|
234
|
+
const picture = `<p:pic><p:nvPicPr><p:cNvPr id="2" name="Slide" descr="${escapeXml(slide.description)}"/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="rId2"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${emu(deck.size.width)}" cy="${emu(deck.size.height)}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>`;
|
|
235
|
+
const boxes = slide.texts.map((box, index) => textBox(box, index + 3, deck.lang)).join("");
|
|
236
|
+
return `${DECL}<p:sld ${PML}><p:cSld><p:spTree>${GROUP}${picture}${boxes}</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* One line of text in a box of its own: no wrapping, so the line breaks where the browser broke
|
|
241
|
+
* it, and no insets, so the text starts where the browser started it. The box runs on past the
|
|
242
|
+
* line's end, since a renderer that wraps anyway, or a substitute font a little wider, would
|
|
243
|
+
* otherwise break the line again; the text is set from the left, so the room shows nowhere.
|
|
244
|
+
*/
|
|
245
|
+
function textBox(box: PptxTextBox, id: number, lang: string): string {
|
|
246
|
+
const runs = box.runs.map((run) => textRun(run, lang)).join("");
|
|
247
|
+
const spacing = Math.max(1, Math.round(box.lineHeight * PT_PER_PX * 100));
|
|
248
|
+
const room = Math.max(box.width * 0.25, box.lineHeight * 2);
|
|
249
|
+
return `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="Text ${id - 2}"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="${emu(box.x)}" y="${emu(box.y)}"/><a:ext cx="${emu(box.width + room)}" cy="${emu(box.height)}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/></p:spPr><p:txBody><a:bodyPr wrap="none" lIns="0" tIns="0" rIns="0" bIns="0" rtlCol="0" anchor="t"><a:noAutofit/></a:bodyPr><a:lstStyle/><a:p><a:pPr><a:lnSpc><a:spcPts val="${spacing}"/></a:lnSpc></a:pPr>${runs}</a:p></p:txBody></p:sp>`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function textRun(run: PptxRun, lang: string): string {
|
|
253
|
+
const attributes = [
|
|
254
|
+
`lang="${escapeXml(lang)}"`,
|
|
255
|
+
`sz="${Math.max(100, Math.round(run.size * PT_PER_PX * 100))}"`,
|
|
256
|
+
`b="${run.bold ? 1 : 0}"`,
|
|
257
|
+
`i="${run.italic ? 1 : 0}"`,
|
|
258
|
+
...(run.underline ? ['u="sng"'] : []),
|
|
259
|
+
...(run.strike ? ['strike="sngStrike"'] : []),
|
|
260
|
+
...(run.letterSpacing !== 0
|
|
261
|
+
? [`spc="${Math.round(run.letterSpacing * PT_PER_PX * 100)}"`]
|
|
262
|
+
: []),
|
|
263
|
+
'dirty="0"',
|
|
264
|
+
].join(" ");
|
|
265
|
+
const alpha = run.alpha < 1 ? `<a:alpha val="${Math.round(run.alpha * 100000)}"/>` : "";
|
|
266
|
+
const color = run.color.map((channel) => channel.toString(16).padStart(2, "0")).join("");
|
|
267
|
+
const font = run.font
|
|
268
|
+
? `<a:latin typeface="${escapeXml(run.font)}"/><a:ea typeface="${escapeXml(run.font)}"/><a:cs typeface="${escapeXml(run.font)}"/>`
|
|
269
|
+
: "";
|
|
270
|
+
return `<a:r><a:rPr ${attributes}><a:solidFill><a:srgbClr val="${color.toUpperCase()}">${alpha}</a:srgbClr></a:solidFill>${font}</a:rPr><a:t>${escapeXml(run.text)}</a:t></a:r>`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function emu(px: number): number {
|
|
274
|
+
return Math.round(px * EMU_PER_PX);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Text as XML holds it: markup escaped, and the characters XML 1.0 cannot hold left out. */
|
|
278
|
+
function escapeXml(text: string): string {
|
|
279
|
+
return [...text]
|
|
280
|
+
.filter((char) => xmlChar(char.codePointAt(0) ?? 0))
|
|
281
|
+
.join("")
|
|
282
|
+
.replace(/&/g, "&")
|
|
283
|
+
.replace(/</g, "<")
|
|
284
|
+
.replace(/>/g, ">")
|
|
285
|
+
.replace(/"/g, """);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Whether XML 1.0 holds the code point: tab, newline, return, and the rest of Unicode but surrogates. */
|
|
289
|
+
function xmlChar(code: number): boolean {
|
|
290
|
+
return (
|
|
291
|
+
code === 0x9 ||
|
|
292
|
+
code === 0xa ||
|
|
293
|
+
code === 0xd ||
|
|
294
|
+
(code >= 0x20 && code <= 0xd7ff) ||
|
|
295
|
+
(code >= 0xe000 && code <= 0xfffd) ||
|
|
296
|
+
code >= 0x10000
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* A complete theme: PowerPoint repairs a file whose theme leaves out the colors, the fonts, or
|
|
302
|
+
* the three lists of fill, line, and effect styles. Nothing in a slide uses it but the notes.
|
|
303
|
+
*/
|
|
304
|
+
function theme(name: string): string {
|
|
305
|
+
const solid = (value: string) => `<a:solidFill><a:schemeClr val="${value}"/></a:solidFill>`;
|
|
306
|
+
const fills = `${solid("phClr")}${solid("phClr")}${solid("phClr")}`;
|
|
307
|
+
const line = (width: number) =>
|
|
308
|
+
`<a:ln w="${width}" cap="flat" cmpd="sng" algn="ctr">${solid("phClr")}<a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>`;
|
|
309
|
+
const effect = "<a:effectStyle><a:effectLst/></a:effectStyle>";
|
|
310
|
+
const clr = (tag: string, value: string) => `<a:${tag}><a:srgbClr val="${value}"/></a:${tag}>`;
|
|
311
|
+
return `${DECL}<a:theme xmlns:a="${NS.a}" name="${escapeXml(name)}"><a:themeElements><a:clrScheme name="dek"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>${clr("dk2", "44546A")}${clr("lt2", "E7E6E6")}${clr("accent1", "4472C4")}${clr("accent2", "ED7D31")}${clr("accent3", "A5A5A5")}${clr("accent4", "FFC000")}${clr("accent5", "5B9BD5")}${clr("accent6", "70AD47")}${clr("hlink", "0563C1")}${clr("folHlink", "954F72")}</a:clrScheme><a:fontScheme name="dek"><a:majorFont><a:latin typeface="Calibri Light"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont><a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont></a:fontScheme><a:fmtScheme name="dek"><a:fillStyleLst>${fills}</a:fillStyleLst><a:lnStyleLst>${line(6350)}${line(12700)}${line(19050)}</a:lnStyleLst><a:effectStyleLst>${effect}${effect}${effect}</a:effectStyleLst><a:bgFillStyleLst>${fills}</a:bgFillStyleLst></a:fmtScheme></a:themeElements><a:objectDefaults/><a:extraClrSchemeLst/></a:theme>`;
|
|
312
|
+
}
|