diff --git a/CHANGELOG.md b/CHANGELOG.md index 8322582..8f154fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ - Font family picker (lists only fonts installed on the machine; remembers recently used fonts) - Font size - Font color and highlight color (color picker with custom colors) -- Headings H1–H6 with LibreOffice's sizes (18/16/14/13/12/12 pt, levels 4 and 6 italic), including un-bolding a heading +- Headings H1–H10 with LibreOffice's sizes (18/16/14/13/12/12 pt, levels 4 and 6 italic), including un-bolding a heading. Levels 7–10 continue level 6 at 12pt — probed, LibreOffice writes those styles with no properties of their own and resolves them from its style pool, so there is no file value to follow. HTML stops at `h6`, so they render as unknown elements the editor's CSS makes blocks **Styles** - Named paragraph styles with LibreOffice's inheritance model: a style has a parent, a follow-on style and its own properties; changing a style updates every paragraph using it. Built-ins Default text → Heading → Heading 1–6 / Title / Subtitle / Quote, with LibreOffice's values (18/16/14/13/12/12 pt headings) @@ -152,7 +152,6 @@ The gap against Word/LibreOffice, most valuable first. Reviewed 2026-08-14. - One colour for every tracked change, where both word processors give each author their own — and no reviewing pane listing the revisions the way the comments pane lists comments - Hyphenation's zone and ladder count (`fo:hyphenation-ladder-count`, `w:hyphenationZone`) — CSS exposes neither - Restarting the page numbering **per section** (only the document's own format and start value exist) -- Heading levels 7–10 (Word and LibreOffice go that far; HTML stops at `h6`) - A bibliography's citation style: the list is set the one way ("key: author, title, year"), where both word processors offer APA, MLA, Chicago and the rest, and numbered citations (`[1]`) instead of the source's short name - A section's own **page format** is set only by the file it came from — the ribbon offers the orientation per section, the paper size document-wide. Pages of different widths are left-aligned in the scroll, where both word processors centre each - A sort dialog's second and third key, and its choice of sort type over the detected one; a formula's own number format (LibreOffice's Number Format dialog per cell — the general format is the only one here), and a formula reaching into another table diff --git a/src/lib/export/CLAUDE.md b/src/lib/export/CLAUDE.md index 1ee7f05..207df95 100644 --- a/src/lib/export/CLAUDE.md +++ b/src/lib/export/CLAUDE.md @@ -8,7 +8,11 @@ Frames, tables and pagination have their own deep-dives in `docs/architecture/`. - **Headings** follow LibreOffice too (`HEADING_STYLE_OVERRIDES`): 18 / 16 / 14 / 13 / 12 / 12pt (levels 4 and 6 italic) with the Heading style's 0.423cm/0.212cm margins on every level, bold, and **sans** (`HEADING_FONT` = Arial; on screen the bundled `@font-face` maps it to Liberation Sans, metric-identical, mirroring Liberation Serif → - Times New Roman for the body). Levels 1–6 (`MAX_HEADING_LEVEL` = `HEADING_STYLE_OVERRIDES.length`); + Times New Roman for the body). Levels **7–10** continue level 6 at 12pt (alternating italic): + probed, LibreOffice writes those styles with no properties at all and resolves them from its + own pool, so there is no file value to follow. HTML stops at `h6`, so 7–10 render as unknown + elements that `editor.css` gives `display: block`. + Levels 1–10 (`MAX_HEADING_LEVEL` = `HEADING_STYLE_OVERRIDES.length`); both constants live in `export/odt.ts` and feed the importers, the TOC, the DOCX heading styles, `utils/fontSize.ts`, and `editor.css` (kept in sync by `tests/unit/font-size-display.test.ts`). The importers no longer override a file's own heading formatting: they only suppress values diff --git a/src/lib/export/odt.ts b/src/lib/export/odt.ts index 71892e6..ab942ad 100644 --- a/src/lib/export/odt.ts +++ b/src/lib/export/odt.ts @@ -224,6 +224,12 @@ export const HEADING_STYLE_OVERRIDES: { name: string; fontSize: string; marginTo { name: 'Heading_20_4', fontSize: '13pt', marginTop: '0.423cm', marginBottom: '0.212cm', italic: true }, { name: 'Heading_20_5', fontSize: '12pt', marginTop: '0.423cm', marginBottom: '0.212cm' }, { name: 'Heading_20_6', fontSize: '12pt', marginTop: '0.423cm', marginBottom: '0.212cm', italic: true }, + // 7–10 continue level 6 rather than adding a step of our own: probed, LibreOffice + // writes these styles with no properties at all and resolves them from its own pool. + { name: 'Heading_20_7', fontSize: '12pt', marginTop: '0.423cm', marginBottom: '0.212cm' }, + { name: 'Heading_20_8', fontSize: '12pt', marginTop: '0.423cm', marginBottom: '0.212cm', italic: true }, + { name: 'Heading_20_9', fontSize: '12pt', marginTop: '0.423cm', marginBottom: '0.212cm' }, + { name: 'Heading_20_10', fontSize: '12pt', marginTop: '0.423cm', marginBottom: '0.212cm', italic: true }, ]; // Headings are sans (LibreOffice's Heading style). On screen the bundled 'Arial' diff --git a/src/lib/import/docx.ts b/src/lib/import/docx.ts index 2894da6..c5e6651 100644 --- a/src/lib/import/docx.ts +++ b/src/lib/import/docx.ts @@ -765,7 +765,7 @@ function styleIdOf(ppr: Element | null, ctx: Ctx): string | null { // LibreOffice writes styleId "Standard" with the name "Normal"). function registryName(id: string, wordName: string, isDefault: boolean): string { if (isDefault || id === 'Normal' || /^normal$/i.test(wordName)) return DEFAULT_STYLE; - const heading = /^Heading\s?([1-9])$/i.exec(id) ?? /^heading\s?([1-9])$/i.exec(wordName); + const heading = /^Heading\s?(10|[1-9])$/i.exec(id) ?? /^heading\s?(10|[1-9])$/i.exec(wordName); if (heading) return `Heading ${heading[1]}`; if (/^Title$/i.test(id)) return 'Title'; if (/^Subtitle$/i.test(id)) return 'Subtitle'; @@ -894,7 +894,7 @@ function collectStyleSheet(ctx: Ctx): StyleSheet { para: ownProps(stylePara(ctx, id), stylePara(ctx, parentId)), text: ownProps(styleText(ctx, id), styleText(ctx, parentId)), }; - const level = /^Heading (\d)$/.exec(name); + const level = /^Heading (\d+)$/.exec(name); if (level) style.outlineLevel = Number(level[1]); sheet.paragraph[name] = style; } @@ -1029,7 +1029,7 @@ function headingLevelOf(ppr: Element | null, ctx: Ctx): number | null { const ps = fc(ppr, 'pStyle'); const id = ps ? wVal(ps) : null; if (id) { - const m = /^Heading\s?([1-9])$/i.exec(id); + const m = /^Heading\s?(10|[1-9])$/i.exec(id); if (m) return clamp(parseInt(m[1], 10)); const ol = ctx.styles.styleOutlineLvl(id); if (ol != null && ol >= 0 && ol <= 8) return clamp(ol + 1); diff --git a/src/lib/import/odt.ts b/src/lib/import/odt.ts index 5cb7029..f9cde87 100644 --- a/src/lib/import/odt.ts +++ b/src/lib/import/odt.ts @@ -1366,7 +1366,7 @@ function collectStyleSheet(resolver: StyleResolver, ctx: Ctx): StyleSheet { para: ownProps(paraPropsFromOdf(resolver.paraProps(odfName)), paraPropsFromOdf(def.parent ? resolver.paraProps(def.parent) : {})), text: ownProps(textPropsFromOdf(resolver.paraTextProps(odfName), resolver), textPropsFromOdf(def.parent ? resolver.paraTextProps(def.parent) : {}, resolver)), }; - const level = /^Heading (\d)$/.exec(name); + const level = /^Heading (\d+)$/.exec(name); if (level) style.outlineLevel = Number(level[1]); sheet.paragraph[name] = style; } diff --git a/src/lib/styles/styleSheet.ts b/src/lib/styles/styleSheet.ts index 0a84fc3..fd27395 100644 --- a/src/lib/styles/styleSheet.ts +++ b/src/lib/styles/styleSheet.ts @@ -70,8 +70,9 @@ const BUILTINS: Style[] = [ para: { spaceBefore: 0, spaceAfter: 0 }, text: { fontFamily: 'Liberation Serif', fontSizePt: 12 } }, { name: HEADING_PARENT, parent: DEFAULT_STYLE, next: DEFAULT_STYLE, builtin: true, para: { spaceBefore: 12, spaceAfter: 6 }, text: { fontFamily: 'Liberation Sans', bold: true } }, - // Sizes and the italic on 4 and 6 are LibreOffice's; the margins come from the Heading parent. - ...([[18], [16], [14], [13, true], [12], [12, true]] as [number, boolean?][]).map(([size, italic], i) => ({ + // Sizes and the italic on 4 and 6 are LibreOffice's; the margins come from the Heading + // parent. 7–10 continue level 6 (HEADING_STYLE_OVERRIDES says why). + ...([[18], [16], [14], [13, true], [12], [12, true], [12], [12, true], [12], [12, true]] as [number, boolean?][]).map(([size, italic], i) => ({ name: `Heading ${i + 1}`, parent: HEADING_PARENT, next: DEFAULT_STYLE, outlineLevel: i + 1, builtin: true, para: {}, text: italic ? { fontSizePt: size, italic: true } : { fontSizePt: size }, @@ -97,7 +98,7 @@ const CHAR_BUILTINS: Style[] = [ // Bumped whenever the built-in definitions change: a stored sheet from an older version // keeps its user styles but takes the new factory built-ins (see mergeStoredSheet). -export const STYLE_SHEET_VERSION = 9; +export const STYLE_SHEET_VERSION = 10; // A persisted sheet merged onto the current built-ins. Same version: stored entries win // (a document's own styles, and edits to built-ins). Older: only user styles survive. diff --git a/src/styles/editor.css b/src/styles/editor.css index f1108bf..58c0544 100644 --- a/src/styles/editor.css +++ b/src/styles/editor.css @@ -576,14 +576,11 @@ overflow-wrap: break-word; } -.paper .tiptap h1, -.paper .tiptap h2, -.paper .tiptap h3, -.paper .tiptap h4, -.paper .tiptap h5, -.paper .tiptap h6 { +.paper .tiptap :is(h1, h2, h3, h4, h5, h6, h7, h8, h9, h10) { /* Neutral base: font, size and margins come from the named paragraph styles - (styles/styleSheet.ts → the #document-styles element Editor.svelte writes). */ + (styles/styleSheet.ts → the #document-styles element Editor.svelte writes). + HTML stops at h6, so 7–10 are unknown elements — inline until told otherwise. */ + display: block; font: inherit; margin: 0; } @@ -596,10 +593,10 @@ inherits: false; initial-value: 0; } -.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6, .toc, .toc-title, .toc-entry) { +.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10, .toc, .toc-title, .toc-entry) { padding-top: var(--space-before); } -.paper[data-spacing-model='max'] .tiptap :is(p, h1, h2, h3, h4, h5, h6, .toc, .toc-title, .toc-entry) { +.paper[data-spacing-model='max'] .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10, .toc, .toc-title, .toc-entry) { padding-top: 0; margin-top: var(--space-before); } @@ -613,14 +610,14 @@ /* The line box: the paragraph's spacing factor times its font's natural line height. Either half may come from the style rule or, nearer, from the block's own attrs (lineHeight.ts / blockFontSize.ts) — multiplying them here is what keeps both. */ -.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6, li, .toc-entry) { +.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10, li, .toc-entry) { line-height: calc(var(--line-factor) * var(--natural-line)); } /* A spacing above single puts all of its extra leading *below* each line (measured in LibreOffice); CSS splits it half above. Shifting the block up by that half draws its lines where LibreOffice does and leaves the flow — so pagination — untouched. */ -.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6) { +.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10) { position: relative; top: calc((1 - var(--line-factor)) * var(--natural-line) * 0.5em); } @@ -647,11 +644,11 @@ /* Out of flow, so the marker cannot touch the line it labels — in flow it grew every list line by a pixel, which cost the thesis fixture a page. `top: 0` seats it on the first line, since the pseudo takes the block's own line-height. */ -.paper .tiptap li > :is(p, h1, h2, h3, h4, h5, h6) { +.paper .tiptap li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10) { position: relative; } -.paper .tiptap li > :is(p, h1, h2, h3, h4, h5, h6)::before { +.paper .tiptap li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { position: absolute; top: 0; left: -0.635cm; @@ -668,7 +665,7 @@ /* A label set against the far end of the hanging indent grows into the margin instead of into the text — what keeps Word's Roman numerals off the words they label. */ -.paper .tiptap :is(ol, ul)[data-marker-align="right"] > li > :is(p, h1, h2, h3, h4, h5, h6)::before { +.paper .tiptap :is(ol, ul)[data-marker-align="right"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { left: auto; right: calc(100% + 0.635cm); } @@ -676,21 +673,21 @@ /* Ordered numbering, keyed by data-eff-list-style — the effective numbering orderedList.ts's plugin resolves per