Skip to content

epub: escape ids and hrefs in the EPUB templates - #2269

Merged
josevalim merged 1 commit into
elixir-lang:mainfrom
youdie006:epub-escape-attributes
Sep 15, 2026
Merged

josevalim merged 1 commit into
elixir-lang:mainfrom
youdie006:epub-escape-attributes

Conversation

@youdie006

Copy link
Copy Markdown
Contributor

26a96dd3 "Escape attributes" routed interpolated attribute values through ExDoc.Utils.h/1. It fixed DocAST.ast_attributes_to_string and HTML.Templates.enc, and left the EPUB templates interpolating bare URI.encode/1 output into XML attributes.

URI.encode/1 keeps every reserved character literal, so & survives it. In XML a bare & is a fatal parse error, not a quirk.

A configured extra filename reaches those attributes unfiltered, because validate_extra_string! only checks is_binary. Generating with extras: [{"readme.md", filename: "a&b"}] emits

<item id="a&b" href="a&b.xhtml" media-type="application/xhtml+xml" properties="scripted"/>
<itemref idref="a&b"/>

and neither content.opf nor nav.xhtml is well-formed, so the book does not open. nav_grouped_item_template.eex:7 shows the asymmetry in one line: the link text goes through h, the href beside it does not.

Adds the same enc/1 the HTML templates already use, h(URI.encode(...)), and applies it at the five live sites.

Verification

Measured by generating an EPUB and handing the result to an XML parser.

tree <item id=...> emitted content.opf nav.xhtml
main id="a&b%3Cc%22d" not well-formed, line 17 col 19 not well-formed, line 20 col 22
this change id="a&amp;b%3Cc%22d" parses parses
main, benign filename id="plainname" parses parses

Note < and " are already handled, because URI.encode percent-encodes them; & is the one that gets through, which is why the symptom is a hard parse failure rather than mangled text.

Added one test to epub_test.exs asserting the escaped output and then feeding both files to :xmerl_scan.string/1, which raises on malformed XML. Reverting only the three template files makes it fail. Full suite: 452 tests, 0 failures, against 451 on main.

toc_item_template.eex has the same shape at three sites but is left alone: it is defined at epub/templates.ex:106 and has zero call sites, since nav_template.eex only invokes nav_grouped_item_template. It looks like a leftover from the EPUB2 NCX era. Happy to include it or delete it if you would rather it did not linger.

The module and task ids that flow through the same lines cannot contain these characters when they come from Elixir, so the reachable producer here is the :filename option. The sites are fixed uniformly rather than only the reachable ones, since they all answer the same question.

Related: #1851 reports the same class of fatal EPUB parse error. The specific href in that issue is already fixed by 26a96dd3; these template sites are the part it did not reach. This is a different surface from #2111, which is about named character references in content needing an entity declaration.

Disclosure: written with AI assistance (Claude). Every number above is measured.

26a96dd routed interpolated attribute values through ExDoc.Utils.h/1,
fixing DocAST.ast_attributes_to_string and HTML.Templates.enc, but the
EPUB templates still interpolate bare URI.encode/1 output into XML
attributes. URI.encode keeps every reserved character literal, so an
ampersand survives it, and in XML a bare ampersand is a fatal parse
error rather than a quirk.

A configured extra filename reaches those attributes unfiltered:
validate_extra_string! only checks is_binary. Generating with
extras: [{"readme.md", filename: "a&b"}] emits

  <item id="a&b" href="a&b.xhtml" .../>

and neither content.opf nor nav.xhtml is well-formed, so the book will
not open.

Adds the same enc/1 the HTML templates use, which is h(URI.encode(...)),
and applies it at the five live sites. toc_item_template.eex has the same
shape but is unreachable, with no call sites since the EPUB2 NCX era, so
it is left alone.
@github-actions

Copy link
Copy Markdown

@josevalim
josevalim merged commit 78105a2 into elixir-lang:main Sep 15, 2026
6 checks passed
@josevalim

Copy link
Copy Markdown
Member

💚 💙 💜 💛 ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants