diff --git a/lib/ex_doc/formatter/epub/templates.ex b/lib/ex_doc/formatter/epub/templates.ex
index 2bea6d45c..f88b990c5 100644
--- a/lib/ex_doc/formatter/epub/templates.ex
+++ b/lib/ex_doc/formatter/epub/templates.ex
@@ -8,6 +8,8 @@ defmodule ExDoc.Formatter.EPUB.Templates do
alias ExDoc.Formatter.HTML.Templates, as: H
alias ExDoc.Formatter.EPUB.Assets
+ defp enc(binary), do: h(URI.encode(binary))
+
# The actual rendering happens here
defp render_doc(ast), do: ast && ExDoc.DocAST.to_html(ast)
diff --git a/lib/ex_doc/formatter/epub/templates/content_template.eex b/lib/ex_doc/formatter/epub/templates/content_template.eex
index eecaf0d42..248839186 100644
--- a/lib/ex_doc/formatter/epub/templates/content_template.eex
+++ b/lib/ex_doc/formatter/epub/templates/content_template.eex
@@ -18,10 +18,10 @@
<%= for extra <- extras do %>
-
+
<% end %>
<%= for node <- modules ++ tasks do %>
-
+
<% end %>
<%= for {static_file, media_type} <- static_files do %>
@@ -37,10 +37,10 @@
<%= for extra <- extras do %>
-
+
<% end %>
<%= for node <- modules ++ tasks do %>
-
+
<% end %>
diff --git a/lib/ex_doc/formatter/epub/templates/nav_grouped_item_template.eex b/lib/ex_doc/formatter/epub/templates/nav_grouped_item_template.eex
index 67b1d0396..22b87e440 100644
--- a/lib/ex_doc/formatter/epub/templates/nav_grouped_item_template.eex
+++ b/lib/ex_doc/formatter/epub/templates/nav_grouped_item_template.eex
@@ -4,7 +4,7 @@
<% end %>
<%= for node <- nodes do %>
- - <%=h node.title %>
+ - <%=h node.title %>
<% end %>
<%= if title do %>
diff --git a/test/ex_doc/formatter/epub_test.exs b/test/ex_doc/formatter/epub_test.exs
index b63624314..96f501082 100644
--- a/test/ex_doc/formatter/epub_test.exs
+++ b/test/ex_doc/formatter/epub_test.exs
@@ -66,6 +66,28 @@ defmodule ExDoc.Formatter.EPUBTest do
assert content =~ ~r{Jane Doe}
end
+ test "escapes ids and hrefs built from a configured filename", %{tmp_dir: tmp_dir} = context do
+ extra = tmp_dir <> "/readme.md"
+ File.write!(extra, "# Hello\n")
+
+ generate_and_unzip(context, config(context, extras: [{extra, filename: "a&b"}]))
+
+ content = File.read!(tmp_dir <> "/epub/OEBPS/content.opf")
+ assert content =~ ~s{ }
+
+ nav = File.read!(tmp_dir <> "/epub/OEBPS/nav.xhtml")
+ assert nav =~ ~s{}
+
+ # an unescaped & here is a fatal XML error, not a quirk
+ for file <- ["content.opf", "nav.xhtml"] do
+ (tmp_dir <> "/epub/OEBPS/" <> file)
+ |> File.read!()
+ |> :binary.bin_to_list()
+ |> :xmerl_scan.string()
+ end
+ end
+
test "generates an EPUB file in the default directory", %{tmp_dir: tmp_dir} = context do
generate(config(context))
assert File.regular?(tmp_dir <> "/epub/#{config(context)[:project]}.epub")