787 lines
40 KiB
TypeScript
787 lines
40 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import JSZip from "jszip";
|
|
import {
|
|
exportPptxBlob,
|
|
extractTextBlocks,
|
|
importPptxFile,
|
|
listSlidePaths,
|
|
normalizeWhitespace,
|
|
presentationSlidePaths,
|
|
relationshipTargetByType,
|
|
resolvePptTarget
|
|
} from "./powerPointOffice";
|
|
|
|
const tinyPngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=";
|
|
|
|
describe("powerPointOffice helpers", () => {
|
|
it("извлекает текстовые runs из OOXML-подобных узлов", () => {
|
|
const parsedSlide = {
|
|
sld: {
|
|
cSld: {
|
|
spTree: {
|
|
sp: [
|
|
{ txBody: { p: { r: { t: "Заголовок" } } } },
|
|
{ txBody: { p: [{ r: { t: "Первый пункт" } }, { r: { t: "Второй пункт" } }] } }
|
|
]
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
expect(extractTextBlocks(parsedSlide)).toEqual(["Заголовок", "Первый пункт", "Второй пункт"]);
|
|
});
|
|
|
|
it("сортирует пути слайдов по числовому индексу", () => {
|
|
const zip = {
|
|
files: {
|
|
"ppt/slides/slide10.xml": {},
|
|
"ppt/slides/slide2.xml": {},
|
|
"ppt/slides/slide1.xml": {},
|
|
"ppt/notesSlides/notesSlide1.xml": {}
|
|
},
|
|
file: () => null
|
|
};
|
|
|
|
expect(listSlidePaths(zip)).toEqual([
|
|
{ path: "ppt/slides/slide1.xml", index: 1 },
|
|
{ path: "ppt/slides/slide2.xml", index: 2 },
|
|
{ path: "ppt/slides/slide10.xml", index: 10 }
|
|
]);
|
|
});
|
|
|
|
it("читает порядок слайдов из presentation relationships", () => {
|
|
const presentationXml = {
|
|
presentation: {
|
|
sldIdLst: {
|
|
sldId: [{ id: "300", "r:id": "rIdB" }, { id: "200", "r:id": "rIdA" }]
|
|
}
|
|
}
|
|
};
|
|
const relationshipsXml = {
|
|
Relationships: {
|
|
Relationship: [
|
|
{
|
|
Id: "rIdA",
|
|
Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide",
|
|
Target: "slides/slide1.xml"
|
|
},
|
|
{
|
|
Id: "rIdB",
|
|
Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide",
|
|
Target: "slides/slide10.xml"
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
expect(presentationSlidePaths(presentationXml, relationshipsXml)).toEqual(["ppt/slides/slide10.xml", "ppt/slides/slide1.xml"]);
|
|
expect(listSlidePaths({ files: {}, file: () => null }, presentationXml, relationshipsXml)).toEqual([
|
|
{ path: "ppt/slides/slide10.xml", index: 10 },
|
|
{ path: "ppt/slides/slide1.xml", index: 1 }
|
|
]);
|
|
});
|
|
|
|
it("разрешает относительные targets заметок от части слайда", () => {
|
|
expect(resolvePptTarget("ppt/slides", "../notesSlides/notesSlide7.xml")).toBe("ppt/notesSlides/notesSlide7.xml");
|
|
expect(
|
|
relationshipTargetByType(
|
|
{
|
|
Relationships: {
|
|
Relationship: {
|
|
Id: "rIdNotes",
|
|
Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide",
|
|
Target: "../notesSlides/notesSlide7.xml"
|
|
}
|
|
}
|
|
},
|
|
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide",
|
|
"ppt/slides/slide3.xml"
|
|
)
|
|
).toBe("ppt/notesSlides/notesSlide7.xml");
|
|
});
|
|
|
|
it("импортирует PPTX в порядке presentation.xml и берет заметки через slide rels", async () => {
|
|
const zip = new JSZip();
|
|
zip.file(
|
|
"ppt/presentation.xml",
|
|
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="rId2"/><p:sldId id="255" r:id="rId1"/></p:sldIdLst></p:presentation>`
|
|
);
|
|
zip.file(
|
|
"ppt/_rels/presentation.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide2.xml"/></Relationships>`
|
|
);
|
|
zip.file("ppt/slides/slide1.xml", slideXml("Первый файл", "Но второй по порядку"));
|
|
zip.file("ppt/slides/slide2.xml", slideXml("Первый по порядку", "Содержимое первого слайда"));
|
|
zip.file(
|
|
"ppt/slides/_rels/slide2.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rIdNotes" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide" Target="../notesSlides/notesSlide9.xml"/></Relationships>`
|
|
);
|
|
zip.file("ppt/notesSlides/notesSlide9.xml", slideXml("Заметка докладчика"));
|
|
|
|
const blob = await zip.generateAsync({
|
|
type: "blob",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
});
|
|
const imported = await importPptxFile(new File([blob], "Дорожная карта.pptx", { type: blob.type }));
|
|
|
|
expect(imported.slides.map((slide) => slide.title)).toEqual(["Первый по порядку", "Первый файл"]);
|
|
expect(imported.slides[0].subtitle).toBe("Содержимое первого слайда");
|
|
expect(imported.slides[0].notes).toBe("Заметка докладчика");
|
|
});
|
|
|
|
it("сохраняет переносы абзацев внутри текстового блока PPTX при импорте", async () => {
|
|
const zip = new JSZip();
|
|
zip.file(
|
|
"ppt/presentation.xml",
|
|
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`
|
|
);
|
|
zip.file(
|
|
"ppt/_rels/presentation.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>`
|
|
);
|
|
zip.file("ppt/slides/slide1.xml", slideXmlWithMultilineSubtitle("Roadmap", ["Plan", "Build", "Ship"]));
|
|
|
|
const blob = await zip.generateAsync({
|
|
type: "blob",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
});
|
|
const imported = await importPptxFile(new File([blob], "Roadmap.pptx", { type: blob.type }));
|
|
|
|
expect(imported.slides[0].title).toBe("Roadmap");
|
|
expect(imported.slides[0].subtitle).toBe("Plan\nBuild\nShip");
|
|
});
|
|
|
|
it("сохраняет ручные переносы строк a:br внутри абзаца PPTX при импорте", async () => {
|
|
const zip = new JSZip();
|
|
zip.file(
|
|
"ppt/presentation.xml",
|
|
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`
|
|
);
|
|
zip.file(
|
|
"ppt/_rels/presentation.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>`
|
|
);
|
|
zip.file("ppt/slides/slide1.xml", slideXmlWithManualBreakSubtitle("Roadmap", ["Plan", "Build", "Ship"]));
|
|
|
|
const blob = await zip.generateAsync({
|
|
type: "blob",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
});
|
|
const imported = await importPptxFile(new File([blob], "Roadmap.pptx", { type: blob.type }));
|
|
|
|
expect(imported.slides[0].title).toBe("Roadmap");
|
|
expect(imported.slides[0].subtitle).toBe("Plan\nBuild\nShip");
|
|
});
|
|
|
|
it("импортирует маркированные и нумерованные списки подзаголовка PPTX", async () => {
|
|
const bulletZip = new JSZip();
|
|
bulletZip.file(
|
|
"ppt/presentation.xml",
|
|
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`
|
|
);
|
|
bulletZip.file(
|
|
"ppt/_rels/presentation.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>`
|
|
);
|
|
bulletZip.file("ppt/slides/slide1.xml", slideXmlWithListSubtitle("План", ["Собрать требования", "Проверить импорт"], "bullet"));
|
|
|
|
const bulletBlob = await bulletZip.generateAsync({
|
|
type: "blob",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
});
|
|
const importedBullet = await importPptxFile(new File([bulletBlob], "Маркированный.pptx", { type: bulletBlob.type }));
|
|
|
|
expect(importedBullet.slides[0].subtitle).toBe("Собрать требования\nПроверить импорт");
|
|
expect(importedBullet.slides[0].subtitleListStyle).toBe("bullet");
|
|
|
|
const numberZip = new JSZip();
|
|
numberZip.file(
|
|
"ppt/presentation.xml",
|
|
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`
|
|
);
|
|
numberZip.file(
|
|
"ppt/_rels/presentation.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>`
|
|
);
|
|
numberZip.file("ppt/slides/slide1.xml", slideXmlWithListSubtitle("План", ["Шаг один", "Шаг два"], "number"));
|
|
|
|
const numberBlob = await numberZip.generateAsync({
|
|
type: "blob",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
});
|
|
const importedNumber = await importPptxFile(new File([numberBlob], "Нумерованный.pptx", { type: numberBlob.type }));
|
|
|
|
expect(importedNumber.slides[0].subtitle).toBe("Шаг один\nШаг два");
|
|
expect(importedNumber.slides[0].subtitleListStyle).toBe("number");
|
|
});
|
|
|
|
it("экспортирует списки подзаголовка PPTX как OOXML bullets", async () => {
|
|
const bulletBlob = await exportPptxBlob({
|
|
title: "Маркированный.pptx",
|
|
slides: [
|
|
{
|
|
id: "slide-1",
|
|
title: "План",
|
|
subtitle: "Собрать требования\nПроверить импорт",
|
|
subtitleListStyle: "bullet",
|
|
notes: "",
|
|
theme: "classic"
|
|
}
|
|
]
|
|
});
|
|
const bulletZip = await JSZip.loadAsync(await bulletBlob.arrayBuffer());
|
|
const bulletSlideXml = (await bulletZip.file("ppt/slides/slide1.xml")?.async("string")) ?? "";
|
|
|
|
expect(bulletSlideXml).toContain("<a:buChar");
|
|
|
|
const numberBlob = await exportPptxBlob({
|
|
title: "Нумерованный.pptx",
|
|
slides: [
|
|
{
|
|
id: "slide-1",
|
|
title: "План",
|
|
subtitle: "Шаг один\nШаг два",
|
|
subtitleListStyle: "number",
|
|
notes: "",
|
|
theme: "classic"
|
|
}
|
|
]
|
|
});
|
|
const numberZip = await JSZip.loadAsync(await numberBlob.arrayBuffer());
|
|
const numberSlideXml = (await numberZip.file("ppt/slides/slide1.xml")?.async("string")) ?? "";
|
|
|
|
expect(numberSlideXml).toContain("<a:buAutoNum");
|
|
});
|
|
|
|
it("импортирует изображения слайда из PPTX media relationships", async () => {
|
|
const zip = new JSZip();
|
|
zip.file(
|
|
"ppt/presentation.xml",
|
|
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`
|
|
);
|
|
zip.file(
|
|
"ppt/_rels/presentation.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>`
|
|
);
|
|
zip.file("ppt/slides/slide1.xml", slideXmlWithPicture("Слайд с изображением"));
|
|
zip.file(
|
|
"ppt/slides/_rels/slide1.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rIdImage" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/logo.png"/></Relationships>`
|
|
);
|
|
zip.file("ppt/media/logo.png", tinyPngBase64, { base64: true });
|
|
|
|
const blob = await zip.generateAsync({
|
|
type: "blob",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
});
|
|
const imported = await importPptxFile(new File([blob], "Изображения.pptx", { type: blob.type }));
|
|
|
|
expect(imported.slides[0].title).toBe("Слайд с изображением");
|
|
expect(imported.slides[0].images).toEqual([
|
|
{
|
|
id: "pptx-slide-1-image-1",
|
|
src: `data:image/png;base64,${tinyPngBase64}`,
|
|
alt: "Логотип",
|
|
x: 1,
|
|
y: 2,
|
|
width: 3,
|
|
height: 1.5
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("импортирует внешние ссылки изображений PPTX через hlinkClick relationships", async () => {
|
|
const zip = new JSZip();
|
|
zip.file(
|
|
"ppt/presentation.xml",
|
|
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`
|
|
);
|
|
zip.file(
|
|
"ppt/_rels/presentation.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>`
|
|
);
|
|
zip.file("ppt/slides/slide1.xml", slideXmlWithLinkedPicture("Слайд с кликабельным изображением"));
|
|
zip.file(
|
|
"ppt/slides/_rels/slide1.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rIdImage" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/logo.png"/><Relationship Id="rIdImageLink" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="https://example.com/logo" TargetMode="External"/></Relationships>`
|
|
);
|
|
zip.file("ppt/media/logo.png", tinyPngBase64, { base64: true });
|
|
|
|
const blob = await zip.generateAsync({
|
|
type: "blob",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
});
|
|
const imported = await importPptxFile(new File([blob], "Кликабельное изображение.pptx", { type: blob.type }));
|
|
|
|
expect(imported.slides[0].images?.[0]).toMatchObject({
|
|
alt: "Логотип",
|
|
hyperlink: "https://example.com/logo"
|
|
});
|
|
});
|
|
|
|
it("экспортирует изображения слайда в PPTX media relationships", async () => {
|
|
const blob = await exportPptxBlob({
|
|
title: "Слайды с изображением.pptx",
|
|
slides: [
|
|
{
|
|
id: "slide-1",
|
|
title: "Слайд с изображением",
|
|
subtitle: "Проверка экспорта",
|
|
notes: "",
|
|
theme: "classic",
|
|
images: [
|
|
{
|
|
id: "image-1",
|
|
src: `data:image/png;base64,${tinyPngBase64}`,
|
|
alt: "Логотип",
|
|
x: 1,
|
|
y: 2,
|
|
width: 3,
|
|
height: 1.5
|
|
}
|
|
]
|
|
}
|
|
]
|
|
});
|
|
const zip = await JSZip.loadAsync(await blob.arrayBuffer());
|
|
const slideXml = await zip.file("ppt/slides/slide1.xml")?.async("string");
|
|
const relationshipsXml = await zip.file("ppt/slides/_rels/slide1.xml.rels")?.async("string");
|
|
const mediaFiles = Object.keys(zip.files).filter((path) => path.startsWith("ppt/media/") && path.endsWith(".png"));
|
|
|
|
expect(slideXml).toContain("<p:pic>");
|
|
expect(slideXml).toContain('descr="Логотип"');
|
|
expect(relationshipsXml).toContain('Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"');
|
|
expect(mediaFiles.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("экспортирует внешние ссылки изображений PPTX", async () => {
|
|
const blob = await exportPptxBlob({
|
|
title: "Кликабельное изображение.pptx",
|
|
slides: [
|
|
{
|
|
id: "slide-1",
|
|
title: "Слайд с кликабельным изображением",
|
|
subtitle: "Проверка ссылки",
|
|
notes: "",
|
|
theme: "classic",
|
|
images: [
|
|
{
|
|
id: "image-1",
|
|
src: `data:image/png;base64,${tinyPngBase64}`,
|
|
alt: "Логотип",
|
|
hyperlink: "https://example.com/logo",
|
|
x: 1,
|
|
y: 2,
|
|
width: 3,
|
|
height: 1.5
|
|
}
|
|
]
|
|
}
|
|
]
|
|
});
|
|
const zip = await JSZip.loadAsync(await blob.arrayBuffer());
|
|
const slideXml = (await zip.file("ppt/slides/slide1.xml")?.async("string")) ?? "";
|
|
const relationshipsXml = (await zip.file("ppt/slides/_rels/slide1.xml.rels")?.async("string")) ?? "";
|
|
const reimported = await importPptxFile(new File([blob], "Кликабельное изображение.pptx", { type: blob.type }));
|
|
|
|
expect(slideXml).toContain("hlinkClick");
|
|
expect(relationshipsXml).toContain('Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"');
|
|
expect(relationshipsXml).toContain('Target="https://example.com/logo"');
|
|
expect(reimported.slides[0].images?.[0]).toMatchObject({ hyperlink: "https://example.com/logo" });
|
|
});
|
|
|
|
it("экспортирует заметки докладчика в PPTX notesSlide relationships", async () => {
|
|
const blob = await exportPptxBlob({
|
|
title: "Заметки.pptx",
|
|
slides: [
|
|
{
|
|
id: "slide-1",
|
|
title: "Слайд с заметками",
|
|
subtitle: "Проверка экспорта",
|
|
notes: "Первый тезис\nВторой тезис",
|
|
theme: "classic"
|
|
}
|
|
]
|
|
});
|
|
const zip = await JSZip.loadAsync(await blob.arrayBuffer());
|
|
const relationshipsXml = await zip.file("ppt/slides/_rels/slide1.xml.rels")?.async("string");
|
|
const notesPath = Object.keys(zip.files).find((path) => path.startsWith("ppt/notesSlides/notesSlide") && path.endsWith(".xml"));
|
|
const notesXml = notesPath ? await zip.file(notesPath)?.async("string") : "";
|
|
|
|
expect(relationshipsXml).toContain('Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide"');
|
|
expect(notesXml).toContain("Первый тезис");
|
|
expect(notesXml).toContain("Второй тезис");
|
|
});
|
|
|
|
it("импортирует и экспортирует пользовательский цвет фона слайда", async () => {
|
|
const zip = new JSZip();
|
|
zip.file(
|
|
"ppt/presentation.xml",
|
|
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`
|
|
);
|
|
zip.file(
|
|
"ppt/_rels/presentation.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>`
|
|
);
|
|
zip.file("ppt/slides/slide1.xml", slideXmlWithBackground("Фон слайда", "FCE4D6"));
|
|
|
|
const importedBlob = await zip.generateAsync({
|
|
type: "blob",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
});
|
|
const imported = await importPptxFile(new File([importedBlob], "Фон.pptx", { type: importedBlob.type }));
|
|
|
|
expect(imported.slides[0].backgroundColor).toBe("FCE4D6");
|
|
|
|
const exportedBlob = await exportPptxBlob({
|
|
title: "Фон.pptx",
|
|
slides: [
|
|
{
|
|
id: "slide-1",
|
|
title: "Фон слайда",
|
|
subtitle: "",
|
|
notes: "",
|
|
theme: "classic",
|
|
backgroundColor: "FCE4D6"
|
|
}
|
|
]
|
|
});
|
|
const exportedZip = await JSZip.loadAsync(await exportedBlob.arrayBuffer());
|
|
const exportedSlideXml = await exportedZip.file("ppt/slides/slide1.xml")?.async("string");
|
|
|
|
expect(exportedSlideXml).toContain("FCE4D6");
|
|
});
|
|
|
|
it("импортирует и экспортирует скрытые слайды PPTX через атрибут show", async () => {
|
|
const zip = new JSZip();
|
|
zip.file(
|
|
"ppt/presentation.xml",
|
|
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`
|
|
);
|
|
zip.file(
|
|
"ppt/_rels/presentation.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>`
|
|
);
|
|
zip.file("ppt/slides/slide1.xml", slideXmlWithShow("Скрытый слайд", "0"));
|
|
|
|
const importedBlob = await zip.generateAsync({
|
|
type: "blob",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
});
|
|
const imported = await importPptxFile(new File([importedBlob], "Скрытый.pptx", { type: importedBlob.type }));
|
|
|
|
expect(imported.slides[0]).toMatchObject({ title: "Скрытый слайд", hidden: true });
|
|
|
|
const exportedBlob = await exportPptxBlob({
|
|
title: "Скрытый.pptx",
|
|
slides: [
|
|
{
|
|
id: "slide-1",
|
|
title: "Скрытый слайд",
|
|
subtitle: "",
|
|
notes: "",
|
|
theme: "classic",
|
|
hidden: true
|
|
}
|
|
]
|
|
});
|
|
const exportedZip = await JSZip.loadAsync(await exportedBlob.arrayBuffer());
|
|
const exportedSlideXml = (await exportedZip.file("ppt/slides/slide1.xml")?.async("string")) ?? "";
|
|
const reimported = await importPptxFile(new File([exportedBlob], "Скрытый.pptx", { type: exportedBlob.type }));
|
|
|
|
expect(exportedSlideXml).toContain('show="0"');
|
|
expect(reimported.slides[0].hidden).toBe(true);
|
|
});
|
|
|
|
it("импортирует и экспортирует переходы слайдов PPTX через p:transition", async () => {
|
|
const zip = new JSZip();
|
|
zip.file(
|
|
"ppt/presentation.xml",
|
|
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`
|
|
);
|
|
zip.file(
|
|
"ppt/_rels/presentation.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>`
|
|
);
|
|
zip.file("ppt/slides/slide1.xml", slideXmlWithTransition("Слайд с переходом", "fade"));
|
|
|
|
const importedBlob = await zip.generateAsync({
|
|
type: "blob",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
});
|
|
const imported = await importPptxFile(new File([importedBlob], "Переход.pptx", { type: importedBlob.type }));
|
|
|
|
expect(imported.slides[0]).toMatchObject({ title: "Слайд с переходом", transition: "fade" });
|
|
|
|
const exportedBlob = await exportPptxBlob({
|
|
title: "Переход.pptx",
|
|
slides: [
|
|
{
|
|
id: "slide-1",
|
|
title: "Слайд с переходом",
|
|
subtitle: "",
|
|
notes: "",
|
|
theme: "classic",
|
|
transition: "wipe"
|
|
}
|
|
]
|
|
});
|
|
const exportedZip = await JSZip.loadAsync(await exportedBlob.arrayBuffer());
|
|
const exportedSlideXml = (await exportedZip.file("ppt/slides/slide1.xml")?.async("string")) ?? "";
|
|
const reimportedTransition = await importPptxFile(new File([exportedBlob], "Переход.pptx", { type: exportedBlob.type }));
|
|
|
|
expect(exportedSlideXml).toContain("<p:transition><p:wipe/></p:transition>");
|
|
expect(reimportedTransition.slides[0].transition).toBe("wipe");
|
|
});
|
|
|
|
it("импортирует и экспортирует цвет и размер текста слайда", async () => {
|
|
const zip = new JSZip();
|
|
zip.file(
|
|
"ppt/presentation.xml",
|
|
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`
|
|
);
|
|
zip.file(
|
|
"ppt/_rels/presentation.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>`
|
|
);
|
|
zip.file("ppt/slides/slide1.xml", slideXmlWithTextStyles("Заголовок", "Подзаголовок"));
|
|
|
|
const importedBlob = await zip.generateAsync({
|
|
type: "blob",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
});
|
|
const imported = await importPptxFile(new File([importedBlob], "Текст.pptx", { type: importedBlob.type }));
|
|
|
|
expect(imported.slides[0]).toMatchObject({
|
|
title: "Заголовок",
|
|
subtitle: "Подзаголовок",
|
|
titleColor: "C2410C",
|
|
titleFontSize: 34,
|
|
titleFontFamily: "Courier New",
|
|
subtitleColor: "0F766E",
|
|
subtitleFontSize: 22,
|
|
subtitleFontFamily: "Times New Roman"
|
|
});
|
|
|
|
const exportedBlob = await exportPptxBlob({
|
|
title: "Текст.pptx",
|
|
slides: [
|
|
{
|
|
id: "slide-1",
|
|
title: "Заголовок",
|
|
subtitle: "Подзаголовок",
|
|
notes: "",
|
|
theme: "classic",
|
|
titleColor: "C2410C",
|
|
titleFontSize: 34,
|
|
titleFontFamily: "Courier New",
|
|
subtitleColor: "0F766E",
|
|
subtitleFontSize: 22,
|
|
subtitleFontFamily: "Times New Roman"
|
|
}
|
|
]
|
|
});
|
|
const exportedZip = await JSZip.loadAsync(await exportedBlob.arrayBuffer());
|
|
const exportedSlideXml = await exportedZip.file("ppt/slides/slide1.xml")?.async("string");
|
|
|
|
expect(exportedSlideXml).toContain("C2410C");
|
|
expect(exportedSlideXml).toContain("0F766E");
|
|
expect(exportedSlideXml).toContain('sz="3400"');
|
|
expect(exportedSlideXml).toContain('sz="2200"');
|
|
expect(exportedSlideXml).toContain('typeface="Courier New"');
|
|
expect(exportedSlideXml).toContain('typeface="Times New Roman"');
|
|
});
|
|
|
|
it("импортирует и экспортирует начертание текста слайда PPTX", async () => {
|
|
const zip = new JSZip();
|
|
zip.file(
|
|
"ppt/presentation.xml",
|
|
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`
|
|
);
|
|
zip.file(
|
|
"ppt/_rels/presentation.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>`
|
|
);
|
|
zip.file("ppt/slides/slide1.xml", slideXmlWithTextRunStyles("Заголовок", "Подзаголовок"));
|
|
|
|
const importedBlob = await zip.generateAsync({
|
|
type: "blob",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
});
|
|
const imported = await importPptxFile(new File([importedBlob], "Начертание.pptx", { type: importedBlob.type }));
|
|
|
|
expect(imported.slides[0]).toMatchObject({
|
|
titleBold: true,
|
|
titleItalics: true,
|
|
titleUnderline: true,
|
|
titleStrike: true,
|
|
subtitleBold: true,
|
|
subtitleItalics: true,
|
|
subtitleUnderline: true,
|
|
subtitleStrike: true
|
|
});
|
|
|
|
const exportedBlob = await exportPptxBlob({
|
|
title: "Начертание.pptx",
|
|
slides: [
|
|
{
|
|
id: "slide-1",
|
|
title: "Заголовок",
|
|
subtitle: "Подзаголовок",
|
|
notes: "",
|
|
theme: "classic",
|
|
titleBold: false,
|
|
titleItalics: true,
|
|
titleUnderline: true,
|
|
titleStrike: true,
|
|
subtitleBold: true,
|
|
subtitleItalics: true,
|
|
subtitleUnderline: true,
|
|
subtitleStrike: true
|
|
}
|
|
]
|
|
});
|
|
const exportedZip = await JSZip.loadAsync(await exportedBlob.arrayBuffer());
|
|
const exportedSlideXml = (await exportedZip.file("ppt/slides/slide1.xml")?.async("string")) ?? "";
|
|
|
|
expect(exportedSlideXml).toContain('i="1"');
|
|
expect(exportedSlideXml).toContain('u="sng"');
|
|
expect(exportedSlideXml).toContain('strike="sngStrike"');
|
|
expect(exportedSlideXml).toContain('b="1"');
|
|
|
|
const reimported = await importPptxFile(new File([exportedBlob], "Начертание.pptx", { type: exportedBlob.type }));
|
|
expect(reimported.slides[0]).toMatchObject({
|
|
titleBold: false,
|
|
titleItalics: true,
|
|
titleUnderline: true,
|
|
titleStrike: true,
|
|
subtitleBold: true,
|
|
subtitleItalics: true,
|
|
subtitleUnderline: true,
|
|
subtitleStrike: true
|
|
});
|
|
});
|
|
|
|
it("импортирует внешние ссылки текста PPTX через hlinkClick relationships", async () => {
|
|
const zip = new JSZip();
|
|
zip.file(
|
|
"ppt/presentation.xml",
|
|
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`
|
|
);
|
|
zip.file(
|
|
"ppt/_rels/presentation.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>`
|
|
);
|
|
zip.file("ppt/slides/slide1.xml", slideXmlWithTextHyperlinks("Roadmap", "Open report"));
|
|
zip.file(
|
|
"ppt/slides/_rels/slide1.xml.rels",
|
|
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rIdTitleLink" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="https://example.com/title" TargetMode="External"/><Relationship Id="rIdSubtitleLink" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="mailto:team@example.com" TargetMode="External"/></Relationships>`
|
|
);
|
|
|
|
const blob = await zip.generateAsync({
|
|
type: "blob",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
});
|
|
const imported = await importPptxFile(new File([blob], "Links.pptx", { type: blob.type }));
|
|
|
|
expect(imported.slides[0]).toMatchObject({
|
|
title: "Roadmap",
|
|
titleHyperlink: "https://example.com/title",
|
|
subtitle: "Open report",
|
|
subtitleHyperlink: "mailto:team@example.com"
|
|
});
|
|
});
|
|
|
|
it("экспортирует внешние ссылки заголовка и подзаголовка PPTX", async () => {
|
|
const blob = await exportPptxBlob({
|
|
title: "Links.pptx",
|
|
slides: [
|
|
{
|
|
id: "slide-1",
|
|
title: "Roadmap",
|
|
titleHyperlink: "https://example.com/title",
|
|
subtitle: "Open report",
|
|
subtitleHyperlink: "mailto:team@example.com",
|
|
notes: "",
|
|
theme: "classic"
|
|
}
|
|
]
|
|
});
|
|
const zip = await JSZip.loadAsync(await blob.arrayBuffer());
|
|
const slideXml = (await zip.file("ppt/slides/slide1.xml")?.async("string")) ?? "";
|
|
const relationshipsXml = (await zip.file("ppt/slides/_rels/slide1.xml.rels")?.async("string")) ?? "";
|
|
const reimported = await importPptxFile(new File([blob], "Links.pptx", { type: blob.type }));
|
|
|
|
expect(slideXml).toContain("hlinkClick");
|
|
expect(relationshipsXml).toContain('Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"');
|
|
expect(relationshipsXml).toContain('Target="https://example.com/title"');
|
|
expect(relationshipsXml).toContain('Target="mailto:team@example.com"');
|
|
expect(reimported.slides[0]).toMatchObject({
|
|
titleHyperlink: "https://example.com/title",
|
|
subtitleHyperlink: "mailto:team@example.com"
|
|
});
|
|
});
|
|
|
|
it("нормализует пробелы в импортированном тексте", () => {
|
|
expect(normalizeWhitespace(" Строка\tс \n пробелами ")).toBe("Строка с пробелами");
|
|
});
|
|
});
|
|
|
|
function slideXml(...texts: string[]) {
|
|
return `<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><p:cSld><p:spTree>${texts
|
|
.map((text) => `<p:sp><p:txBody><a:p><a:r><a:t>${text}</a:t></a:r></a:p></p:txBody></p:sp>`)
|
|
.join("")}</p:spTree></p:cSld></p:sld>`;
|
|
}
|
|
|
|
function slideXmlWithBackground(title: string, color: string) {
|
|
return `<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><p:cSld><p:bg><p:bgPr><a:solidFill><a:srgbClr val="${color}"/></a:solidFill></p:bgPr></p:bg><p:spTree><p:sp><p:txBody><a:p><a:r><a:t>${title}</a:t></a:r></a:p></p:txBody></p:sp></p:spTree></p:cSld></p:sld>`;
|
|
}
|
|
|
|
function slideXmlWithShow(title: string, show: string) {
|
|
return `<p:sld show="${show}" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><p:cSld><p:spTree><p:sp><p:txBody><a:p><a:r><a:t>${title}</a:t></a:r></a:p></p:txBody></p:sp></p:spTree></p:cSld></p:sld>`;
|
|
}
|
|
|
|
function slideXmlWithTransition(title: string, transition: "fade" | "push" | "wipe") {
|
|
return `<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><p:cSld><p:spTree><p:sp><p:txBody><a:p><a:r><a:t>${title}</a:t></a:r></a:p></p:txBody></p:sp></p:spTree></p:cSld><p:transition><p:${transition}/></p:transition></p:sld>`;
|
|
}
|
|
|
|
function slideXmlWithTextStyles(title: string, subtitle: string) {
|
|
return `<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><p:cSld><p:spTree><p:sp><p:txBody><a:p><a:r><a:rPr sz="3400"><a:solidFill><a:srgbClr val="C2410C"/></a:solidFill><a:latin typeface="Courier New"/></a:rPr><a:t>${title}</a:t></a:r></a:p></p:txBody></p:sp><p:sp><p:txBody><a:p><a:r><a:rPr sz="2200"><a:solidFill><a:srgbClr val="0F766E"/></a:solidFill><a:latin typeface="Times New Roman"/></a:rPr><a:t>${subtitle}</a:t></a:r></a:p></p:txBody></p:sp></p:spTree></p:cSld></p:sld>`;
|
|
}
|
|
|
|
function slideXmlWithTextRunStyles(title: string, subtitle: string) {
|
|
return `<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><p:cSld><p:spTree><p:sp><p:txBody><a:p><a:r><a:rPr b="1" i="1" u="sng" strike="sngStrike"/><a:t>${title}</a:t></a:r></a:p></p:txBody></p:sp><p:sp><p:txBody><a:p><a:r><a:rPr b="1" i="1" u="sng" strike="sngStrike"/><a:t>${subtitle}</a:t></a:r></a:p></p:txBody></p:sp></p:spTree></p:cSld></p:sld>`;
|
|
}
|
|
|
|
function slideXmlWithTextHyperlinks(title: string, subtitle: string) {
|
|
return `<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:cSld><p:spTree><p:sp><p:txBody><a:p><a:r><a:rPr><a:hlinkClick r:id="rIdTitleLink"/></a:rPr><a:t>${title}</a:t></a:r></a:p></p:txBody></p:sp><p:sp><p:txBody><a:p><a:r><a:rPr><a:hlinkClick r:id="rIdSubtitleLink"/></a:rPr><a:t>${subtitle}</a:t></a:r></a:p></p:txBody></p:sp></p:spTree></p:cSld></p:sld>`;
|
|
}
|
|
|
|
function slideXmlWithMultilineSubtitle(title: string, subtitleParagraphs: string[]) {
|
|
return `<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><p:cSld><p:spTree><p:sp><p:txBody><a:p><a:r><a:t>${title}</a:t></a:r></a:p></p:txBody></p:sp><p:sp><p:txBody>${subtitleParagraphs
|
|
.map((text) => `<a:p><a:r><a:t>${text}</a:t></a:r></a:p>`)
|
|
.join("")}</p:txBody></p:sp></p:spTree></p:cSld></p:sld>`;
|
|
}
|
|
|
|
function slideXmlWithManualBreakSubtitle(title: string, subtitleLines: string[]) {
|
|
return `<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><p:cSld><p:spTree><p:sp><p:txBody><a:p><a:r><a:t>${title}</a:t></a:r></a:p></p:txBody></p:sp><p:sp><p:txBody><a:p>${subtitleLines
|
|
.map((text, index) => `${index > 0 ? "<a:br/>" : ""}<a:r><a:t>${text}</a:t></a:r>`)
|
|
.join("")}</a:p></p:txBody></p:sp></p:spTree></p:cSld></p:sld>`;
|
|
}
|
|
|
|
function slideXmlWithListSubtitle(title: string, subtitleLines: string[], listStyle: "bullet" | "number") {
|
|
const paragraphProperties =
|
|
listStyle === "number" ? '<a:pPr><a:buAutoNum type="arabicPeriod"/></a:pPr>' : '<a:pPr><a:buChar char="•"/></a:pPr>';
|
|
return `<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><p:cSld><p:spTree><p:sp><p:txBody><a:p><a:r><a:t>${title}</a:t></a:r></a:p></p:txBody></p:sp><p:sp><p:txBody>${subtitleLines
|
|
.map((text) => `<a:p>${paragraphProperties}<a:r><a:t>${text}</a:t></a:r></a:p>`)
|
|
.join("")}</p:txBody></p:sp></p:spTree></p:cSld></p:sld>`;
|
|
}
|
|
|
|
function slideXmlWithPicture(title: string) {
|
|
return `<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:cSld><p:spTree><p:sp><p:txBody><a:p><a:r><a:t>${title}</a:t></a:r></a:p></p:txBody></p:sp><p:pic><p:nvPicPr><p:cNvPr id="4" name="Picture 1" descr="Логотип"/></p:nvPicPr><p:blipFill><a:blip r:embed="rIdImage"/></p:blipFill><p:spPr><a:xfrm><a:off x="914400" y="1828800"/><a:ext cx="2743200" cy="1371600"/></a:xfrm></p:spPr></p:pic></p:spTree></p:cSld></p:sld>`;
|
|
}
|
|
|
|
function slideXmlWithLinkedPicture(title: string) {
|
|
return `<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:cSld><p:spTree><p:sp><p:txBody><a:p><a:r><a:t>${title}</a:t></a:r></a:p></p:txBody></p:sp><p:pic><p:nvPicPr><p:cNvPr id="4" name="Picture 1" descr="Логотип"><a:hlinkClick r:id="rIdImageLink"/></p:cNvPr></p:nvPicPr><p:blipFill><a:blip r:embed="rIdImage"/></p:blipFill><p:spPr><a:xfrm><a:off x="914400" y="1828800"/><a:ext cx="2743200" cy="1371600"/></a:xfrm></p:spPr></p:pic></p:spTree></p:cSld></p:sld>`;
|
|
}
|