Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/formats/epub/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,56 @@ mod tests {
.collect::<String>();
assert_eq!(text, "chapter text", "sixty-four itemrefs naming one part");
}

#[test]
fn a_title_page_with_a_bare_ampersand_is_still_read() {
// A chapter that is not well-formed XML used to be dropped whole, so
// a title page reading "Tom & Jerry" vanished and the book appeared
// to start at the following chapter.
let parts = [
(
"META-INF/container.xml",
r#"<?xml version="1.0"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles><rootfile full-path="c.opf"
media-type="application/oebps-package+xml"/></rootfiles></container>"#,
),
(
"c.opf",
r#"<?xml version="1.0"?>
<package xmlns="http://www.idpf.org/2007/opf" version="3.0"><metadata/>
<manifest>
<item id="tp" href="title.xhtml" media-type="application/xhtml+xml"/>
<item id="c1" href="ch1.xhtml" media-type="application/xhtml+xml"/>
</manifest>
<spine><itemref idref="tp"/><itemref idref="c1"/></spine></package>"#,
),
(
"title.xhtml",
r#"<?xml version="1.0"?><html xmlns="http://www.w3.org/1999/xhtml">
<body><p>Tom & Jerry (1940)</p></body></html>"#,
),
(
"ch1.xhtml",
r#"<?xml version="1.0"?><html xmlns="http://www.w3.org/1999/xhtml">
<body><p>chapter one</p></body></html>"#,
),
];
let mut w = zip::ZipWriter::new(Cursor::new(Vec::new()));
for (name, body) in &parts {
w.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
w.write_all(body.as_bytes()).unwrap();
}

let doc = parse(&w.finish().unwrap().into_inner()).unwrap();
let text = doc
.blocks
.iter()
.filter_map(|b| match b {
Block::Paragraph(inlines) => Some(crate::model::inlines_to_plain_text(inlines)),
_ => None,
})
.collect::<String>();
assert_eq!(text, "Tom & Jerry (1940)chapter one", "the title page opens the book");
}
}
31 changes: 31 additions & 0 deletions src/package/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@ pub fn parse_xml(bytes: &[u8]) -> Result<Element, ConvertError> {
let utf8 = to_utf8(bytes);
let mut reader = NsReader::from_reader(utf8.as_ref());
reader.config_mut().check_end_names = false;
// A bare `&` is common in real XHTML and HTML-ish parts ("R&D", "50% &
// rising"). Rejecting it would discard the whole part - and the scan for
// the missing `;` runs to end of input, so one stray ampersand costs
// every element after it. Dangling ampersands stay literal text instead;
// well-formed references still arrive as `GeneralRef`.
reader.config_mut().allow_dangling_amp = true;

let mut interner: HashMap<Vec<u8>, Rc<str>> = HashMap::new();
let mut root =
Expand Down Expand Up @@ -627,4 +633,29 @@ mod tests {
assert_eq!(root.text(), "leaf");
assert_eq!(root.descendants("", "never").count(), 0);
}

#[test]
fn dangling_ampersand_stays_literal_text() {
// A `&` with no reference after it is not well-formed XML, but real
// XHTML carries it constantly. It must not cost the rest of the part:
// the scan for the missing `;` runs to end of input, so rejecting
// here would drop every element that follows.
for (xml, want) in [
(&br#"<r><p>Tom & Jerry</p><p>after</p></r>"#[..], "Tom & Jerryafter"),
(&br#"<r><p>R&D</p><p>after</p></r>"#[..], "R&Dafter"),
(&br#"<r><p>50% & rising</p><p>after</p></r>"#[..], "50% & risingafter"),
(&br#"<r><p>trailing &</p></r>"#[..], "trailing &"),
] {
let root = parse_xml(xml).unwrap();
assert_eq!(root.text(), want, "{}", String::from_utf8_lossy(xml));
}
}

#[test]
fn well_formed_references_still_resolve_beside_a_dangling_one() {
// Leniency for the bare `&` must not stop the real references on
// either side of it from expanding.
let root = parse_xml(br#"<r><p>a &amp; b & c &#65; d &nbsp;e</p></r>"#).unwrap();
assert_eq!(root.text(), "a & b & c A d \u{a0}e");
}
}