fix: a header or footer the zone schema can't hold renders anyway
A hyperlink in a fixture's copyright footer threw generateHTML and blanked the whole zone. Drop what the one-paragraph schema lacks, keep its text. A master handing over to a successor is also the different-first- page flag, so a title master with no zones leaves page one blank.
This commit is contained in:
1 parent
e3a5594f10
commit
d6bd53d971
2 files changed
+31
-4
No files matched your search
+27
-4
@@ -22,6 +22,9 @@ import { languageFromOdf, NO_LANGUAGE, type DocumentLanguage } from '../storage/
|
||||
import type { HfDoc, HfSet } from '../storage/headerFooter';
|
||||
import type { EmbeddedFont } from '../fonts/embeddedFonts';
|
||||
import { cellPaddingAttr, DEFAULT_CELL_PADDING, type CellPadding } from '../editor/extensions/tableCellPadding';
|
||||
import { getSchema } from '@tiptap/core';
|
||||
import type { Schema } from '@tiptap/pm/model';
|
||||
import { hfExtensions } from '../editor/extensions/headerFooter';
|
||||
|
||||
// .odt → TipTap JSON, inverting export/odt.ts. Editor-expressible content becomes its
|
||||
// native node/mark/attr; values matching the editor's defaults are suppressed so round
|
||||
@@ -597,7 +600,7 @@ export function importOdt(bytes: Uint8Array, convertedImages: ConvertedImages =
|
||||
const footerFirst = hf.footerFirst ? convertHfZone(hf.footerFirst, ctx) : null;
|
||||
// The presence of a first-page element is the flag, even when it's empty (an empty
|
||||
// first-page zone deliberately blanks page 1 while the default fills later pages).
|
||||
const differentFirstPage = !!(hf.headerFirst || hf.footerFirst);
|
||||
const differentFirstPage = !!(hf.headerFirst || hf.footerFirst || hf.firstPageOnly);
|
||||
const headerEven = hf.headerLeft ? convertHfZone(hf.headerLeft, ctx) : null;
|
||||
const footerEven = hf.footerLeft ? convertHfZone(hf.footerLeft, ctx) : null;
|
||||
const differentOddEven = !!(hf.headerLeft || hf.footerLeft);
|
||||
@@ -645,7 +648,7 @@ function hfSetOfMasterPage(name: string, ctx: Ctx): HfSet {
|
||||
footer: zone(hf.footer),
|
||||
headerFirst: zone(hf.headerFirst),
|
||||
footerFirst: zone(hf.footerFirst),
|
||||
differentFirstPage: !!(hf.headerFirst || hf.footerFirst),
|
||||
differentFirstPage: !!(hf.headerFirst || hf.footerFirst || hf.firstPageOnly),
|
||||
headerEven: zone(hf.headerLeft),
|
||||
footerEven: zone(hf.footerLeft),
|
||||
differentOddEven: !!(hf.headerLeft || hf.footerLeft),
|
||||
@@ -687,9 +690,10 @@ function convertHfZone(zoneEl: Element, ctx: Ctx): HfDoc {
|
||||
// runs and no inter-paragraph break, so inline stays empty. Keep such a zone only when
|
||||
// it carries a background/rule line (a footer that is just a colored line has no text).
|
||||
const box = mergeHfBox(boxMaps);
|
||||
if (inline.length === 0 && Object.keys(box).length === 0) return null;
|
||||
const content = fitZoneSchema(inline);
|
||||
if (content.length === 0 && Object.keys(box).length === 0) return null;
|
||||
|
||||
const para: Node = { type: 'paragraph', content: inline };
|
||||
const para: Node = { type: 'paragraph', content };
|
||||
const attrs: Record<string, string> = {};
|
||||
if (textAlign) attrs.textAlign = textAlign;
|
||||
if (stops) attrs.tabStops = stops;
|
||||
@@ -698,6 +702,25 @@ function convertHfZone(zoneEl: Element, ctx: Ctx): HfDoc {
|
||||
return { type: 'doc', content: [para] };
|
||||
}
|
||||
|
||||
// The header/footer schema is a subset of the body's, and it is not forgiving: one node
|
||||
// or mark it doesn't know (a hyperlink, a date field) makes the whole zone fail to render
|
||||
// and come out blank. Drop what it can't hold, keeping the text.
|
||||
let zoneSchema: Schema | null = null;
|
||||
function fitZoneSchema(nodes: Node[]): Node[] {
|
||||
zoneSchema ??= getSchema(hfExtensions());
|
||||
const out: Node[] = [];
|
||||
for (const n of nodes) {
|
||||
if (!zoneSchema.nodes[n.type]) {
|
||||
const text = typeof n.attrs?.text === 'string' ? n.attrs.text : '';
|
||||
if (text) out.push({ type: 'text', text });
|
||||
continue;
|
||||
}
|
||||
const marks = n.marks?.filter((m) => zoneSchema!.marks[m.type]);
|
||||
out.push(marks && marks.length !== n.marks?.length ? { ...n, marks } : n);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// The zone collapses several source paragraphs into one, so pick the box props that
|
||||
// reproduce the visible band+rule: background/side rules from the first that declares
|
||||
// them, but the bottom rule from the LAST paragraph (it sits under the whole band).
|
||||
|
||||
@@ -541,6 +541,7 @@ export class StyleResolver {
|
||||
footerLeft: Element | null;
|
||||
headerExtraCm: number;
|
||||
footerExtraCm: number;
|
||||
firstPageOnly: boolean;
|
||||
} {
|
||||
const mp = this.masterPageEl(pageName);
|
||||
const layout = this.pageLayoutEl();
|
||||
@@ -597,6 +598,9 @@ export class StyleResolver {
|
||||
footerLeft: zoneIn(rest, 'footer-left'),
|
||||
headerExtraCm: extraCm('header-style', 'margin-bottom'),
|
||||
footerExtraCm: extraCm('footer-style', 'margin-top'),
|
||||
// Handing over to a successor is itself the "different first page" flag: a title
|
||||
// master with no zones of its own leaves page one deliberately blank.
|
||||
firstPageOnly: !!successor,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user