Files
edentext/tests/unit/vertical-text-box.test.ts
Steffen Becker 060dcd7380 feat: a text box sits in the line of text
textBox becomes an inline node carrying block content, like the frame both
formats write, so "in line with text" really is in the line and a box reaches
a table cell or a list item through their paragraphs. Both exports write it
where it sits (an as-char draw:frame / a wp:inline drawing) and both imports
leave it there, which drops the hoisting machinery on either side.

Two guards the schema cannot express: no box inside a box, and a selection
reaching into a box grows to the box's bounds rather than dissolving it.
2026-09-04 19:30:01 +02:00

61 lines
2.9 KiB
TypeScript

// A text box whose text runs top-to-bottom. ODF keeps it as the style's own writing
// mode — a text frame in its graphic properties (where LibreOffice writes it), a
// drawing shape in its style's paragraph properties (probed both) — Word as `vert`.
import { describe, it, expect } from 'vitest';
import { unzipSync, strFromU8 } from 'fflate';
import { buildOdt } from '../../src/lib/export/odt';
import { buildDocx } from '../../src/lib/export/docx';
import { importOdt } from '../../src/lib/import/odt';
import { importDocx } from '../../src/lib/import/docx';
type N = any;
const box = (attrs: N): N => ({
type: 'textBox',
attrs: { width: 200, height: 160, ...attrs },
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Senkrecht' }] }],
});
const doc: N = {
type: 'doc',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'davor' }] },
{ type: 'paragraph', content: [box({ textVertical: true })] }],
};
const margins = { top: 2, bottom: 2, left: 2, right: 2 };
const boxOf = (r: N): N => {
const find = (n: N): N => n?.type === 'textBox' ? n : (n?.content ?? []).map(find).find(Boolean);
return find((r.content as N));
};
describe('a text box with vertical text', () => {
it('round-trips through ODF', async () => {
const bytes = await buildOdt(doc, margins, 'portrait');
const xml = strFromU8(unzipSync(bytes)['content.xml']);
expect(xml).toMatch(/<style:graphic-properties [^>]*style:writing-mode="tb-rl"/);
expect(boxOf(importOdt(bytes))?.attrs.textVertical).toBe(true);
});
it('keeps the writing mode of a shape in its paragraph properties', async () => {
const shaped: N = { ...doc, content: [doc.content[0], { type: 'paragraph', content: [box({ textVertical: true, shapeKind: 'roundRect' })] }] };
const xml = strFromU8(unzipSync(await buildOdt(shaped, margins, 'portrait'))['content.xml']);
expect(xml).toContain('<style:paragraph-properties style:writing-mode="tb-rl"/>');
expect(boxOf(importOdt(await buildOdt(shaped, margins, 'portrait')))?.attrs.textVertical).toBe(true);
});
it('round-trips through DOCX', async () => {
const bytes = await buildDocx(doc, margins, 'portrait');
const xml = strFromU8(unzipSync(bytes)['word/document.xml']);
expect(xml).toContain('vert="vert"');
expect(boxOf(importDocx(bytes))?.attrs.textVertical).toBe(true);
});
it('leaves a horizontal box alone', async () => {
const flat: N = { ...doc, content: [doc.content[0], { type: 'paragraph', content: [box({})] }] };
const odt = strFromU8(unzipSync(await buildOdt(flat, margins, 'portrait'))['content.xml']);
expect(odt).not.toContain('writing-mode="tb-rl"');
const docx = strFromU8(unzipSync(await buildDocx(flat, margins, 'portrait'))['word/document.xml']);
expect(docx).toContain('vert="horz"');
expect(boxOf(importOdt(await buildOdt(flat, margins, 'portrait')))?.attrs.textVertical).toBeUndefined();
});
});