diff --git a/src/lib/export/odt.ts b/src/lib/export/odt.ts index ff0f387..5082443 100644 --- a/src/lib/export/odt.ts +++ b/src/lib/export/odt.ts @@ -1931,19 +1931,18 @@ function applyParagraphStyles(odtBytes: Uint8Array): Uint8Array { return name; }; - const styRe = new RegExp(`${STY}([^${STY}]*)${STY}`); + // Anchored right after the opening tag (where the sentinel is emitted): a scan to the + // closing tag would start at a text box's anchor paragraph and steal the first box + // child's sentinel — nested (box children, note bodies) make spans unsafe. content = content.replace( - new RegExp(`]*[^/>])?)>([\\s\\S]*?)`, 'g'), - (m, tag: string, attrs: string, inner: string) => { - const sm = styRe.exec(inner); - if (!sm) return m; - const cleaned = inner.replace(styRe, ''); + new RegExp(`]*[^/>])?)>${STY}([^${STY}]*)${STY}`, 'g'), + (_m, tag: string, attrs: string, styleName: string) => { const srcM = /text:style-name="([^"]*)"/.exec(attrs); - const name = styleFor(srcM ? srcM[1] : '', sm[1]); + const name = styleFor(srcM ? srcM[1] : '', styleName); const newAttrs = srcM ? attrs.replace(/text:style-name="[^"]*"/, `text:style-name="${name}"`) : ` text:style-name="${name}"${attrs}`; - return `${cleaned}`; + return ``; }, ); content = content.replace(new RegExp(`${STY}[^${STY}]*${STY}`, 'g'), ''); @@ -1989,19 +1988,18 @@ function applyParagraphBoxes(odtBytes: Uint8Array): Uint8Array { return name; }; - const pbxRe = new RegExp(`${PBX}([^${PBX}]*)${PBX}`); + // Anchored right after the opening tag, over an optional STY run (its pass runs later; + // emission order is FSZ STY PBX). A scan to the closing tag would start at a text box's + // anchor paragraph and steal the first box child's sentinel. content = content.replace( - new RegExp(`]*[^/>])?)>([\\s\\S]*?)`, 'g'), - (m, tag: string, attrs: string, inner: string) => { - const sm = pbxRe.exec(inner); - if (!sm) return m; - const cleaned = inner.replace(pbxRe, ''); + new RegExp(`]*[^/>])?)>((?:${STY}[^${STY}]*${STY})?)${PBX}([^${PBX}]*)${PBX}`, 'g'), + (_m, tag: string, attrs: string, sty: string, spec: string) => { const srcM = /text:style-name="([^"]*)"/.exec(attrs); - const name = boxStyleFor(srcM ? srcM[1] : '', sm[1]); + const name = boxStyleFor(srcM ? srcM[1] : '', spec); const newAttrs = srcM ? attrs.replace(/text:style-name="[^"]*"/, `text:style-name="${name}"`) : ` text:style-name="${name}"${attrs}`; - return `${cleaned}`; + return `${sty}`; }, ); // Drop any sentinels not consumed above (defensive — never legitimate text). @@ -3191,10 +3189,17 @@ function resolveTextEffects(xml: string, mint: (styleXml: string) => void, prefi }, ); // A run whose only formatting is such an effect never got a span: give it one, up to - // the next real element — a tab/line-break/space inside the run rides along, or the - // text behind it would fall out of the span and lose the effect. + // the next real element — a tab/line-break/space, a bookmark marker or a comment's + // annotation inside the run rides along, or the text behind it would fall out of the + // span and lose the effect. + // The annotation block is tempered to its first close tag, and (?=[\s>]) keeps it off + // ]*/>` + + `|])(?:(?!)[\\s\\S])*` + + `|]*/>`; out = out.replace( - new RegExp(`${TEF}([^${TEF}]+)${TEF}((?:[^<${TEF}]|]*/>)*)(?:${TEF}${TEF})?`, 'g'), + new RegExp(`${TEF}([^${TEF}]+)${TEF}((?:[^<${TEF}]|${inline})*)(?:${TEF}${TEF})?`, 'g'), (_m, extra: string, text: string) => `${text}`, ); // Drop any sentinels not consumed above (defensive — never legitimate text).