The default tab interval was fixed at 1.25cm, so a file's own w:defaultTabStop / style:tab-stop-distance was ignored and the DOCX export wrote 1.25 back over Word's 1.27. It is a document setting now. A stop can also carry the character it repeats across its gap. odf-kit has no leader option and mints one automatic style per option set, so the leader rides style:type behind a sentinel — matching on position and alignment alone gave every stop at 6cm the same leader, which LibreOffice drew back plain as day.
69 lines
2.8 KiB
TypeScript
69 lines
2.8 KiB
TypeScript
// Tab stops: positions are cm from the left text margin in both formats (probe: a Word
|
|
// stop at 100mm in a 30mm-indented paragraph stays 100mm in ODF and renders there), so
|
|
// neither side shifts them by the paragraph indent.
|
|
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';
|
|
import { parseTabStops, formatTabStops } from '../../src/lib/editor/extensions/tabStops';
|
|
|
|
type N = any;
|
|
|
|
const P = (text: string, attrs?: N): N =>
|
|
({ type: 'paragraph', ...(attrs ? { attrs } : {}), content: [{ type: 'text', text }] });
|
|
|
|
const doc: N = {
|
|
type: 'doc',
|
|
content: [
|
|
P('plain'),
|
|
P('left\tcentred\tright\t12.34', { tabStops: '6c;12r;16d' }),
|
|
P('indented\tright', { indent: 3, tabStops: '10r' }),
|
|
],
|
|
};
|
|
|
|
const margins = { top: 2, bottom: 2, left: 2, right: 2 };
|
|
const stopsOf = (d: N, i: number) => d.content[i].attrs?.tabStops ?? null;
|
|
|
|
describe('tab stops', () => {
|
|
it('parses and formats the canonical attr form', () => {
|
|
expect(parseTabStops('6c;12r;16d')).toEqual([
|
|
{ pos: 6, align: 'center', leader: null },
|
|
{ pos: 12, align: 'right', leader: null },
|
|
{ pos: 16, align: 'decimal', leader: null },
|
|
]);
|
|
// Sorted, deduped by position, rounded to 0.01cm; empty becomes null.
|
|
expect(formatTabStops([{ pos: 12, align: 'right' }, { pos: 1.004, align: 'left' }])).toBe('1l;12r');
|
|
expect(formatTabStops([])).toBe(null);
|
|
expect(parseTabStops(null)).toEqual([]);
|
|
});
|
|
|
|
it('round-trips through ODF', async () => {
|
|
const bytes = await buildOdt(doc, margins, 'portrait');
|
|
const xml = strFromU8(unzipSync(bytes)['content.xml']);
|
|
expect(xml).toMatch(/<style:tab-stop style:position="6cm" style:type="center"\/>/);
|
|
// ODF's decimal stop is style:type="char" plus the separator itself.
|
|
expect(xml).toMatch(/style:position="16cm" style:type="char" style:char="\."/);
|
|
|
|
const back = importOdt(bytes).content as N;
|
|
expect(stopsOf(back, 0)).toBe(null);
|
|
expect(stopsOf(back, 1)).toBe('6c;12r;16d');
|
|
expect(stopsOf(back, 2)).toBe('10r');
|
|
});
|
|
|
|
it('round-trips through DOCX', async () => {
|
|
const bytes = await buildDocx(doc, margins, 'portrait');
|
|
const xml = strFromU8(unzipSync(bytes)['word/document.xml']);
|
|
expect(xml).toMatch(/<w:tab w:val="center" w:pos="3402"\/>/);
|
|
expect(xml).toMatch(/<w:tab w:val="decimal" w:pos="9071"\/>/);
|
|
// The indented paragraph's stop keeps its margin-relative position.
|
|
expect(xml).toMatch(/<w:tab w:val="right" w:pos="5669"\/>/);
|
|
|
|
const back = importDocx(bytes).content as N;
|
|
expect(stopsOf(back, 0)).toBe(null);
|
|
expect(stopsOf(back, 1)).toBe('6c;12r;16d');
|
|
expect(stopsOf(back, 2)).toBe('10r');
|
|
});
|
|
});
|