// A drawing the editor cannot author but must keep: a polygon, a polyline, a bezier
// curve and a connector's elbow. Each imports as a box drawing the file's own outline,
// and both exports write that outline back.
import { describe, it, expect } from 'vitest';
import { zipSync, strToU8, unzipSync, strFromU8 } from 'fflate';
import { buildOdt } from '../../src/lib/export/odt';
import { importOdt } from '../../src/lib/import/odt';
import { buildDocx } from '../../src/lib/export/docx';
import { importDocx } from '../../src/lib/import/docx';
type N = any;
const NS = 'xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"'
+ ' xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"'
+ ' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"'
+ ' xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"'
+ ' xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"'
+ ' xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"';
function odt(body: string): Uint8Array {
const content = ``
+ ''
+ ''
+ `${body}`;
return zipSync({
'mimetype': strToU8('application/vnd.oasis.opendocument.text'),
'content.xml': strToU8(content),
'styles.xml': strToU8(``),
});
}
const W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
const VML = 'urn:schemas-microsoft-com:vml';
const docx = (body: string): Uint8Array => zipSync({
'word/document.xml': strToU8(``
+ `${body}`),
});
const shape = (r: N): N => {
const find = (n: N): N => n?.type === 'textBox' ? n : (n?.content ?? []).map(find).find(Boolean);
return find((r.content as N));
};
const margins = { top: 2, bottom: 2, left: 2, right: 2 };
describe('a freeform drawing', () => {
it('reads a polygon as its own closed outline', () => {
const r = importOdt(odt('x'));
expect(shape(r).attrs.shapePath).toBe('M 0 0 L 100 33.333 L 50 100 Z');
expect(shape(r).attrs.fillColor).toBe('#FFD320');
});
it('leaves a polyline open, so it is stroked and not filled', () => {
const r = importOdt(odt(''));
expect(shape(r).attrs.shapePath).toBe('M 0 0 L 25 100 L 100 50');
});
it('reads the relative bezier LibreOffice writes for a curve', () => {
const r = importOdt(odt(''));
expect(shape(r).attrs.shapePath).toBe('M 0 0 C 25 0 75 100 100 50 L 0 100 Z');
});
it("takes a connector's elbow, and the box its endpoints span", () => {
const r = importOdt(odt(''));
const a = shape(r).attrs;
expect(a.shapePath).toBe('M 20 0 L 70 0 L 70 100 L 120 100');
// 5cm × 2cm at 96dpi.
expect(a.width).toBe(189);
expect(a.height).toBe(75.59);
});
it('round-trips through ODF as a non-primitive custom shape', async () => {
const doc: N = {
type: 'doc',
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'before' }] },
{ type: 'paragraph', content: [{
type: 'textBox',
attrs: { width: 200, height: 150, shapePath: 'M 0 0 C 25 0 75 100 100 50 L 0 100 Z' },
content: [{ type: 'paragraph' }],
}] },
],
};
const bytes = await buildOdt(doc, margins, 'portrait');
const xml = strFromU8(unzipSync(bytes)['content.xml']);
expect(xml).toContain('draw:type="non-primitive"');
expect(xml).toContain('draw:enhanced-path="M 0 0 C 5400 0 16200 21600 21600 10800 L 0 21600 Z N"');
expect(shape(importOdt(bytes)).attrs.shapePath).toBe('M 0 0 C 25 0 75 100 100 50 L 0 100 Z');
});
it('round-trips through DOCX as a custGeom', async () => {
const doc: N = {
type: 'doc',
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'before' }] },
{ type: 'paragraph', content: [{
type: 'textBox',
attrs: { width: 200, height: 100, shapePath: 'M 0 0 L 100 50 L 0 100 Z' },
content: [{ type: 'paragraph' }],
}] },
],
};
const bytes = await buildDocx(doc, margins, 'portrait');
const xml = strFromU8(unzipSync(bytes)['word/document.xml']);
expect(xml).toContain('');
expect(shape(importDocx(bytes)).attrs.shapePath).toBe('M 0 0 L 100 50 L 0 100 Z');
});
it('reads the VML path LibreOffice writes into a .docx', () => {
const body = ''
+ ''
+ '';
const r = importDocx(docx(body));
expect(shape(r).attrs.shapePath).toBe('M 0 0 L 100 33.333 L 50 100 L 0 0');
expect(shape(r).attrs.fillColor).toBe('#FFD320');
});
it('still drops a shape whose geometry is a formula', () => {
const r = importOdt(odt(''));
expect(shape(r)).toBeUndefined();
expect(r.warnings.some((w: string) => /shapes/i.test(w))).toBe(true);
});
});