Files
Steffen Becker 6957f5682b fix: a style kerns where its own w:kern says so, not only the document's
Word kerns nothing unless w:kern names a size to start at, and the Office
theme declares it on Title alone — but TextProps.kerning stored only the
off state, and a style inherits its parent's, so a title under a document
that kerns nothing could never turn it back on. Its 26pt line then ran
0.36mm over the text width and wrapped a word onto a second line where
LibreOffice sets it in one (measured 151.64mm kerned against 151.98).
Both states are stored now, and both formats read and write them.
2026-08-13 19:15:28 +02:00

80 lines
3.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import { zipSync, strToU8 } from 'fflate';
import { styleCss, builtinStyleSheet, resolveStyle } from '../../src/lib/styles/styleSheet';
import { importDocx } from '../../src/lib/import/docx';
import { buildDocx } from '../../src/lib/export/docx';
// ODF kerns by default and Word does not, so only the off state is ever stored.
describe('pair kerning', () => {
it('renders font-kerning only where a style turns it off', () => {
const sheet = builtinStyleSheet();
expect(styleCss(sheet)).not.toContain('font-kerning');
sheet.paragraph.Standard.text = { ...sheet.paragraph.Standard.text, kerning: false };
expect(styleCss(sheet)).toContain('font-kerning: none');
});
it('inherits the off state down the parent chain', () => {
const sheet = builtinStyleSheet();
sheet.paragraph.Standard.text = { ...sheet.paragraph.Standard.text, kerning: false };
const css = styleCss(sheet);
const heading = css.slice(css.indexOf('[data-style="Heading 1"]'));
expect(heading.slice(0, heading.indexOf('}'))).toContain('font-kerning: none');
});
it('lets a style turn it back on under one that turns it off', () => {
const sheet = builtinStyleSheet();
sheet.paragraph.Standard.text = { ...sheet.paragraph.Standard.text, kerning: false };
sheet.paragraph.Title.text = { ...sheet.paragraph.Title.text, kerning: true };
const css = styleCss(sheet);
const title = css.slice(css.indexOf('[data-style="Title"]'));
expect(title.slice(0, title.indexOf('}'))).toContain('font-kerning: normal');
});
});
// w:kern names the smallest size pair kerning applies to. A file that declares it on the
// Title style alone kerns the title and nothing else — LibreOffice sets that title 0.34mm
// narrower, which is a line break where the text fills its column.
const W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
const STYLES = `<?xml version="1.0"?>
<w:styles xmlns:w="${W}">
<w:docDefaults><w:rPrDefault><w:rPr><w:sz w:val="22"/></w:rPr></w:rPrDefault></w:docDefaults>
<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/></w:style>
<w:style w:type="paragraph" w:styleId="Title"><w:name w:val="Title"/><w:basedOn w:val="Normal"/>
<w:rPr><w:kern w:val="28"/><w:sz w:val="52"/></w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Small"><w:name w:val="Small"/><w:basedOn w:val="Normal"/>
<w:rPr><w:kern w:val="28"/><w:sz w:val="20"/></w:rPr>
</w:style>
</w:styles>`;
const docx = () =>
zipSync({
'word/document.xml': strToU8(`<?xml version="1.0"?><w:document xmlns:w="${W}"><w:body>` +
`<w:p><w:pPr><w:pStyle w:val="Title"/></w:pPr><w:r><w:t>Title</w:t></w:r></w:p>` +
`<w:p><w:pPr><w:pStyle w:val="Small"/></w:pPr><w:r><w:t>small</w:t></w:r></w:p>` +
`<w:p><w:r><w:t>body</w:t></w:r></w:p></w:body></w:document>`),
'word/styles.xml': strToU8(STYLES),
});
describe('a Word style’s own w:kern', () => {
const sheet = importDocx(docx()).styles!;
it('kerns a style set at or above the size it names', () => {
expect(sheet.paragraph.Title.text.kerning).toBe(true);
});
it('leaves the body and a style set below that size unkerned', () => {
expect(sheet.paragraph.Standard.text.kerning).toBe(false);
expect(resolveStyle(sheet, 'Small').text.kerning).toBe(false);
});
it('survives a round trip through the DOCX export', async () => {
const doc = importDocx(docx());
const out = await buildDocx(doc.content as never, undefined, 'portrait', undefined, null, 'A4', doc.styles);
const again = importDocx(out).styles!;
expect(again.paragraph.Title.text.kerning).toBe(true);
expect(again.paragraph.Standard.text.kerning).toBe(false);
});
});