From 3668729fd9c1094695b91a5733a3fabeab95d5e5 Mon Sep 17 00:00:00 2001 From: Stu Kabakoff Date: Tue, 28 Jul 2026 10:41:36 -0400 Subject: [PATCH] Keep each document's DOCTYPE through the rewrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit etree.tostring writes out the element tree alone, so the optimizer was silently dropping the DOCTYPE of every document it re-serialized — and a document that loses its DTD loses every named entity with it, leaving   and friends as fatal "undefined entity" errors on-device. Hand the declaration back to the serializer instead. Co-Authored-By: Claude Opus 5 --- crosspoint_reader/optimizer.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crosspoint_reader/optimizer.py b/crosspoint_reader/optimizer.py index 898d498..a3c2315 100644 --- a/crosspoint_reader/optimizer.py +++ b/crosspoint_reader/optimizer.py @@ -388,9 +388,12 @@ def _fix_xhtml(text, log): for img in root.iter('img'): modified |= _fix_img_element(img) if modified: + # tostring() writes out the element tree alone, so the DOCTYPE + # has to be handed back or the rewrite drops it — and a + # document that loses its DTD loses its named entities with it. text = etree.tostring( - root, encoding='unicode', - xml_declaration=False) + root, encoding='unicode', xml_declaration=False, + doctype=root.getroottree().docinfo.doctype or None) if not text.lstrip().startswith('\n' + text except Exception: @@ -459,7 +462,8 @@ def _fix_opf(text, log): del it.attrib['properties'] _ensure_cover_meta_lxml(root, opf_ns, log) - out = etree.tostring(root, encoding='unicode', xml_declaration=False) + out = etree.tostring(root, encoding='unicode', xml_declaration=False, + doctype=root.getroottree().docinfo.doctype or None) if not out.lstrip().startswith('\n' + out return out