CSS only has the fixed tab-size grid, so a tab that resolves to one of the paragraph's stops is measured and given the exact advance as an inline margin. A hanging indent implies a stop at the text position, as in Word.
67 lines
2.8 KiB
TypeScript
67 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' }, { pos: 12, align: 'right' }, { pos: 16, align: 'decimal' },
|
|
]);
|
|
// 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');
|
|
});
|
|
});
|