@coco-vexel/coco-office-oojson 0.0.0-stage → 1.0.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/LICENSE +202 -0
- package/README.md +99 -2
- package/THIRD_PARTY_NOTICES.md +1188 -0
- package/dist/oojson.global.js +30 -0
- package/package.json +45 -3
- package/src/color.mjs +38 -0
- package/src/extra.mjs +37 -0
- package/src/flat-opc.mjs +394 -0
- package/src/index.mjs +39 -0
- package/src/json-to-ooxml.mjs +605 -0
- package/src/long-tail-parts.mjs +424 -0
- package/src/numbering-to-ooxml.mjs +120 -0
- package/src/ooxml-drawing-to-json.mjs +209 -0
- package/src/ooxml-to-json.mjs +305 -0
- package/src/ppt-content.mjs +346 -0
- package/src/ppt-deck.mjs +248 -0
- package/src/styles-to-ooxml.mjs +302 -0
- package/src/writeback.mjs +1082 -0
- package/src/xml.mjs +66 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
function clone(value) {
|
|
2
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function parseJson(value, label = "ONLYOFFICE PPT content JSON") {
|
|
6
|
+
if (typeof value === "string") {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(value);
|
|
9
|
+
} catch (error) {
|
|
10
|
+
throw new Error(`${label}: invalid JSON string: ${error?.message || error}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (!value || typeof value !== "object") {
|
|
14
|
+
throw new Error(`${label}: expected object or JSON string`);
|
|
15
|
+
}
|
|
16
|
+
return clone(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeHexColor(value) {
|
|
20
|
+
if (typeof value !== "string") return undefined;
|
|
21
|
+
const raw = value.trim().replace(/^#/, "");
|
|
22
|
+
if (!/^[0-9a-fA-F]{6}$/.test(raw)) return undefined;
|
|
23
|
+
return `#${raw.toUpperCase()}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ooColorToHex(value) {
|
|
27
|
+
if (!value || value.auto) return undefined;
|
|
28
|
+
const rgba = value.fill?.color?.color?.rgba || value.color?.rgba || value.rgba || value.fill?.color?.rgba;
|
|
29
|
+
const r = Number(value.r ?? rgba?.red);
|
|
30
|
+
const g = Number(value.g ?? rgba?.green);
|
|
31
|
+
const b = Number(value.b ?? rgba?.blue);
|
|
32
|
+
if (![r, g, b].every((n) => Number.isFinite(n))) return undefined;
|
|
33
|
+
const hx = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0").toUpperCase();
|
|
34
|
+
return `#${hx(r)}${hx(g)}${hx(b)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hexToOoColor(value) {
|
|
38
|
+
const hex = normalizeHexColor(value);
|
|
39
|
+
if (!hex) return undefined;
|
|
40
|
+
return {
|
|
41
|
+
auto: false,
|
|
42
|
+
r: parseInt(hex.slice(1, 3), 16),
|
|
43
|
+
g: parseInt(hex.slice(3, 5), 16),
|
|
44
|
+
b: parseInt(hex.slice(5, 7), 16),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hexToOnlyOfficePptUniFill(value, base) {
|
|
49
|
+
const color = hexToOoColor(value);
|
|
50
|
+
if (!color) return undefined;
|
|
51
|
+
const rgba = { red: color.r, green: color.g, blue: color.b, alpha: 255 };
|
|
52
|
+
return {
|
|
53
|
+
fill: {
|
|
54
|
+
color: {
|
|
55
|
+
rgba,
|
|
56
|
+
color: {
|
|
57
|
+
rgba,
|
|
58
|
+
type: "srgb",
|
|
59
|
+
},
|
|
60
|
+
type: "uniColor",
|
|
61
|
+
},
|
|
62
|
+
type: "solid",
|
|
63
|
+
},
|
|
64
|
+
transparent: base && Object.prototype.hasOwnProperty.call(base, "transparent") ? base.transparent : null,
|
|
65
|
+
type: base?.type || "fill",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function textPrToPptStyle(rPr = {}) {
|
|
70
|
+
const style = {};
|
|
71
|
+
const color = ooColorToHex(rPr.uniFill || rPr.color || rPr.fill?.color);
|
|
72
|
+
if (color) style.color = color;
|
|
73
|
+
if (typeof rPr.b === "boolean") style.bold = rPr.b;
|
|
74
|
+
if (typeof rPr.i === "boolean") style.italic = rPr.i;
|
|
75
|
+
if (rPr.u != null) style.underline = rPr.u !== false && rPr.u !== "none";
|
|
76
|
+
if (typeof rPr.spacing === "number") style.characterSpacing = rPr.spacing;
|
|
77
|
+
if (typeof rPr.sz === "number") style.fontSize = rPr.sz / 2;
|
|
78
|
+
const fonts = rPr.rFonts;
|
|
79
|
+
const family = fonts?.ascii || fonts?.hAnsi || fonts?.eastAsia || fonts?.cs;
|
|
80
|
+
if (family) style.fontFamily = family;
|
|
81
|
+
return style;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function mergeStyleIntoTextPr(base = {}, style = {}) {
|
|
85
|
+
const next = { ...base, type: base.type || "textPr" };
|
|
86
|
+
const color = hexToOoColor(style.color);
|
|
87
|
+
if (color) {
|
|
88
|
+
next.color = color;
|
|
89
|
+
next.uniFill = hexToOnlyOfficePptUniFill(style.color, next.uniFill);
|
|
90
|
+
}
|
|
91
|
+
if (typeof style.bold === "boolean") {
|
|
92
|
+
next.b = style.bold;
|
|
93
|
+
next.bCs = style.bold;
|
|
94
|
+
}
|
|
95
|
+
if (typeof style.italic === "boolean") {
|
|
96
|
+
next.i = style.italic;
|
|
97
|
+
next.iCs = style.italic;
|
|
98
|
+
}
|
|
99
|
+
if (typeof style.underline === "boolean") next.u = style.underline ? "single" : false;
|
|
100
|
+
if (typeof style.characterSpacing === "number") next.spacing = style.characterSpacing;
|
|
101
|
+
if (typeof style.fontSize === "number") {
|
|
102
|
+
next.sz = style.fontSize * 2;
|
|
103
|
+
next.szCs = style.fontSize * 2;
|
|
104
|
+
}
|
|
105
|
+
if (style.fontFamily) {
|
|
106
|
+
next.rFonts = {
|
|
107
|
+
...(next.rFonts || {}),
|
|
108
|
+
ascii: style.fontFamily,
|
|
109
|
+
hAnsi: style.fontFamily,
|
|
110
|
+
eastAsia: style.fontFamily,
|
|
111
|
+
cs: style.fontFamily,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return next;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isParagraph(item) {
|
|
118
|
+
return item && item.type === "paragraph" && Array.isArray(item.content);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isRun(item) {
|
|
122
|
+
return item && item.type === "run" && Array.isArray(item.content);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isTextRunLike(item) {
|
|
126
|
+
return isRun(item) || (item && item.type === "hyperlink" && Array.isArray(item.content));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function flattenTextContent(content) {
|
|
130
|
+
if (!Array.isArray(content)) return "";
|
|
131
|
+
return content
|
|
132
|
+
.map((item) => {
|
|
133
|
+
if (typeof item === "string") return item;
|
|
134
|
+
if (item?.type === "tab") return "\t";
|
|
135
|
+
if (item?.type === "break") return "\n";
|
|
136
|
+
return "";
|
|
137
|
+
})
|
|
138
|
+
.join("");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function visitRunsInParagraph(paragraph, visitor) {
|
|
142
|
+
for (const item of paragraph.content || []) {
|
|
143
|
+
if (isRun(item)) visitor(item, paragraph);
|
|
144
|
+
if (item?.type === "hyperlink" && Array.isArray(item.content)) {
|
|
145
|
+
for (const child of item.content) {
|
|
146
|
+
if (isRun(child)) visitor(child, paragraph);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function textRunsOfContent(docContent) {
|
|
153
|
+
const runs = [];
|
|
154
|
+
for (const block of docContent.content || []) {
|
|
155
|
+
if (!isParagraph(block)) continue;
|
|
156
|
+
visitRunsInParagraph(block, (run, paragraph) => {
|
|
157
|
+
const text = flattenTextContent(run.content);
|
|
158
|
+
if (text) runs.push({ run, paragraph, text });
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return runs;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function makeRun(text, style = {}, template) {
|
|
165
|
+
return {
|
|
166
|
+
bFromDocument: true,
|
|
167
|
+
footnotes: [],
|
|
168
|
+
endnotes: [],
|
|
169
|
+
reviewType: "common",
|
|
170
|
+
...(template ? clone(template) : {}),
|
|
171
|
+
rPr: mergeStyleIntoTextPr(template?.rPr || { bFromDocument: true, type: "textPr" }, style),
|
|
172
|
+
content: [text || ""],
|
|
173
|
+
type: "run",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function makeEndRun(template, style = {}) {
|
|
178
|
+
return {
|
|
179
|
+
bFromDocument: true,
|
|
180
|
+
footnotes: [],
|
|
181
|
+
endnotes: [],
|
|
182
|
+
reviewType: "common",
|
|
183
|
+
...(template ? clone(template) : {}),
|
|
184
|
+
rPr: mergeStyleIntoTextPr(template?.rPr || { bFromDocument: true, type: "textPr" }, style),
|
|
185
|
+
content: [],
|
|
186
|
+
type: "endRun",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function makeParagraph(write = {}, template) {
|
|
191
|
+
const source = template && isParagraph(template) ? clone(template) : undefined;
|
|
192
|
+
const templateRun = source?.content?.find(isRun);
|
|
193
|
+
const templateEndRun = source?.content?.find((item) => item?.type === "endRun");
|
|
194
|
+
const runs = Array.isArray(write.runs) && write.runs.length
|
|
195
|
+
? write.runs.map((run) => makeRun(run.text || "", run.style || write.style || {}, templateRun))
|
|
196
|
+
: [makeRun(write.text || "", write.style || {}, templateRun)];
|
|
197
|
+
return {
|
|
198
|
+
bFromDocument: true,
|
|
199
|
+
...(source || {}),
|
|
200
|
+
pPr: {
|
|
201
|
+
bFromDocument: true,
|
|
202
|
+
type: "paraPr",
|
|
203
|
+
...(source?.pPr || {}),
|
|
204
|
+
},
|
|
205
|
+
rPr: mergeStyleIntoTextPr(source?.rPr || { bFromDocument: true, type: "textPr" }, write.style || {}),
|
|
206
|
+
content: [...runs, makeEndRun(templateEndRun, write.style || {})],
|
|
207
|
+
changes: source?.changes || [],
|
|
208
|
+
type: "paragraph",
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function ensureDocContent(contentJson) {
|
|
213
|
+
const docContent = parseJson(contentJson);
|
|
214
|
+
if (docContent.type !== "docContent" && docContent.type !== "documentContent") {
|
|
215
|
+
throw new Error(`ONLYOFFICE PPT content JSON: expected docContent/documentContent, got ${docContent.type || "unknown"}`);
|
|
216
|
+
}
|
|
217
|
+
if (!Array.isArray(docContent.content)) docContent.content = [];
|
|
218
|
+
return docContent;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function onlyOfficePptContentToTextBody(contentJson) {
|
|
222
|
+
const docContent = ensureDocContent(contentJson);
|
|
223
|
+
const paragraphs = [];
|
|
224
|
+
const allRuns = [];
|
|
225
|
+
for (const block of docContent.content || []) {
|
|
226
|
+
if (!isParagraph(block)) continue;
|
|
227
|
+
const runs = [];
|
|
228
|
+
visitRunsInParagraph(block, (run) => {
|
|
229
|
+
const text = flattenTextContent(run.content);
|
|
230
|
+
if (!text) return;
|
|
231
|
+
const textRun = { text, style: textPrToPptStyle(run.rPr || {}) };
|
|
232
|
+
runs.push(textRun);
|
|
233
|
+
allRuns.push(textRun);
|
|
234
|
+
});
|
|
235
|
+
paragraphs.push({
|
|
236
|
+
text: runs.map((run) => run.text).join(""),
|
|
237
|
+
style: block.pPr?.jc ? { alignment: block.pPr.jc } : undefined,
|
|
238
|
+
runs,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
text: paragraphs.map((paragraph) => paragraph.text).join("\n"),
|
|
243
|
+
style: allRuns[0]?.style,
|
|
244
|
+
paragraphs,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function setOnlyOfficePptContentText(contentJson, text, options = {}) {
|
|
249
|
+
const docContent = ensureDocContent(contentJson);
|
|
250
|
+
const firstParagraph = docContent.content.find(isParagraph);
|
|
251
|
+
docContent.content = [makeParagraph({ text: String(text ?? ""), style: options.style || {} }, firstParagraph)];
|
|
252
|
+
return docContent;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function setOnlyOfficePptContentParagraphs(contentJson, paragraphs) {
|
|
256
|
+
const docContent = ensureDocContent(contentJson);
|
|
257
|
+
const templates = docContent.content.filter(isParagraph);
|
|
258
|
+
const writes = Array.isArray(paragraphs) && paragraphs.length ? paragraphs : [{ text: "" }];
|
|
259
|
+
docContent.content = writes.map((paragraph, index) => makeParagraph(paragraph || {}, templates[index] || templates[0]));
|
|
260
|
+
return docContent;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function setOnlyOfficePptContentStyle(contentJson, style) {
|
|
264
|
+
const docContent = ensureDocContent(contentJson);
|
|
265
|
+
for (const block of docContent.content || []) {
|
|
266
|
+
if (!isParagraph(block)) continue;
|
|
267
|
+
block.rPr = mergeStyleIntoTextPr(block.rPr || { bFromDocument: true, type: "textPr" }, style || {});
|
|
268
|
+
for (const item of block.content || []) {
|
|
269
|
+
if (item?.type !== "endRun") continue;
|
|
270
|
+
item.rPr = mergeStyleIntoTextPr(item.rPr || { bFromDocument: true, type: "textPr" }, style || {});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const { run, paragraph } of textRunsOfContent(docContent)) {
|
|
274
|
+
paragraph.rPr = mergeStyleIntoTextPr(
|
|
275
|
+
paragraph.rPr || { bFromDocument: true, type: "textPr" },
|
|
276
|
+
style || {},
|
|
277
|
+
);
|
|
278
|
+
run.rPr = mergeStyleIntoTextPr(run.rPr || { bFromDocument: true, type: "textPr" }, style || {});
|
|
279
|
+
}
|
|
280
|
+
return docContent;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function setOnlyOfficePptContentRunStyle(contentJson, runIndex, style) {
|
|
284
|
+
const docContent = ensureDocContent(contentJson);
|
|
285
|
+
const runs = textRunsOfContent(docContent);
|
|
286
|
+
if (!Number.isInteger(runIndex) || runIndex < 0 || runIndex >= runs.length) {
|
|
287
|
+
throw new Error(`ONLYOFFICE PPT content JSON: runIndex out of range: ${runIndex}`);
|
|
288
|
+
}
|
|
289
|
+
const target = runs[runIndex].run;
|
|
290
|
+
const paragraph = runs[runIndex].paragraph;
|
|
291
|
+
paragraph.rPr = mergeStyleIntoTextPr(
|
|
292
|
+
paragraph.rPr || { bFromDocument: true, type: "textPr" },
|
|
293
|
+
style || {},
|
|
294
|
+
);
|
|
295
|
+
target.rPr = mergeStyleIntoTextPr(target.rPr || { bFromDocument: true, type: "textPr" }, style || {});
|
|
296
|
+
return docContent;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function replaceOnlyOfficePptContentText(contentJson, search, replacement, count = Number.POSITIVE_INFINITY) {
|
|
300
|
+
const docContent = ensureDocContent(contentJson);
|
|
301
|
+
if (search == null || search === "") return { content: docContent, count: 0 };
|
|
302
|
+
let changed = 0;
|
|
303
|
+
for (const { run } of textRunsOfContent(docContent)) {
|
|
304
|
+
run.content = (run.content || []).map((item) => {
|
|
305
|
+
if (typeof item !== "string" || changed >= count) return item;
|
|
306
|
+
let next = item;
|
|
307
|
+
while (next.includes(search) && changed < count) {
|
|
308
|
+
next = next.replace(search, replacement ?? "");
|
|
309
|
+
changed += 1;
|
|
310
|
+
}
|
|
311
|
+
return next;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return { content: docContent, count: changed };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function applyPptTextMutationToOnlyOfficeContent(contentJson, mutation) {
|
|
318
|
+
if (!mutation || typeof mutation !== "object") {
|
|
319
|
+
throw new Error("PPT mutation is required");
|
|
320
|
+
}
|
|
321
|
+
switch (mutation.type) {
|
|
322
|
+
case "shape.text.set":
|
|
323
|
+
return { content: setOnlyOfficePptContentText(contentJson, mutation.text), changedCount: 1 };
|
|
324
|
+
case "shape.text.paragraphs.set":
|
|
325
|
+
return {
|
|
326
|
+
content: setOnlyOfficePptContentParagraphs(contentJson, mutation.paragraphs),
|
|
327
|
+
changedCount: Array.isArray(mutation.paragraphs) ? mutation.paragraphs.length : 0,
|
|
328
|
+
};
|
|
329
|
+
case "shape.text.style.set":
|
|
330
|
+
return {
|
|
331
|
+
content: setOnlyOfficePptContentStyle(contentJson, mutation.style),
|
|
332
|
+
changedCount: textRunsOfContent(ensureDocContent(contentJson)).length,
|
|
333
|
+
};
|
|
334
|
+
case "shape.text.run.style.set":
|
|
335
|
+
return {
|
|
336
|
+
content: setOnlyOfficePptContentRunStyle(contentJson, mutation.runIndex, mutation.style),
|
|
337
|
+
changedCount: 1,
|
|
338
|
+
};
|
|
339
|
+
case "shape.text.replace": {
|
|
340
|
+
const result = replaceOnlyOfficePptContentText(contentJson, mutation.search, mutation.replacement, mutation.count);
|
|
341
|
+
return { content: result.content, changedCount: result.count };
|
|
342
|
+
}
|
|
343
|
+
default:
|
|
344
|
+
throw new Error(`Unsupported PPT text mutation for ONLYOFFICE content bridge: ${mutation.type}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
package/src/ppt-deck.mjs
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import {
|
|
2
|
+
onlyOfficePptContentToTextBody,
|
|
3
|
+
setOnlyOfficePptContentParagraphs,
|
|
4
|
+
setOnlyOfficePptContentStyle,
|
|
5
|
+
setOnlyOfficePptContentText,
|
|
6
|
+
} from "./ppt-content.mjs";
|
|
7
|
+
|
|
8
|
+
function clone(value) {
|
|
9
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseJson(value, label = "ONLYOFFICE PPT JSON") {
|
|
13
|
+
if (typeof value === "string") {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(value);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw new Error(`${label}: invalid JSON string: ${error?.message || error}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!value || typeof value !== "object") {
|
|
21
|
+
throw new Error(`${label}: expected object or JSON string`);
|
|
22
|
+
}
|
|
23
|
+
return clone(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeSlidesRoot(value) {
|
|
27
|
+
const root = parseJson(value, "ONLYOFFICE PPT deck JSON");
|
|
28
|
+
if (!Array.isArray(root.slides)) {
|
|
29
|
+
throw new Error("ONLYOFFICE PPT deck JSON: expected slides[]");
|
|
30
|
+
}
|
|
31
|
+
return root;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function shapeId(shape, index) {
|
|
35
|
+
return String(shape?.id ?? shape?.nvSpPr?.cNvPr?.id ?? index);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function shapeName(shape) {
|
|
39
|
+
return shape?.nvSpPr?.cNvPr?.name;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function shapeGeometry(shape = {}) {
|
|
43
|
+
const xfrm = shape.spPr?.xfrm || {};
|
|
44
|
+
const off = xfrm.off || {};
|
|
45
|
+
const ext = xfrm.ext || {};
|
|
46
|
+
return {
|
|
47
|
+
left: Number.isFinite(Number(off.x)) ? Number(off.x) : undefined,
|
|
48
|
+
top: Number.isFinite(Number(off.y)) ? Number(off.y) : undefined,
|
|
49
|
+
width: Number.isFinite(Number(ext.cx ?? shape.extX)) ? Number(ext.cx ?? shape.extX) : undefined,
|
|
50
|
+
height: Number.isFinite(Number(ext.cy ?? shape.extY)) ? Number(ext.cy ?? shape.extY) : undefined,
|
|
51
|
+
rotation: Number.isFinite(Number(xfrm.rot)) ? Number(xfrm.rot) : undefined,
|
|
52
|
+
flipHorizontal: xfrm.flipH === true ? true : undefined,
|
|
53
|
+
flipVertical: xfrm.flipV === true ? true : undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function shapeKind(shape = {}) {
|
|
58
|
+
if (shape.type === "group") return "group";
|
|
59
|
+
if (shape.type === "image" || shape.blipFill) return "picture";
|
|
60
|
+
if (shape.graphicObject?.table || shape.table) return "table";
|
|
61
|
+
if (shape.graphicObject?.chart || shape.chart) return "chart";
|
|
62
|
+
if (shapeDocContent(shape)) return "textBox";
|
|
63
|
+
if (shape.spPr?.custGeom || shape.spPr?.prstGeom || shape.spPr?.geometry) return "geometricShape";
|
|
64
|
+
return "unknown";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function slideSize(root) {
|
|
68
|
+
const size = root.sldSz || {};
|
|
69
|
+
const width = Number(size.cx ?? size.w ?? size.width);
|
|
70
|
+
const height = Number(size.cy ?? size.h ?? size.height);
|
|
71
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return undefined;
|
|
72
|
+
return { width, height };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function textBodyFromShape(shape) {
|
|
76
|
+
const docContent = shapeDocContent(shape);
|
|
77
|
+
if (!docContent) return undefined;
|
|
78
|
+
try {
|
|
79
|
+
return onlyOfficePptContentToTextBody(docContent);
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function shapeDocContent(shape) {
|
|
86
|
+
const content = shape?.content;
|
|
87
|
+
if (!content || typeof content !== "object") return undefined;
|
|
88
|
+
if (content.type === "docContent" || content.type === "documentContent") return content;
|
|
89
|
+
const inner = content.content;
|
|
90
|
+
if (inner && typeof inner === "object" && (inner.type === "docContent" || inner.type === "documentContent")) {
|
|
91
|
+
return inner;
|
|
92
|
+
}
|
|
93
|
+
if (inner && typeof inner === "object" && Array.isArray(inner.content)) {
|
|
94
|
+
return { ...inner, type: "docContent", bPresentation: inner.bPresentation ?? true };
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function setShapeDocContent(shape, docContent) {
|
|
100
|
+
if (!shape?.content || !docContent) return false;
|
|
101
|
+
if (shape.content.type === "docContent" || shape.content.type === "documentContent") {
|
|
102
|
+
shape.content = docContent;
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
shape.content = {
|
|
106
|
+
...shape.content,
|
|
107
|
+
content: docContent,
|
|
108
|
+
};
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function titleFromShapes(shapes) {
|
|
113
|
+
for (const shape of shapes) {
|
|
114
|
+
const text = shape.text?.text?.trim();
|
|
115
|
+
if (text) return text;
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function ooShapeToCoreShape(shape, index) {
|
|
121
|
+
const text = textBodyFromShape(shape);
|
|
122
|
+
const children = Array.isArray(shape.spTree)
|
|
123
|
+
? shape.spTree.map((child, childIndex) => ooShapeToCoreShape(child, childIndex))
|
|
124
|
+
: undefined;
|
|
125
|
+
return {
|
|
126
|
+
id: shapeId(shape, index),
|
|
127
|
+
name: shapeName(shape),
|
|
128
|
+
kind: shapeKind(shape),
|
|
129
|
+
geometry: shapeGeometry(shape),
|
|
130
|
+
text,
|
|
131
|
+
children,
|
|
132
|
+
raw: {
|
|
133
|
+
hostType: "onlyoffice-ppt-oojson",
|
|
134
|
+
presetGeometry: shape.spPr?.prstGeom?.prst || shape.spPr?.custGeom?.preset,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function onlyOfficePptDeckToPptModel(deckJson, options = {}) {
|
|
140
|
+
const root = normalizeSlidesRoot(deckJson);
|
|
141
|
+
const slides = root.slides.map((slide, slideIndex) => {
|
|
142
|
+
const spTree = Array.isArray(slide?.cSld?.spTree) ? slide.cSld.spTree : [];
|
|
143
|
+
const shapes = spTree.map((shape, shapeIndex) => ooShapeToCoreShape(shape, shapeIndex));
|
|
144
|
+
return {
|
|
145
|
+
id: String(slide?.id ?? slideIndex),
|
|
146
|
+
index: slideIndex,
|
|
147
|
+
name: slide?.name,
|
|
148
|
+
title: titleFromShapes(shapes),
|
|
149
|
+
shapes,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
id: options.id,
|
|
154
|
+
title: options.title,
|
|
155
|
+
slideSize: slideSize(root),
|
|
156
|
+
slides,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function findSlide(root, modelSlide) {
|
|
161
|
+
if (Number.isInteger(modelSlide?.index) && root.slides[modelSlide.index]) {
|
|
162
|
+
return root.slides[modelSlide.index];
|
|
163
|
+
}
|
|
164
|
+
return root.slides.find((slide) => String(slide?.id) === String(modelSlide?.id));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function findShape(spTree, modelShape) {
|
|
168
|
+
if (!Array.isArray(spTree)) return undefined;
|
|
169
|
+
const requestedId = String(modelShape?.id ?? "");
|
|
170
|
+
const requestedInternalId = requestedId.includes("/shape:")
|
|
171
|
+
? requestedId.slice(requestedId.lastIndexOf("/shape:") + "/shape:".length)
|
|
172
|
+
: requestedId;
|
|
173
|
+
return spTree.find((shape, index) => {
|
|
174
|
+
const id = shapeId(shape, index);
|
|
175
|
+
return (
|
|
176
|
+
id === requestedId ||
|
|
177
|
+
id === requestedInternalId ||
|
|
178
|
+
(shapeName(shape) && shapeName(shape) === modelShape?.name)
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function applyTextBodyToShape(shape, textBody) {
|
|
184
|
+
const docContent = shapeDocContent(shape);
|
|
185
|
+
if (!shape || !textBody || !docContent) return false;
|
|
186
|
+
let next = docContent;
|
|
187
|
+
if (Array.isArray(textBody.paragraphs) && textBody.paragraphs.length) {
|
|
188
|
+
next = setOnlyOfficePptContentParagraphs(next, textBody.paragraphs);
|
|
189
|
+
} else if (typeof textBody.text === "string") {
|
|
190
|
+
next = setOnlyOfficePptContentText(next, textBody.text, { style: textBody.style });
|
|
191
|
+
}
|
|
192
|
+
if (textBody.style) {
|
|
193
|
+
next = setOnlyOfficePptContentStyle(next, textBody.style);
|
|
194
|
+
}
|
|
195
|
+
return setShapeDocContent(shape, next);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function applyPptModelToOnlyOfficePptDeck(deckJson, model) {
|
|
199
|
+
const root = normalizeSlidesRoot(deckJson);
|
|
200
|
+
let changedCount = 0;
|
|
201
|
+
const changed = [];
|
|
202
|
+
for (const modelSlide of model?.slides || []) {
|
|
203
|
+
const slide = findSlide(root, modelSlide);
|
|
204
|
+
const spTree = slide?.cSld?.spTree;
|
|
205
|
+
if (!Array.isArray(spTree)) continue;
|
|
206
|
+
for (const modelShape of modelSlide.shapes || []) {
|
|
207
|
+
const shape = findShape(spTree, modelShape);
|
|
208
|
+
if (!shape || !modelShape.text) continue;
|
|
209
|
+
if (applyTextBodyToShape(shape, modelShape.text)) {
|
|
210
|
+
changedCount += 1;
|
|
211
|
+
changed.push({
|
|
212
|
+
slideIndex: modelSlide.index,
|
|
213
|
+
shapeId: modelShape.id,
|
|
214
|
+
shapeName: modelShape.name,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { deck: root, changedCount, changed };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function updateOnlyOfficePptDeckTextShape(deckJson, target, textBody) {
|
|
223
|
+
const model = onlyOfficePptDeckToPptModel(deckJson);
|
|
224
|
+
const slide = model.slides[target?.slideIndex ?? 0];
|
|
225
|
+
if (!slide) return { deck: normalizeSlidesRoot(deckJson), changedCount: 0, changed: [] };
|
|
226
|
+
const shape = slide.shapes.find((item) => {
|
|
227
|
+
const itemInternalId = item.id.includes("/shape:")
|
|
228
|
+
? item.id.slice(item.id.lastIndexOf("/shape:") + "/shape:".length)
|
|
229
|
+
: item.id;
|
|
230
|
+
const targetId = target?.shapeId == null ? undefined : String(target.shapeId);
|
|
231
|
+
const targetInternalId = targetId?.includes("/shape:")
|
|
232
|
+
? targetId.slice(targetId.lastIndexOf("/shape:") + "/shape:".length)
|
|
233
|
+
: targetId;
|
|
234
|
+
return (
|
|
235
|
+
item.id === targetId ||
|
|
236
|
+
itemInternalId === targetInternalId ||
|
|
237
|
+
(target?.shapeName && item.name === target.shapeName) ||
|
|
238
|
+
(target?.text && item.text?.text === target.text)
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
if (!shape) return { deck: normalizeSlidesRoot(deckJson), changedCount: 0, changed: [] };
|
|
242
|
+
const nextText = { ...(shape.text || { text: "" }), ...(textBody || {}) };
|
|
243
|
+
if (typeof textBody?.text === "string" && !Array.isArray(textBody.paragraphs)) {
|
|
244
|
+
delete nextText.paragraphs;
|
|
245
|
+
}
|
|
246
|
+
shape.text = nextText;
|
|
247
|
+
return applyPptModelToOnlyOfficePptDeck(deckJson, { slides: [slide] });
|
|
248
|
+
}
|