web-doc 0.6.2 → 0.7.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/THIRD_PARTY_NOTICES.md +26 -1
- package/dist/adapters/docx-images.d.ts +13 -4
- package/dist/adapters/docx-images.js +3 -0
- package/dist/adapters/docx-paragraphs.d.ts +65 -0
- package/dist/adapters/docx-paragraphs.js +165 -0
- package/dist/adapters/docx-prepass.d.ts +17 -0
- package/dist/adapters/docx-prepass.js +230 -0
- package/dist/adapters/office.d.ts +31 -5
- package/dist/adapters/office.js +60 -29
- package/dist/adapters/pdf.d.ts +9 -0
- package/dist/adapters/pdf.js +10 -0
- package/dist/assets/pdfium/pdfium.wasm +0 -0
- package/dist/contracts.d.ts +47 -2
- package/dist/edit/assets.d.ts +23 -0
- package/dist/edit/assets.js +75 -0
- package/dist/edit/docx/elements.d.ts +6 -0
- package/dist/edit/docx/elements.js +90 -0
- package/dist/edit/docx/engine.d.ts +51 -0
- package/dist/edit/docx/engine.js +473 -0
- package/dist/edit/docx/handlers.d.ts +3 -0
- package/dist/edit/docx/handlers.js +15 -0
- package/dist/edit/docx/ids.d.ts +45 -0
- package/dist/edit/docx/ids.js +101 -0
- package/dist/edit/docx/model.d.ts +92 -0
- package/dist/edit/docx/model.js +339 -0
- package/dist/edit/docx/operations.d.ts +37 -0
- package/dist/edit/docx/operations.js +3 -0
- package/dist/edit/docx/provider.d.ts +13 -0
- package/dist/edit/docx/provider.js +34 -0
- package/dist/edit/docx/schemas.d.ts +6 -0
- package/dist/edit/docx/schemas.js +192 -0
- package/dist/edit/docx/session.d.ts +37 -0
- package/dist/edit/docx/session.js +426 -0
- package/dist/edit/docx/structure-ops.d.ts +6 -0
- package/dist/edit/docx/structure-ops.js +494 -0
- package/dist/edit/docx/style.d.ts +45 -0
- package/dist/edit/docx/style.js +375 -0
- package/dist/edit/docx/table-ops.d.ts +4 -0
- package/dist/edit/docx/table-ops.js +219 -0
- package/dist/edit/docx/text-ops.d.ts +18 -0
- package/dist/edit/docx/text-ops.js +458 -0
- package/dist/edit/docx/text.d.ts +34 -0
- package/dist/edit/docx/text.js +263 -0
- package/dist/edit/docx/types.d.ts +191 -0
- package/dist/edit/docx/types.js +1 -0
- package/dist/edit/docx/write.d.ts +50 -0
- package/dist/edit/docx/write.js +352 -0
- package/dist/edit/engine.d.ts +117 -0
- package/dist/edit/engine.js +1 -0
- package/dist/edit/history.d.ts +53 -0
- package/dist/edit/history.js +101 -0
- package/dist/edit/ooxml/names.d.ts +20 -0
- package/dist/edit/ooxml/names.js +61 -0
- package/dist/edit/ooxml/opc.d.ts +50 -0
- package/dist/edit/ooxml/opc.js +150 -0
- package/dist/edit/ooxml/package.d.ts +82 -0
- package/dist/edit/ooxml/package.js +233 -0
- package/dist/edit/ooxml/patch.d.ts +51 -0
- package/dist/edit/ooxml/patch.js +250 -0
- package/dist/edit/ooxml/transaction.d.ts +39 -0
- package/dist/edit/ooxml/transaction.js +316 -0
- package/dist/edit/ooxml/worker.d.ts +8 -0
- package/dist/edit/ooxml/worker.js +12 -0
- package/dist/edit/ooxml/writer.d.ts +21 -0
- package/dist/edit/ooxml/writer.js +187 -0
- package/dist/edit/ooxml/xml.d.ts +74 -0
- package/dist/edit/ooxml/xml.js +451 -0
- package/dist/edit/ooxml/zip.d.ts +54 -0
- package/dist/edit/ooxml/zip.js +280 -0
- package/dist/edit/operations.d.ts +19 -0
- package/dist/edit/operations.js +137 -0
- package/dist/edit/pdf/engine/compact.d.ts +6 -0
- package/dist/edit/pdf/engine/compact.js +442 -0
- package/dist/edit/pdf/engine/document.d.ts +95 -0
- package/dist/edit/pdf/engine/document.js +868 -0
- package/dist/edit/pdf/engine/elements.d.ts +45 -0
- package/dist/edit/pdf/engine/elements.js +313 -0
- package/dist/edit/pdf/engine/existing-text.d.ts +4 -0
- package/dist/edit/pdf/engine/existing-text.js +424 -0
- package/dist/edit/pdf/engine/fonts.d.ts +78 -0
- package/dist/edit/pdf/engine/fonts.js +466 -0
- package/dist/edit/pdf/engine/geometry.d.ts +36 -0
- package/dist/edit/pdf/engine/geometry.js +97 -0
- package/dist/edit/pdf/engine/handler.d.ts +21 -0
- package/dist/edit/pdf/engine/handler.js +108 -0
- package/dist/edit/pdf/engine/images.d.ts +33 -0
- package/dist/edit/pdf/engine/images.js +188 -0
- package/dist/edit/pdf/engine/layout.d.ts +23 -0
- package/dist/edit/pdf/engine/layout.js +177 -0
- package/dist/edit/pdf/engine/operations.d.ts +67 -0
- package/dist/edit/pdf/engine/operations.js +3 -0
- package/dist/edit/pdf/engine/pages.d.ts +6 -0
- package/dist/edit/pdf/engine/pages.js +98 -0
- package/dist/edit/pdf/engine/pdfium.d.ts +195 -0
- package/dist/edit/pdf/engine/pdfium.js +249 -0
- package/dist/edit/pdf/engine/shapes.d.ts +4 -0
- package/dist/edit/pdf/engine/shapes.js +183 -0
- package/dist/edit/pdf/engine/tables.d.ts +44 -0
- package/dist/edit/pdf/engine/tables.js +315 -0
- package/dist/edit/pdf/engine/text-box.d.ts +71 -0
- package/dist/edit/pdf/engine/text-box.js +317 -0
- package/dist/edit/pdf/engine/text-layout.d.ts +28 -0
- package/dist/edit/pdf/engine/text-layout.js +67 -0
- package/dist/edit/pdf/engine/transform.d.ts +5 -0
- package/dist/edit/pdf/engine/transform.js +137 -0
- package/dist/edit/pdf/provider.d.ts +35 -0
- package/dist/edit/pdf/provider.js +92 -0
- package/dist/edit/pdf/range-map.d.ts +14 -0
- package/dist/edit/pdf/range-map.js +107 -0
- package/dist/edit/pdf/schemas.d.ts +11 -0
- package/dist/edit/pdf/schemas.js +291 -0
- package/dist/edit/pdf/selection.d.ts +5 -0
- package/dist/edit/pdf/selection.js +152 -0
- package/dist/edit/pdf/session.d.ts +53 -0
- package/dist/edit/pdf/session.js +273 -0
- package/dist/edit/pdf/types.d.ts +336 -0
- package/dist/edit/pdf/types.js +1 -0
- package/dist/edit/pptx/elements.d.ts +74 -0
- package/dist/edit/pptx/elements.js +301 -0
- package/dist/edit/pptx/engine.d.ts +37 -0
- package/dist/edit/pptx/engine.js +466 -0
- package/dist/edit/pptx/geometry.d.ts +72 -0
- package/dist/edit/pptx/geometry.js +193 -0
- package/dist/edit/pptx/handler.d.ts +7 -0
- package/dist/edit/pptx/handler.js +91 -0
- package/dist/edit/pptx/handlers.d.ts +3 -0
- package/dist/edit/pptx/handlers.js +21 -0
- package/dist/edit/pptx/image-table-ops.d.ts +5 -0
- package/dist/edit/pptx/image-table-ops.js +233 -0
- package/dist/edit/pptx/model.d.ts +69 -0
- package/dist/edit/pptx/model.js +170 -0
- package/dist/edit/pptx/operations.d.ts +50 -0
- package/dist/edit/pptx/operations.js +11 -0
- package/dist/edit/pptx/provider.d.ts +17 -0
- package/dist/edit/pptx/provider.js +40 -0
- package/dist/edit/pptx/schemas.d.ts +6 -0
- package/dist/edit/pptx/schemas.js +220 -0
- package/dist/edit/pptx/session.d.ts +44 -0
- package/dist/edit/pptx/session.js +114 -0
- package/dist/edit/pptx/shape-ops.d.ts +10 -0
- package/dist/edit/pptx/shape-ops.js +486 -0
- package/dist/edit/pptx/slide-ops.d.ts +6 -0
- package/dist/edit/pptx/slide-ops.js +353 -0
- package/dist/edit/pptx/style.d.ts +31 -0
- package/dist/edit/pptx/style.js +181 -0
- package/dist/edit/pptx/text-ops.d.ts +13 -0
- package/dist/edit/pptx/text-ops.js +267 -0
- package/dist/edit/pptx/text-write.d.ts +67 -0
- package/dist/edit/pptx/text-write.js +293 -0
- package/dist/edit/pptx/text.d.ts +41 -0
- package/dist/edit/pptx/text.js +99 -0
- package/dist/edit/pptx/types.d.ts +232 -0
- package/dist/edit/pptx/types.js +1 -0
- package/dist/edit/schema.d.ts +11 -0
- package/dist/edit/schema.js +275 -0
- package/dist/edit/session.d.ts +66 -0
- package/dist/edit/session.js +701 -0
- package/dist/edit/sessions.d.ts +8 -0
- package/dist/edit/sessions.js +1 -0
- package/dist/edit/types.d.ts +276 -0
- package/dist/edit/types.js +1 -0
- package/dist/edit/worker-engine.d.ts +31 -0
- package/dist/edit/worker-engine.js +91 -0
- package/dist/fonts/THIRD_PARTY_NOTICES.md +3 -0
- package/dist/fonts/manifest.json +6 -1
- package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
- package/dist/fonts.d.ts +2 -0
- package/dist/fonts.js +4 -0
- package/dist/headless.d.ts +4 -0
- package/dist/headless.js +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/limits.js +3 -0
- package/dist/ooxml-edit-worker.d.ts +1 -0
- package/dist/ooxml-edit-worker.js +8 -0
- package/dist/pdf-edit-worker.d.ts +1 -0
- package/dist/pdf-edit-worker.js +35 -0
- package/dist/spreadsheet-viewport.d.ts +5 -0
- package/dist/spreadsheet-viewport.js +11 -0
- package/dist/ui.js +7 -0
- package/dist/viewer.d.ts +6 -0
- package/dist/viewer.js +293 -19
- package/dist/viewport.d.ts +13 -0
- package/dist/viewport.js +126 -1
- package/dist/worker-protocol.d.ts +39 -2
- package/dist/workers/ooxml-edit-worker.js +8671 -0
- package/dist/workers/pdf-edit-worker.js +10844 -0
- package/package.json +3 -3
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { patches } from "../ooxml/patch.js";
|
|
2
|
+
import { encodePart, escapeAttribute } from "../ooxml/xml.js";
|
|
3
|
+
import { relativeTarget, } from "../ooxml/transaction.js";
|
|
4
|
+
import { relationshipsPartOf } from "../ooxml/names.js";
|
|
5
|
+
import { readPlaceholders } from "./elements.js";
|
|
6
|
+
import { partNumber, RELATIONSHIP_TYPES } from "./model.js";
|
|
7
|
+
import { committedParts, } from "./operations.js";
|
|
8
|
+
/*
|
|
9
|
+
* Slide operations: a new slide instantiates its layout's placeholders, a
|
|
10
|
+
* duplicate copies the slide part and clones the parts only it may own,
|
|
11
|
+
* deletion takes the notes slide along, and reordering touches only
|
|
12
|
+
* p:sldIdLst. Every one keeps presentation.xml, its relationships and the
|
|
13
|
+
* content types consistent through one transaction.
|
|
14
|
+
*/
|
|
15
|
+
const SLIDE_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
|
|
16
|
+
const XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
|
|
17
|
+
const NAMESPACES = 'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"';
|
|
18
|
+
const TREE_HEAD = '<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>';
|
|
19
|
+
/** Placeholder types PowerPoint instantiates on a new slide. */
|
|
20
|
+
const INSTANTIATED = new Set([
|
|
21
|
+
"title",
|
|
22
|
+
"ctrTitle",
|
|
23
|
+
"body",
|
|
24
|
+
"subTitle",
|
|
25
|
+
"obj",
|
|
26
|
+
"pic",
|
|
27
|
+
"tbl",
|
|
28
|
+
"chart",
|
|
29
|
+
"dgm",
|
|
30
|
+
"media",
|
|
31
|
+
"clipArt",
|
|
32
|
+
]);
|
|
33
|
+
/** Relationship types whose targets two slides may share. */
|
|
34
|
+
const SHARED_TARGETS = new Set([
|
|
35
|
+
RELATIONSHIP_TYPES.slideLayout,
|
|
36
|
+
RELATIONSHIP_TYPES.image,
|
|
37
|
+
RELATIONSHIP_TYPES.slide,
|
|
38
|
+
`${RELATIONSHIP_TYPES.image.slice(0, -5)}audio`,
|
|
39
|
+
`${RELATIONSHIP_TYPES.image.slice(0, -5)}video`,
|
|
40
|
+
`${RELATIONSHIP_TYPES.image.slice(0, -5)}media`,
|
|
41
|
+
`${RELATIONSHIP_TYPES.image.slice(0, -5)}font`,
|
|
42
|
+
`${RELATIONSHIP_TYPES.image.slice(0, -5)}hyperlink`,
|
|
43
|
+
"http://schemas.microsoft.com/office/2007/relationships/media",
|
|
44
|
+
]);
|
|
45
|
+
/** Relationship types a duplicate leaves behind. */
|
|
46
|
+
const NOT_COPIED = new Set([
|
|
47
|
+
RELATIONSHIP_TYPES.notesSlide,
|
|
48
|
+
`${RELATIONSHIP_TYPES.image.slice(0, -5)}comments`,
|
|
49
|
+
"http://schemas.microsoft.com/office/2018/10/relationships/comments",
|
|
50
|
+
]);
|
|
51
|
+
function checkIndex(value, path, maximum, issue) {
|
|
52
|
+
if (!Number.isInteger(value) || value < 0 || value > maximum) {
|
|
53
|
+
issue(path, "range", `${path.slice(1)} must be between 0 and ${maximum}`);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
/** One more than the largest slide number the package ever held. */
|
|
59
|
+
function nextSlideNumber(pkg) {
|
|
60
|
+
let max = 0;
|
|
61
|
+
for (const name of [...pkg.partNames, ...pkg.currentPartNames])
|
|
62
|
+
if (/^\/ppt\/slides\/slide\d+\.xml$/i.test(name))
|
|
63
|
+
max = Math.max(max, partNumber(name));
|
|
64
|
+
return max + 1;
|
|
65
|
+
}
|
|
66
|
+
/** One more than the largest p:sldId/@id, at least 256. */
|
|
67
|
+
function nextSlideId(context) {
|
|
68
|
+
let max = 255;
|
|
69
|
+
for (const slide of context.model.slides)
|
|
70
|
+
max = Math.max(max, Number(slide.id) || 0);
|
|
71
|
+
return max + 1;
|
|
72
|
+
}
|
|
73
|
+
/** The presentation part scanned at the current revision. */
|
|
74
|
+
function presentationOf(context) {
|
|
75
|
+
return context.pkg.xml(context.model.presentationPart);
|
|
76
|
+
}
|
|
77
|
+
/** The `p:sldId` of a slide in a fresh scan of the presentation. */
|
|
78
|
+
function sldIdOf(presentation, slide) {
|
|
79
|
+
return presentation
|
|
80
|
+
.findAll("sldId")
|
|
81
|
+
.find((node) => presentation.attribute(node, "r:id") === slide.rId);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Patches that put a `p:sldId` at `index` of the list and, when the deck
|
|
85
|
+
* has sections (`p14:sectionLst`), the slide's id after the id of the
|
|
86
|
+
* slide before the position, or first in the first section.
|
|
87
|
+
*/
|
|
88
|
+
function sldIdPatches(context, presentation, index, xml, slideId) {
|
|
89
|
+
const list = presentation.find("sldIdLst");
|
|
90
|
+
const at = context.model.slides[index];
|
|
91
|
+
const anchor = at ? sldIdOf(presentation, at) : undefined;
|
|
92
|
+
const items = [];
|
|
93
|
+
if (anchor)
|
|
94
|
+
items.push(patches.insertBefore(presentation, anchor, xml));
|
|
95
|
+
else if (list)
|
|
96
|
+
items.push(patches.appendChild(presentation, list, xml));
|
|
97
|
+
else {
|
|
98
|
+
// A presentation without a slide list gets one where the schema puts
|
|
99
|
+
// it: before p:sldSz, else at the end of the root.
|
|
100
|
+
const sldSz = presentation.root.children.find((child) => child.local === "sldSz");
|
|
101
|
+
const fragment = `<p:sldIdLst>${xml}</p:sldIdLst>`;
|
|
102
|
+
items.push(sldSz
|
|
103
|
+
? patches.insertBefore(presentation, sldSz, fragment)
|
|
104
|
+
: patches.appendChild(presentation, presentation.root, fragment));
|
|
105
|
+
}
|
|
106
|
+
const sections = presentation.find("sectionLst");
|
|
107
|
+
if (sections) {
|
|
108
|
+
const entry = `<p14:sldId id="${slideId}"/>`;
|
|
109
|
+
const before = context.model.slides[index - 1];
|
|
110
|
+
const previous = before
|
|
111
|
+
? presentation
|
|
112
|
+
.findAll("sldId", sections)
|
|
113
|
+
.find((node) => presentation.attribute(node, "id") === before.id)
|
|
114
|
+
: undefined;
|
|
115
|
+
const firstList = presentation.find("sldIdLst", sections);
|
|
116
|
+
if (previous)
|
|
117
|
+
items.push(patches.insertAfter(presentation, previous, entry));
|
|
118
|
+
else if (firstList) {
|
|
119
|
+
const first = firstList.children[0];
|
|
120
|
+
items.push(first
|
|
121
|
+
? patches.insertBefore(presentation, first, entry)
|
|
122
|
+
: patches.appendChild(presentation, firstList, entry));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return items;
|
|
126
|
+
}
|
|
127
|
+
/** Registers a new slide part with the presentation; returns its sldId XML. */
|
|
128
|
+
async function registerSlide(context, transaction, part) {
|
|
129
|
+
const rId = await transaction.addRelationship(context.model.presentationPart, RELATIONSHIP_TYPES.slide, relativeTarget(context.model.presentationPart, part));
|
|
130
|
+
const id = nextSlideId(context);
|
|
131
|
+
return { xml: `<p:sldId id="${id}" r:id=${escapeAttribute(rId)}/>`, id };
|
|
132
|
+
}
|
|
133
|
+
function slideRange(from, to) {
|
|
134
|
+
const pages = [];
|
|
135
|
+
for (let page = from; page <= to; page += 1)
|
|
136
|
+
pages.push(page);
|
|
137
|
+
return pages;
|
|
138
|
+
}
|
|
139
|
+
export const insertSlideHandler = {
|
|
140
|
+
async validate(operation, context, issue) {
|
|
141
|
+
checkIndex(operation.index, "/index", context.pageCount, issue);
|
|
142
|
+
if (operation.layout !== undefined &&
|
|
143
|
+
!context.model.layoutById(operation.layout))
|
|
144
|
+
issue("/layout", "unknown-layout", `No layout ${operation.layout}`);
|
|
145
|
+
if (operation.layout === undefined && context.model.layouts.length === 0)
|
|
146
|
+
issue("/layout", "unknown-layout", "The deck has no layout to instantiate");
|
|
147
|
+
},
|
|
148
|
+
async apply(operation, context) {
|
|
149
|
+
const { model, pkg } = context;
|
|
150
|
+
const layout = operation.layout
|
|
151
|
+
? model.layoutById(operation.layout)
|
|
152
|
+
: (model.slides[operation.index - 1]?.layout ??
|
|
153
|
+
model.slides[0]?.layout ??
|
|
154
|
+
model.layouts[0]);
|
|
155
|
+
if (!layout)
|
|
156
|
+
throw new Error("The deck has no layout to instantiate");
|
|
157
|
+
const number = nextSlideNumber(pkg);
|
|
158
|
+
const part = `/ppt/slides/slide${number}.xml`;
|
|
159
|
+
const placeholders = await readPlaceholders(pkg, layout.part);
|
|
160
|
+
const shapes = [];
|
|
161
|
+
const createdIds = [];
|
|
162
|
+
let id = 2;
|
|
163
|
+
for (const placeholder of placeholders.placeholders) {
|
|
164
|
+
if (!INSTANTIATED.has(placeholder.type))
|
|
165
|
+
continue;
|
|
166
|
+
const cNvPr = placeholder.node.children
|
|
167
|
+
.find((child) => child.local === "nvSpPr")
|
|
168
|
+
?.children.find((child) => child.local === "cNvPr");
|
|
169
|
+
const name = cNvPr
|
|
170
|
+
? placeholders.part.attribute(cNvPr, "name")
|
|
171
|
+
: undefined;
|
|
172
|
+
const ph = placeholder.node.children
|
|
173
|
+
.find((child) => child.local === "nvSpPr")
|
|
174
|
+
?.children.find((child) => child.local === "nvPr")
|
|
175
|
+
?.children.find((child) => child.local === "ph");
|
|
176
|
+
const type = ph ? placeholders.part.attribute(ph, "type") : undefined;
|
|
177
|
+
const idx = ph ? placeholders.part.attribute(ph, "idx") : undefined;
|
|
178
|
+
shapes.push(`<p:sp><p:nvSpPr><p:cNvPr id="${id}" name=${escapeAttribute(name ?? `Placeholder ${id - 1}`)}/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph${type ? ` type=${escapeAttribute(type)}` : ""}${idx ? ` idx=${escapeAttribute(idx)}` : ""}/></p:nvPr></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody></p:sp>`);
|
|
179
|
+
createdIds.push(`sld${number}:${id}`);
|
|
180
|
+
id += 1;
|
|
181
|
+
}
|
|
182
|
+
const xml = `${XML_HEADER}<p:sld ${NAMESPACES}><p:cSld><p:spTree>${TREE_HEAD}${shapes.join("")}</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>`;
|
|
183
|
+
const transaction = pkg.transaction();
|
|
184
|
+
transaction.setPart(part, encodePart(xml), SLIDE_CONTENT_TYPE);
|
|
185
|
+
await transaction.addRelationship(part, RELATIONSHIP_TYPES.slideLayout, relativeTarget(part, layout.part));
|
|
186
|
+
const registered = await registerSlide(context, transaction, part);
|
|
187
|
+
const presentation = await presentationOf(context);
|
|
188
|
+
transaction.patch(presentation, sldIdPatches(context, presentation, operation.index, registered.xml, registered.id));
|
|
189
|
+
const change = await transaction.commit();
|
|
190
|
+
return {
|
|
191
|
+
createdIds,
|
|
192
|
+
changedPages: slideRange(operation.index, model.pageCount),
|
|
193
|
+
warnings: change.warnings,
|
|
194
|
+
parts: committedParts(change),
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
/** Copies a part and, recursively, the parts only it may own; returns the copy's name. */
|
|
199
|
+
function relationshipXml(item, target) {
|
|
200
|
+
return `<Relationship Id=${escapeAttribute(item.id)} Type=${escapeAttribute(item.type)} Target=${escapeAttribute(target)}${item.targetMode === "External" ? ' TargetMode="External"' : ""}/>`;
|
|
201
|
+
}
|
|
202
|
+
async function clonePart(context, transaction, source, target, contentType, seen, warnings) {
|
|
203
|
+
const { pkg } = context;
|
|
204
|
+
seen.set(source, target);
|
|
205
|
+
transaction.setPart(target, await pkg.part(source), contentType);
|
|
206
|
+
const relationships = await pkg.relationships(source);
|
|
207
|
+
if (relationships.items.length === 0)
|
|
208
|
+
return;
|
|
209
|
+
const copied = [];
|
|
210
|
+
for (const item of relationships.items) {
|
|
211
|
+
if (NOT_COPIED.has(item.type))
|
|
212
|
+
continue;
|
|
213
|
+
let targetValue = item.target;
|
|
214
|
+
if (item.targetMode === "Internal" &&
|
|
215
|
+
item.targetPart &&
|
|
216
|
+
!SHARED_TARGETS.has(item.type)) {
|
|
217
|
+
let clone = seen.get(item.targetPart);
|
|
218
|
+
if (!clone && !pkg.has(item.targetPart)) {
|
|
219
|
+
// A target the package lacks is copied as written, and the host hears.
|
|
220
|
+
warnings.push({
|
|
221
|
+
code: "fidelity-degraded",
|
|
222
|
+
message: `Relationship ${item.id} of ${source} points at a missing part ${item.targetPart}`,
|
|
223
|
+
details: {
|
|
224
|
+
reason: "dangling-relationship",
|
|
225
|
+
part: source,
|
|
226
|
+
id: item.id,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
copied.push(relationshipXml(item, item.target));
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (!clone) {
|
|
233
|
+
const match = /^(.*?)(\d*)(\.[^./]+)$/.exec(item.targetPart);
|
|
234
|
+
clone = match
|
|
235
|
+
? transaction.uniquePartName(match[1], match[3])
|
|
236
|
+
: transaction.uniquePartName(item.targetPart, "");
|
|
237
|
+
await clonePart(context, transaction, item.targetPart, clone, await pkg.contentTypeOf(item.targetPart), seen, warnings);
|
|
238
|
+
}
|
|
239
|
+
targetValue = relativeTarget(target, clone);
|
|
240
|
+
}
|
|
241
|
+
copied.push(relationshipXml(item, targetValue));
|
|
242
|
+
}
|
|
243
|
+
transaction.setPart(relationshipsPartOf(target), encodePart(`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">${copied.join("")}</Relationships>`));
|
|
244
|
+
}
|
|
245
|
+
export const duplicateSlideHandler = {
|
|
246
|
+
async validate(operation, context, issue) {
|
|
247
|
+
checkIndex(operation.pageIndex, "/pageIndex", context.pageCount - 1, issue);
|
|
248
|
+
if (operation.index !== undefined)
|
|
249
|
+
checkIndex(operation.index, "/index", context.pageCount, issue);
|
|
250
|
+
},
|
|
251
|
+
async apply(operation, context) {
|
|
252
|
+
const { model, pkg } = context;
|
|
253
|
+
const source = model.slideAt(operation.pageIndex);
|
|
254
|
+
const index = operation.index ?? operation.pageIndex + 1;
|
|
255
|
+
const number = nextSlideNumber(pkg);
|
|
256
|
+
const part = `/ppt/slides/slide${number}.xml`;
|
|
257
|
+
const transaction = pkg.transaction();
|
|
258
|
+
const warnings = [];
|
|
259
|
+
await clonePart(context, transaction, source.part, part, SLIDE_CONTENT_TYPE, new Map(), warnings);
|
|
260
|
+
const registered = await registerSlide(context, transaction, part);
|
|
261
|
+
const presentation = await presentationOf(context);
|
|
262
|
+
transaction.patch(presentation, sldIdPatches(context, presentation, index, registered.xml, registered.id));
|
|
263
|
+
const elements = await context.elements(operation.pageIndex);
|
|
264
|
+
const change = await transaction.commit();
|
|
265
|
+
return {
|
|
266
|
+
createdIds: elements.records.map((record) => `sld${number}:${record.element.id.slice(source.key.length + 1)}`),
|
|
267
|
+
changedPages: slideRange(index, model.pageCount),
|
|
268
|
+
warnings: [...warnings, ...change.warnings],
|
|
269
|
+
parts: committedParts(change),
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
export const deleteSlideHandler = {
|
|
274
|
+
async validate(operation, context, issue) {
|
|
275
|
+
if (!checkIndex(operation.pageIndex, "/pageIndex", context.pageCount - 1, issue))
|
|
276
|
+
return;
|
|
277
|
+
if (context.pageCount === 1)
|
|
278
|
+
issue("/pageIndex", "last-slide", "The last slide cannot be deleted");
|
|
279
|
+
},
|
|
280
|
+
async apply(operation, context) {
|
|
281
|
+
const { model, pkg } = context;
|
|
282
|
+
const slide = model.slideAt(operation.pageIndex);
|
|
283
|
+
const elements = await context.elements(operation.pageIndex);
|
|
284
|
+
const transaction = pkg.transaction();
|
|
285
|
+
const presentation = await presentationOf(context);
|
|
286
|
+
const entry = sldIdOf(presentation, slide);
|
|
287
|
+
if (!entry)
|
|
288
|
+
throw new Error(`Slide ${slide.key} is not in the slide list`);
|
|
289
|
+
const items = [patches.removeElement(presentation, entry)];
|
|
290
|
+
// Custom shows and sections name the slide too; their entries go with it.
|
|
291
|
+
for (const node of presentation.findAll("sld"))
|
|
292
|
+
if (node.parent?.local === "sldLst" &&
|
|
293
|
+
presentation.attribute(node, "r:id") === slide.rId)
|
|
294
|
+
items.push(patches.removeElement(presentation, node));
|
|
295
|
+
const sections = presentation.find("sectionLst");
|
|
296
|
+
if (sections)
|
|
297
|
+
for (const node of presentation.findAll("sldId", sections))
|
|
298
|
+
if (presentation.attribute(node, "id") === slide.id)
|
|
299
|
+
items.push(patches.removeElement(presentation, node));
|
|
300
|
+
transaction.patch(presentation, items);
|
|
301
|
+
transaction.removeRelationship(model.presentationPart, slide.rId);
|
|
302
|
+
const relationships = await pkg.relationships(slide.part);
|
|
303
|
+
for (const notes of relationships.byType(RELATIONSHIP_TYPES.notesSlide))
|
|
304
|
+
if (notes.targetPart && pkg.has(notes.targetPart))
|
|
305
|
+
transaction.removePart(notes.targetPart);
|
|
306
|
+
transaction.removePart(slide.part);
|
|
307
|
+
const change = await transaction.commit();
|
|
308
|
+
const last = model.pageCount - 2;
|
|
309
|
+
return {
|
|
310
|
+
createdIds: [],
|
|
311
|
+
removedIds: elements.records.map((record) => record.element.id),
|
|
312
|
+
changedPages: operation.pageIndex <= last
|
|
313
|
+
? slideRange(operation.pageIndex, last)
|
|
314
|
+
: [],
|
|
315
|
+
warnings: change.warnings,
|
|
316
|
+
parts: committedParts(change),
|
|
317
|
+
};
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
export const moveSlideHandler = {
|
|
321
|
+
async validate(operation, context, issue) {
|
|
322
|
+
const last = context.pageCount - 1;
|
|
323
|
+
checkIndex(operation.from, "/from", last, issue);
|
|
324
|
+
checkIndex(operation.to, "/to", last, issue);
|
|
325
|
+
},
|
|
326
|
+
async apply(operation, context) {
|
|
327
|
+
const { model, pkg } = context;
|
|
328
|
+
const { from, to } = operation;
|
|
329
|
+
if (from === to)
|
|
330
|
+
return { createdIds: [], changedPages: [], warnings: [] };
|
|
331
|
+
const presentation = await presentationOf(context);
|
|
332
|
+
const moving = sldIdOf(presentation, model.slideAt(from));
|
|
333
|
+
const anchor = sldIdOf(presentation, model.slideAt(to));
|
|
334
|
+
if (!moving || !anchor)
|
|
335
|
+
throw new Error("A slide is missing from the slide list");
|
|
336
|
+
const xml = presentation.text.slice(moving.start, moving.end);
|
|
337
|
+
const transaction = pkg.transaction();
|
|
338
|
+
transaction.patch(presentation, [
|
|
339
|
+
patches.removeElement(presentation, moving),
|
|
340
|
+
to > from
|
|
341
|
+
? patches.insertAfter(presentation, anchor, xml)
|
|
342
|
+
: patches.insertBefore(presentation, anchor, xml),
|
|
343
|
+
]);
|
|
344
|
+
const change = await transaction.commit();
|
|
345
|
+
const result = {
|
|
346
|
+
createdIds: [],
|
|
347
|
+
changedPages: slideRange(Math.min(from, to), Math.max(from, to)),
|
|
348
|
+
warnings: change.warnings,
|
|
349
|
+
parts: committedParts(change),
|
|
350
|
+
};
|
|
351
|
+
return result;
|
|
352
|
+
},
|
|
353
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { EditColor } from "../types.js";
|
|
2
|
+
import type { XmlElement, XmlPart } from "../ooxml/xml.js";
|
|
3
|
+
import type { ThemeFonts } from "./model.js";
|
|
4
|
+
import type { PptxShapeStyle, PptxTextAlign, PptxTextStyle } from "./types.js";
|
|
5
|
+
/** A container of `lvl<N>pPr` children, with the part it lives in. */
|
|
6
|
+
export interface StyleSource {
|
|
7
|
+
readonly part: XmlPart;
|
|
8
|
+
readonly node: XmlElement;
|
|
9
|
+
}
|
|
10
|
+
export interface TextStyleInputs {
|
|
11
|
+
/** `a:rPr` of the run. */
|
|
12
|
+
readonly rPr?: StyleSource;
|
|
13
|
+
/** `a:pPr` of the paragraph. */
|
|
14
|
+
readonly pPr?: StyleSource;
|
|
15
|
+
/** 0-based outline level of the paragraph. */
|
|
16
|
+
readonly level: number;
|
|
17
|
+
/** List styles in priority order. */
|
|
18
|
+
readonly lists: readonly StyleSource[];
|
|
19
|
+
readonly fonts: ThemeFonts;
|
|
20
|
+
}
|
|
21
|
+
/** The colour a fill container holds: `a:solidFill` → a colour, `a:noFill` → "none". */
|
|
22
|
+
export declare function readFill(part: XmlPart, container: XmlElement | undefined): EditColor | "none" | undefined;
|
|
23
|
+
/** The colour inside a `a:solidFill`-like node. */
|
|
24
|
+
export declare function readColor(part: XmlPart, holder: XmlElement): EditColor | undefined;
|
|
25
|
+
/** Fill and line of a `p:spPr`; absent fields are inherited from the style or the theme. */
|
|
26
|
+
export declare function readShapeStyle(part: XmlPart, spPr: XmlElement | undefined): PptxShapeStyle;
|
|
27
|
+
/** Theme font references resolved to the master's faces; other names pass through. */
|
|
28
|
+
export declare function resolveFontName(name: string, fonts: ThemeFonts): string;
|
|
29
|
+
export declare function alignOf(value: string | undefined): PptxTextAlign | undefined;
|
|
30
|
+
/** Resolves the style of one run through the chain; defaults follow PowerPoint. */
|
|
31
|
+
export declare function resolveTextStyle(inputs: TextStyleInputs): PptxTextStyle;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { EMU_PER_PT } from "./geometry.js";
|
|
2
|
+
const DEFAULT_SIZE = 18;
|
|
3
|
+
const DEFAULT_LINE_WIDTH_PT = 0.75;
|
|
4
|
+
const COLOR_MODIFIERS = new Set([
|
|
5
|
+
"tint",
|
|
6
|
+
"shade",
|
|
7
|
+
"alpha",
|
|
8
|
+
"lumMod",
|
|
9
|
+
"lumOff",
|
|
10
|
+
"satMod",
|
|
11
|
+
"satOff",
|
|
12
|
+
"hueMod",
|
|
13
|
+
"hueOff",
|
|
14
|
+
"comp",
|
|
15
|
+
"inv",
|
|
16
|
+
"gray",
|
|
17
|
+
]);
|
|
18
|
+
/** The colour a fill container holds: `a:solidFill` → a colour, `a:noFill` → "none". */
|
|
19
|
+
export function readFill(part, container) {
|
|
20
|
+
if (!container)
|
|
21
|
+
return undefined;
|
|
22
|
+
for (const child of container.children) {
|
|
23
|
+
if (child.local === "noFill")
|
|
24
|
+
return "none";
|
|
25
|
+
if (child.local === "solidFill")
|
|
26
|
+
return readColor(part, child) ?? "auto";
|
|
27
|
+
if (child.local === "gradFill" ||
|
|
28
|
+
child.local === "blipFill" ||
|
|
29
|
+
child.local === "pattFill" ||
|
|
30
|
+
child.local === "grpFill")
|
|
31
|
+
return "auto";
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
/** The colour inside a `a:solidFill`-like node. */
|
|
36
|
+
export function readColor(part, holder) {
|
|
37
|
+
const node = holder.children[0];
|
|
38
|
+
if (!node)
|
|
39
|
+
return undefined;
|
|
40
|
+
switch (node.local) {
|
|
41
|
+
case "srgbClr": {
|
|
42
|
+
const value = part.attribute(node, "val");
|
|
43
|
+
return value && /^[0-9A-Fa-f]{6}$/.test(value)
|
|
44
|
+
? `#${value.toUpperCase()}`
|
|
45
|
+
: "auto";
|
|
46
|
+
}
|
|
47
|
+
case "schemeClr": {
|
|
48
|
+
const theme = part.attribute(node, "val");
|
|
49
|
+
if (!theme)
|
|
50
|
+
return "auto";
|
|
51
|
+
const mods = {};
|
|
52
|
+
for (const child of node.children) {
|
|
53
|
+
if (!COLOR_MODIFIERS.has(child.local))
|
|
54
|
+
continue;
|
|
55
|
+
const value = Number(part.attribute(child, "val") ?? "0");
|
|
56
|
+
mods[child.local] = Number.isFinite(value) ? value : 0;
|
|
57
|
+
}
|
|
58
|
+
return Object.keys(mods).length > 0 ? { theme, mods } : { theme };
|
|
59
|
+
}
|
|
60
|
+
case "sysClr": {
|
|
61
|
+
const value = part.attribute(node, "lastClr");
|
|
62
|
+
return value && /^[0-9A-Fa-f]{6}$/.test(value)
|
|
63
|
+
? `#${value.toUpperCase()}`
|
|
64
|
+
: "auto";
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
return "auto";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Fill and line of a `p:spPr`; absent fields are inherited from the style or the theme. */
|
|
71
|
+
export function readShapeStyle(part, spPr) {
|
|
72
|
+
if (!spPr)
|
|
73
|
+
return {};
|
|
74
|
+
const fill = readFill(part, spPr);
|
|
75
|
+
const ln = spPr.children.find((child) => child.local === "ln");
|
|
76
|
+
let line;
|
|
77
|
+
if (ln) {
|
|
78
|
+
const color = readFill(part, ln);
|
|
79
|
+
if (color !== undefined) {
|
|
80
|
+
const w = Number(part.attribute(ln, "w") ?? NaN);
|
|
81
|
+
line = {
|
|
82
|
+
color,
|
|
83
|
+
width: Number.isFinite(w) ? w / EMU_PER_PT : DEFAULT_LINE_WIDTH_PT,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { ...(fill !== undefined ? { fill } : {}), ...(line ? { line } : {}) };
|
|
88
|
+
}
|
|
89
|
+
function levelProperties(source, level) {
|
|
90
|
+
const name = `lvl${level + 1}pPr`;
|
|
91
|
+
return source.node.children.find((child) => child.local === name);
|
|
92
|
+
}
|
|
93
|
+
function defaultRunProperties(source, level) {
|
|
94
|
+
return levelProperties(source, level)?.children.find((child) => child.local === "defRPr");
|
|
95
|
+
}
|
|
96
|
+
/** Theme font references resolved to the master's faces; other names pass through. */
|
|
97
|
+
export function resolveFontName(name, fonts) {
|
|
98
|
+
if (name.startsWith("+mj"))
|
|
99
|
+
return fonts.major;
|
|
100
|
+
if (name.startsWith("+mn"))
|
|
101
|
+
return fonts.minor;
|
|
102
|
+
return name;
|
|
103
|
+
}
|
|
104
|
+
export function alignOf(value) {
|
|
105
|
+
switch (value) {
|
|
106
|
+
case "l":
|
|
107
|
+
return "left";
|
|
108
|
+
case "ctr":
|
|
109
|
+
return "center";
|
|
110
|
+
case "r":
|
|
111
|
+
return "right";
|
|
112
|
+
case "just":
|
|
113
|
+
case "justLow":
|
|
114
|
+
case "dist":
|
|
115
|
+
case "thaiDist":
|
|
116
|
+
return "justify";
|
|
117
|
+
default:
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Resolves the style of one run through the chain; defaults follow PowerPoint. */
|
|
122
|
+
export function resolveTextStyle(inputs) {
|
|
123
|
+
const runSources = [];
|
|
124
|
+
if (inputs.rPr)
|
|
125
|
+
runSources.push(inputs.rPr);
|
|
126
|
+
for (const list of inputs.lists) {
|
|
127
|
+
const node = defaultRunProperties(list, inputs.level);
|
|
128
|
+
if (node)
|
|
129
|
+
runSources.push({ part: list.part, node });
|
|
130
|
+
}
|
|
131
|
+
const attribute = (name) => {
|
|
132
|
+
for (const source of runSources) {
|
|
133
|
+
const value = source.part.attribute(source.node, name);
|
|
134
|
+
if (value !== undefined)
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
};
|
|
139
|
+
const flag = (name) => {
|
|
140
|
+
const value = attribute(name);
|
|
141
|
+
return value === "1" || value === "true";
|
|
142
|
+
};
|
|
143
|
+
const size = Number(attribute("sz") ?? NaN);
|
|
144
|
+
let fontFamily;
|
|
145
|
+
let color;
|
|
146
|
+
for (const source of runSources) {
|
|
147
|
+
if (fontFamily === undefined) {
|
|
148
|
+
const latin = source.node.children.find((child) => child.local === "latin");
|
|
149
|
+
const face = latin ? source.part.attribute(latin, "typeface") : undefined;
|
|
150
|
+
if (face)
|
|
151
|
+
fontFamily = face;
|
|
152
|
+
}
|
|
153
|
+
if (color === undefined) {
|
|
154
|
+
const fill = readFill(source.part, source.node);
|
|
155
|
+
if (fill !== undefined && fill !== "none")
|
|
156
|
+
color = fill;
|
|
157
|
+
}
|
|
158
|
+
if (fontFamily !== undefined && color !== undefined)
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
const underline = attribute("u");
|
|
162
|
+
let align = inputs.pPr
|
|
163
|
+
? alignOf(inputs.pPr.part.attribute(inputs.pPr.node, "algn"))
|
|
164
|
+
: undefined;
|
|
165
|
+
if (!align)
|
|
166
|
+
for (const list of inputs.lists) {
|
|
167
|
+
const node = levelProperties(list, inputs.level);
|
|
168
|
+
align = node ? alignOf(list.part.attribute(node, "algn")) : undefined;
|
|
169
|
+
if (align)
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
fontFamily: resolveFontName(fontFamily ?? "+mn-lt", inputs.fonts),
|
|
174
|
+
fontSize: Number.isFinite(size) ? size / 100 : DEFAULT_SIZE,
|
|
175
|
+
bold: flag("b"),
|
|
176
|
+
italic: flag("i"),
|
|
177
|
+
underline: underline !== undefined && underline !== "none",
|
|
178
|
+
color: color ?? { theme: "tx1" },
|
|
179
|
+
align: align ?? "left",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { XmlElement, XmlPart } from "../ooxml/xml.js";
|
|
2
|
+
import { type PptxOperationHandler } from "./operations.js";
|
|
3
|
+
import type { TextModel } from "./text.js";
|
|
4
|
+
import type { PptxReplaceTextOperation, PptxSetTextStyleOperation } from "./types.js";
|
|
5
|
+
export declare const replaceTextHandler: PptxOperationHandler<PptxReplaceTextOperation>;
|
|
6
|
+
/**
|
|
7
|
+
* The new content of a text body with `[start, end)` of its text replaced:
|
|
8
|
+
* the paragraphs the range touches are rebuilt, the others keep their
|
|
9
|
+
* bytes. A whole replacement styles the new text like the first run that
|
|
10
|
+
* had text; a ranged one like the run at the start of the range.
|
|
11
|
+
*/
|
|
12
|
+
export declare function replacedBodyContent(part: XmlPart, txBody: XmlElement, model: TextModel, text: string, start: number, end: number, whole: boolean): string;
|
|
13
|
+
export declare const setTextStyleHandler: PptxOperationHandler<PptxSetTextStyleOperation>;
|