feat: headings go to level 10, as both word processors do
HTML stops at h6, so 7-10 render as unknown elements editor.css makes blocks; their built-in styles continue level 6 at 12pt, since LibreOffice writes these styles with no properties and resolves them from its own pool. Both importers read a two-digit level again (the style-name regexes stopped at one digit). Verified through a LibreOffice re-save of both formats and in the live editor.
This commit is contained in:
1 parent
4afd5cd1d5
commit
147034730f
8 files changed
+100
-79
No files matched your search
+1
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+36
-69
@@ -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 <ol> from its attr + ancestor chain. The
|
||||
undecorated <ol> is the plain decimal fallback and must come first. */
|
||||
.paper .tiptap ol > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: counter(list-item, decimal) ". "; }
|
||||
.paper .tiptap ol[data-eff-list-style="decimal"] > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: counter(list-item, decimal) ". "; }
|
||||
.paper .tiptap ol[data-eff-list-style="decimal-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: counter(list-item, decimal) ") "; }
|
||||
.paper .tiptap ol[data-eff-list-style="lower-alpha"] > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: counter(list-item, lower-alpha) ". "; }
|
||||
.paper .tiptap ol[data-eff-list-style="lower-alpha-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: counter(list-item, lower-alpha) ") "; }
|
||||
.paper .tiptap ol[data-eff-list-style="upper-alpha"] > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: counter(list-item, upper-alpha) ". "; }
|
||||
.paper .tiptap ol[data-eff-list-style="upper-alpha-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: counter(list-item, upper-alpha) ") "; }
|
||||
.paper .tiptap ol[data-eff-list-style="lower-roman"] > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: counter(list-item, lower-roman) ". "; }
|
||||
.paper .tiptap ol[data-eff-list-style="lower-roman-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: counter(list-item, lower-roman) ") "; }
|
||||
.paper .tiptap ol[data-eff-list-style="upper-roman"] > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: counter(list-item, upper-roman) ". "; }
|
||||
.paper .tiptap ol[data-eff-list-style="upper-roman-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: counter(list-item, upper-roman) ") "; }
|
||||
.paper .tiptap ol > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, decimal) ". "; }
|
||||
.paper .tiptap ol[data-eff-list-style="decimal"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, decimal) ". "; }
|
||||
.paper .tiptap ol[data-eff-list-style="decimal-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, decimal) ") "; }
|
||||
.paper .tiptap ol[data-eff-list-style="lower-alpha"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, lower-alpha) ". "; }
|
||||
.paper .tiptap ol[data-eff-list-style="lower-alpha-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, lower-alpha) ") "; }
|
||||
.paper .tiptap ol[data-eff-list-style="upper-alpha"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, upper-alpha) ". "; }
|
||||
.paper .tiptap ol[data-eff-list-style="upper-alpha-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, upper-alpha) ") "; }
|
||||
.paper .tiptap ol[data-eff-list-style="lower-roman"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, lower-roman) ". "; }
|
||||
.paper .tiptap ol[data-eff-list-style="lower-roman-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, lower-roman) ") "; }
|
||||
.paper .tiptap ol[data-eff-list-style="upper-roman"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, upper-roman) ". "; }
|
||||
.paper .tiptap ol[data-eff-list-style="upper-roman-paren"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: counter(list-item, upper-roman) ") "; }
|
||||
|
||||
/* Legal/outline numbering (1., 1.1., 1.2.1. …): the plugin tags the multilevel list and
|
||||
every attr-less member of its chain with data-eff-list-style="multilevel". */
|
||||
.paper .tiptap ol[data-eff-list-style="multilevel"] > li > :is(p, h1, h2, h3, h4, h5, h6)::before {
|
||||
.paper .tiptap ol[data-eff-list-style="multilevel"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before {
|
||||
content: counters(list-item, ".") ". ";
|
||||
}
|
||||
|
||||
@@ -698,25 +695,25 @@
|
||||
(DEFAULT_BULLET_CYCLE in bulletListTypes.ts). The bundled symbol shim comes first: it
|
||||
holds only the glyphs Liberation Serif lacks (❖ ➢ ⇨ ✓ ▸ ◆), so those stay compact and
|
||||
platform-independent while every other char falls through to the text font. */
|
||||
.paper .tiptap ul > li > :is(p, h1, h2, h3, h4, h5, h6)::before {
|
||||
.paper .tiptap ul > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before {
|
||||
content: '• ';
|
||||
font-family: 'EdenText Symbols', var(--marker-family, var(--font-serif)), serif;
|
||||
}
|
||||
.paper .tiptap ul ul > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: '◦ '; }
|
||||
.paper .tiptap ul ul ul > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: '▪ '; }
|
||||
.paper .tiptap ul ul ul ul > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: '▸ '; }
|
||||
.paper .tiptap ul ul ul ul ul > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: '– '; }
|
||||
.paper .tiptap ul ul ul ul ul ul > li > :is(p, h1, h2, h3, h4, h5, h6)::before { content: '· '; }
|
||||
.paper .tiptap ul ul > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: '◦ '; }
|
||||
.paper .tiptap ul ul ul > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: '▪ '; }
|
||||
.paper .tiptap ul ul ul ul > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: '▸ '; }
|
||||
.paper .tiptap ul ul ul ul ul > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: '– '; }
|
||||
.paper .tiptap ul ul ul ul ul ul > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before { content: '· '; }
|
||||
|
||||
/* A list's own `bulletChar` beats the cycle at any depth — an attribute outranks the
|
||||
descendant chain. A nested list without one keeps its depth's char, which is what both
|
||||
exports write, even though --bullet inherits into it. */
|
||||
.paper .tiptap ul[data-bullet] > li > :is(p, h1, h2, h3, h4, h5, h6)::before {
|
||||
.paper .tiptap ul[data-bullet] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before {
|
||||
content: var(--bullet) ' ';
|
||||
}
|
||||
|
||||
/* The PDF clone (the .paper itself) replaces every marker with a real span (pdf.ts). */
|
||||
.paper.pdf-list-markers li > :is(p, h1, h2, h3, h4, h5, h6)::before {
|
||||
.paper.pdf-list-markers li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
@@ -874,13 +871,7 @@
|
||||
/* Paragraph mark (¶). Out of flow at its static position — after the last character,
|
||||
but invisible to line breaking, so a line-end space still hangs into the margin and
|
||||
the mark can never be pushed onto a line of its own. The toggle only recolors it. */
|
||||
.paper .tiptap p::after,
|
||||
.paper .tiptap h1::after,
|
||||
.paper .tiptap h2::after,
|
||||
.paper .tiptap h3::after,
|
||||
.paper .tiptap h4::after,
|
||||
.paper .tiptap h5::after,
|
||||
.paper .tiptap h6::after {
|
||||
.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::after {
|
||||
content: '\00A0¶';
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
@@ -899,13 +890,7 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.paper.show-formatting-marks .tiptap p::after,
|
||||
.paper.show-formatting-marks .tiptap h1::after,
|
||||
.paper.show-formatting-marks .tiptap h2::after,
|
||||
.paper.show-formatting-marks .tiptap h3::after,
|
||||
.paper.show-formatting-marks .tiptap h4::after,
|
||||
.paper.show-formatting-marks .tiptap h5::after,
|
||||
.paper.show-formatting-marks .tiptap h6::after {
|
||||
.paper.show-formatting-marks .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::after {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
@@ -918,24 +903,12 @@
|
||||
|
||||
/* A block ProseMirror ends with a trailing-break BR — an empty one, or one ending in a
|
||||
hard break — has its last line before that BR, not after it. */
|
||||
.paper .tiptap p:has(> br.ProseMirror-trailingBreak),
|
||||
.paper .tiptap h1:has(> br.ProseMirror-trailingBreak),
|
||||
.paper .tiptap h2:has(> br.ProseMirror-trailingBreak),
|
||||
.paper .tiptap h3:has(> br.ProseMirror-trailingBreak),
|
||||
.paper .tiptap h4:has(> br.ProseMirror-trailingBreak),
|
||||
.paper .tiptap h5:has(> br.ProseMirror-trailingBreak),
|
||||
.paper .tiptap h6:has(> br.ProseMirror-trailingBreak),
|
||||
.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10):has(> br.ProseMirror-trailingBreak),
|
||||
.paper .tiptap li > p:has(> br.ProseMirror-trailingBreak) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.paper .tiptap p:has(> br.ProseMirror-trailingBreak)::after,
|
||||
.paper .tiptap h1:has(> br.ProseMirror-trailingBreak)::after,
|
||||
.paper .tiptap h2:has(> br.ProseMirror-trailingBreak)::after,
|
||||
.paper .tiptap h3:has(> br.ProseMirror-trailingBreak)::after,
|
||||
.paper .tiptap h4:has(> br.ProseMirror-trailingBreak)::after,
|
||||
.paper .tiptap h5:has(> br.ProseMirror-trailingBreak)::after,
|
||||
.paper .tiptap h6:has(> br.ProseMirror-trailingBreak)::after,
|
||||
.paper .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10):has(> br.ProseMirror-trailingBreak)::after,
|
||||
.paper .tiptap li > p:has(> br.ProseMirror-trailingBreak)::after {
|
||||
/* The static position would land past the BR, a line below the block; name the last
|
||||
line instead — for an empty block that is also its first. */
|
||||
@@ -1086,13 +1059,7 @@
|
||||
content: none;
|
||||
}
|
||||
|
||||
.paper.show-formatting-marks .tiptap p::after,
|
||||
.paper.show-formatting-marks .tiptap h1::after,
|
||||
.paper.show-formatting-marks .tiptap h2::after,
|
||||
.paper.show-formatting-marks .tiptap h3::after,
|
||||
.paper.show-formatting-marks .tiptap h4::after,
|
||||
.paper.show-formatting-marks .tiptap h5::after,
|
||||
.paper.show-formatting-marks .tiptap h6::after {
|
||||
.paper.show-formatting-marks .tiptap :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::after {
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Heading levels 7–10: both word processors go that far, HTML stops at h6. The level
|
||||
// must survive both formats — an outline level ODF writes as-is and Word carries in the
|
||||
// style name — instead of collapsing onto level 6.
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { unzipSync, strFromU8 } from 'fflate';
|
||||
import { buildOdt, MAX_HEADING_LEVEL, HEADING_STYLE_OVERRIDES } 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 { builtinStyleSheet, resolveStyle } from '../../src/lib/styles/styleSheet';
|
||||
|
||||
type N = any;
|
||||
|
||||
const H = (level: number): N =>
|
||||
({ type: 'heading', attrs: { level, styleName: `Heading ${level}` }, content: [{ type: 'text', text: `Level ${level}` }] });
|
||||
|
||||
const doc: N = { type: 'doc', content: [1, 6, 7, 8, 9, 10].map(H) };
|
||||
const margins = { top: 2, bottom: 2, left: 2, right: 2 };
|
||||
const levels = (d: N) => d.content.filter((b: N) => b.type === 'heading').map((b: N) => b.attrs.level);
|
||||
|
||||
describe('heading levels 7–10', () => {
|
||||
it('offers ten levels, with a built-in style each', () => {
|
||||
expect(MAX_HEADING_LEVEL).toBe(10);
|
||||
expect(HEADING_STYLE_OVERRIDES).toHaveLength(10);
|
||||
const sheet = builtinStyleSheet();
|
||||
for (let level = 7; level <= 10; level++) {
|
||||
expect(resolveStyle(sheet, `Heading ${level}`).text.fontSizePt).toBe(12);
|
||||
}
|
||||
});
|
||||
|
||||
it('round-trips through ODF', async () => {
|
||||
const bytes = await buildOdt(doc, margins, 'portrait');
|
||||
const xml = strFromU8(unzipSync(bytes)['content.xml']);
|
||||
expect(xml).toContain('text:outline-level="10"');
|
||||
expect(levels(importOdt(bytes).content as N)).toEqual([1, 6, 7, 8, 9, 10]);
|
||||
});
|
||||
|
||||
it('round-trips through DOCX', async () => {
|
||||
const bytes = await buildDocx(doc, margins, 'portrait');
|
||||
const xml = strFromU8(unzipSync(bytes)['word/document.xml']);
|
||||
expect(xml).toContain('w:val="Heading10"');
|
||||
expect(levels(importDocx(bytes).content as N)).toEqual([1, 6, 7, 8, 9, 10]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user