fix: a note's stock body style no longer imports as direct formatting
Both importers stamped the exporter's own note style back onto the note (styleName "Footnote" from ODT, "footnote text" from DOCX), so an editor-authored document accreted a foreign attr on every round trip and the two formats disagreed. A style that only re-states the stock note look is now suppressed like any other default-equal value; a customized one still lands.
This commit is contained in:
1 parent
7a8c552342
commit
97f9016369
2 files changed
+36
-3
No files matched your search
+15
-1
@@ -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';
|
||||
|
||||
+21
-2
@@ -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 <text:p> 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, ' ');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user