diff --git a/src/lib/import/docx.ts b/src/lib/import/docx.ts index 0e49431..ce39bcb 100644 --- a/src/lib/import/docx.ts +++ b/src/lib/import/docx.ts @@ -1188,15 +1188,29 @@ function noteRefNode(wid: string | null, kind: NoteKind, ctx: Ctx, baseRun: RunP const id = `${kind}${wid}`; // The note renders at the file's own size and indent: its first paragraph names the // style (Word's FootnoteText), and collectStyleSheet only keeps a style in use. + // A style that only re-states the stock look (10pt, nothing else changed against the + // default) equals a styleName-less note and is suppressed like any default-equal value. const firstPara = Array.from(note.children).find((c) => c.namespaceURI === W && c.localName === 'p'); const pStyle = fc(fc(firstPara ?? null, 'pPr'), 'pStyle'); const styleId = pStyle ? wVal(pStyle) : null; - const styleName = styleId ? ctx.styleNames.get(styleId) ?? null : null; + const styleName = styleId && !isStockNoteStyle(ctx, styleId) ? ctx.styleNames.get(styleId) ?? null : null; if (styleId && styleName) ctx.usedStyles.add(styleId); ctx.notes.push({ id, kind, text, content, styleName }); return { type: 'noteRef', attrs: { id, kind, text } }; } +// A note style is "stock" when it resolves to 10pt over the default style with nothing +// else of its own — the look a styleName-less note already renders. +function isStockNoteStyle(ctx: Ctx, styleId: string): boolean { + const run = ctx.styles.paragraphRun(styleId), base = ctx.styles.paragraphRun(null); + if (run.sizeHalfPt !== 20) return false; + const keys = ['bold', 'italic', 'underline', 'strike', 'color', 'font', 'fontTheme', 'highlightFill', 'caps'] as const; + if (!keys.every((k) => (run[k] ?? null) === (base[k] ?? null))) return false; + if (ctx.styles.paragraphAlign(styleId) !== ctx.styles.paragraphAlign(null)) return false; + const sp = ctx.styles.paragraphSpacing(styleId); + return !sp.before && !sp.after && (ctx.styles.styleIndentTwip(styleId) ?? 0) === 0; +} + function convertInline(p: Element, ctx: Ctx, baseRun: RunProps, defaults: BlockDefaults, hfFields: boolean): Node[] { const out: Node[] = []; let fieldMode: 'none' | 'instr' | 'result' = 'none'; diff --git a/src/lib/import/odt.ts b/src/lib/import/odt.ts index b601b55..da091f2 100644 --- a/src/lib/import/odt.ts +++ b/src/lib/import/odt.ts @@ -23,7 +23,7 @@ import type { SpacingModel } from '../storage/spacingModel'; import { pageDimsCm, type PageFormat } from '../storage/pageFormat'; import { languageFromOdf, NO_LANGUAGE, type DocumentLanguage } from '../storage/documentLanguage'; import type { HfDoc, HfSet } from '../storage/headerFooter'; -import type { NoteKind, NoteSettings } from '../storage/noteSettings'; +import { NOTE_FONT_SIZE_PT, NOTE_INDENT_CM, type NoteKind, type NoteSettings } from '../storage/noteSettings'; import { EMPTY_DOC_PROPERTIES, type DocProperties } from '../storage/docProperties'; import { clampPageStart, type PageNumbering } from '../storage/pageNumbering'; import { newCommentId } from '../editor/extensions/comment'; @@ -1807,6 +1807,25 @@ function convertSequenceField(e: Element): Node | null { }; } +// A note style that only re-states the stock look (10pt, the hanging indent, nothing +// else changed against the default style) equals a styleName-less note; suppress it, +// as every other default-equal value is on import. +function isStockNoteStyle(resolver: StyleResolver, name: string): boolean { + // Measured against Standard (the stock styles' parent), so inherited values match. + const text = resolver.paraTextProps(name), baseT = resolver.paraTextProps('Standard'); + const para = resolver.paraProps(name), baseP = resolver.paraProps('Standard'); + const cm = (v?: string) => lengthToCm(v ?? null) ?? 0; + if (Math.abs((lengthToPt(text['fo:font-size']) ?? 0) - NOTE_FONT_SIZE_PT) > 0.3) return false; + if (Math.abs(cm(para['fo:margin-left']) - NOTE_INDENT_CM) > 0.03) return false; + if (Math.abs(cm(para['fo:text-indent']) + NOTE_INDENT_CM) > 0.03) return false; + const sameT = (k: string) => (text[k] ?? '') === (baseT[k] ?? ''); + const sameP = (k: string) => (para[k] ?? '') === (baseP[k] ?? ''); + const sameCm = (k: string) => Math.abs(cm(para[k]) - cm(baseP[k])) < 0.03; + return ['fo:font-family', 'style:font-name', 'fo:font-style', 'fo:font-weight', 'fo:color', 'fo:background-color', 'style:text-underline-style'].every(sameT) + && ['fo:text-align', 'fo:line-height'].every(sameP) + && ['fo:margin-top', 'fo:margin-bottom', 'fo:margin-right'].every(sameCm); +} + // The note's paragraph style: the one its own names, else the class's // configured default. Registered as used, or collectStyleSheet drops it. function noteBodyStyle(e: Element, ctx: Ctx, kind: NoteKind): string | null { @@ -1815,7 +1834,7 @@ function noteBodyStyle(e: Element, ctx: Ctx, kind: NoteKind): string | null { const raw = own?.getAttributeNS(NS.text, 'style-name') ?? ctx.resolver.noteSettings()[kind].bodyStyle.replace(/ /g, '_20_'); const named = ctx.resolver.namedAncestor(raw, 'paragraph'); - if (!named) return null; + if (!named || isStockNoteStyle(ctx.resolver, named)) return null; ctx.usedStyles.add(named); return ctx.styleNames.get(named) ?? named.replace(/_20_/g, ' '); }