Skip to content

Commit b41d5b0

Browse files
committed
Preserve HTML entities in headers
Closes #2114. Closes #2120.
1 parent 7500815 commit b41d5b0

File tree

6 files changed

+54
-61
lines changed

6 files changed

+54
-61
lines changed

lib/ex_doc/doc_ast.ex

+12
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,18 @@ defmodule ExDoc.DocAST do
110110
def extract_title([{:h1, _attrs, inner, _meta} | ast]), do: {:ok, inner, ast}
111111
def extract_title(_ast), do: :error
112112

113+
@doc """
114+
Extracts headers (h2) from the given AST.
115+
116+
Returns the header text.
117+
"""
118+
def extract_headers(doc_ast) do
119+
for {:h2, _, _, _} = node <- doc_ast,
120+
text = ExDoc.DocAST.text(node),
121+
text != "",
122+
do: text
123+
end
124+
113125
@doc """
114126
Compute a synopsis from a document by looking at its first paragraph.
115127
"""

lib/ex_doc/formatter/html.ex

+6-2
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,10 @@ defmodule ExDoc.Formatter.HTML do
320320
source_path: nil,
321321
source_url: config.source_url,
322322
title: "API Reference",
323-
title_content: title_content
323+
title_content: title_content,
324+
headers:
325+
if(nodes_map.modules != [], do: ["Modules"], else: []) ++
326+
if(nodes_map.tasks != [], do: ["Mix Tasks"], else: [])
324327
}
325328
end
326329

@@ -456,7 +459,8 @@ defmodule ExDoc.Formatter.HTML do
456459
source_url: source_url,
457460
search_data: search_data,
458461
title: title,
459-
title_content: title_html || title
462+
title_content: title_html || title,
463+
headers: ExDoc.DocAST.extract_headers(ast)
460464
}
461465
end
462466

lib/ex_doc/formatter/html/templates.ex

+9-13
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ defmodule ExDoc.Formatter.HTML.Templates do
5959
defp sidebar_extras(extras) do
6060
for extra <- extras do
6161
%{id: id, title: title, group: group} = extra
62-
6362
item = %{id: to_string(id), title: to_string(title), group: to_string(group)}
6463

6564
case extra do
@@ -74,14 +73,14 @@ defmodule ExDoc.Formatter.HTML.Templates do
7473
end)
7574

7675
item
77-
|> Map.put(:headers, extract_headers(extra.content))
76+
|> Map.put(:headers, headers_to_id_and_anchors(extra.headers))
7877
|> Map.put(:searchData, search_data)
7978

8079
%{url: url} when is_binary(url) ->
8180
Map.put(item, :url, url)
8281

8382
_ ->
84-
Map.put(item, :headers, extract_headers(extra.content))
83+
Map.put(item, :headers, headers_to_id_and_anchors(extra.headers))
8584
end
8685
end
8786
end
@@ -140,8 +139,9 @@ defmodule ExDoc.Formatter.HTML.Templates do
140139

141140
defp module_sections(module) do
142141
{sections, _} =
143-
module.rendered_doc
144-
|> extract_headers()
142+
module.doc
143+
|> ExDoc.DocAST.extract_headers()
144+
|> headers_to_id_and_anchors()
145145
|> Enum.map_reduce(%{}, fn header, acc ->
146146
# TODO Duplicates some of the logic of link_headings/3
147147
case Map.fetch(acc, header.id) do
@@ -156,14 +156,10 @@ defmodule ExDoc.Formatter.HTML.Templates do
156156
[sections: sections]
157157
end
158158

159-
# TODO: split into sections in Formatter.HTML instead (possibly via DocAST)
160-
defp extract_headers(content) do
161-
~r/<h2.*?>(.*?)<\/h2>/m
162-
|> Regex.scan(content, capture: :all_but_first)
163-
|> List.flatten()
164-
|> Enum.filter(&(&1 != ""))
165-
|> Enum.map(&ExDoc.Utils.strip_tags/1)
166-
|> Enum.map(&%{id: &1, anchor: URI.encode(text_to_id(&1))})
159+
defp headers_to_id_and_anchors(headers) do
160+
Enum.map(headers, fn text ->
161+
%{id: text, anchor: URI.encode(text_to_id(text))}
162+
end)
167163
end
168164

169165
def module_summary(module_node) do

test/ex_doc/doc_ast_test.exs

+25
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,31 @@ defmodule ExDoc.DocASTTest do
150150
end
151151
end
152152

153+
describe "extract_headers" do
154+
test "extracts h2 headers" do
155+
assert extract_headers("""
156+
# h1
157+
## h2-a
158+
### h3
159+
## h2-b
160+
##
161+
""") == ["h2-a", "h2-b"]
162+
end
163+
164+
test "trims whitespace and preserve HTML entities" do
165+
assert extract_headers("""
166+
# h1
167+
##\s\s\sh2\s<&>\sh2\s\s\s
168+
""") == ["h2 <&> h2"]
169+
end
170+
171+
defp extract_headers(markdown) do
172+
markdown
173+
|> ExDoc.DocAST.parse!("text/markdown")
174+
|> ExDoc.DocAST.extract_headers()
175+
end
176+
end
177+
153178
describe "highlight" do
154179
test "with default class" do
155180
# Empty class

test/ex_doc/formatter/html/templates_test.exs

-44
Original file line numberDiff line numberDiff line change
@@ -324,50 +324,6 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do
324324
] = create_sidebar_items(%{modules: nodes}, [])["modules"]
325325
end
326326

327-
test "outputs extras with headers" do
328-
item = %{content: nil, group: nil, id: nil, title: nil}
329-
330-
assert create_sidebar_items(%{}, [%{item | content: "<h2>Foo</h2><h2>Bar</h2>"}])["extras"] ==
331-
[
332-
%{
333-
"group" => "",
334-
"headers" => [
335-
%{"anchor" => "foo", "id" => "Foo"},
336-
%{"anchor" => "bar", "id" => "Bar"}
337-
],
338-
"id" => "",
339-
"title" => ""
340-
}
341-
]
342-
343-
assert create_sidebar_items(%{}, [%{item | content: "<h2>Foo</h2>\n<h2>Bar</h2>"}])[
344-
"extras"
345-
] ==
346-
[
347-
%{
348-
"group" => "",
349-
"headers" => [
350-
%{"anchor" => "foo", "id" => "Foo"},
351-
%{"anchor" => "bar", "id" => "Bar"}
352-
],
353-
"id" => "",
354-
"title" => ""
355-
}
356-
]
357-
358-
assert create_sidebar_items(%{}, [%{item | content: "<h2>Foo</h2><h2></h2>"}])["extras"] ==
359-
[
360-
%{
361-
"group" => "",
362-
"headers" => [
363-
%{"anchor" => "foo", "id" => "Foo"}
364-
],
365-
"id" => "",
366-
"title" => ""
367-
}
368-
]
369-
end
370-
371327
test "builds sections out of moduledocs", context do
372328
names = [CompiledWithDocs, CompiledWithoutDocs, DuplicateHeadings]
373329
config = doc_config(context)

test/ex_doc/formatter/html_test.exs

+2-2
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ defmodule ExDoc.Formatter.HTMLTest do
617617
"headers" => [
618618
%{"anchor" => "heading-without-content", "id" => "Heading without content"},
619619
%{"anchor" => "header-sample", "id" => "Header sample"},
620-
%{"anchor" => "more-than", "id" => "more &gt; than"}
620+
%{"anchor" => "more-than", "id" => "more > than"}
621621
]
622622
},
623623
%{"id" => "livebookfile"},
@@ -768,7 +768,7 @@ defmodule ExDoc.Formatter.HTMLTest do
768768
"headers" => [
769769
%{"anchor" => "heading-without-content", "id" => "Heading without content"},
770770
%{"anchor" => "header-sample", "id" => "Header sample"},
771-
%{"anchor" => "more-than", "id" => "more &gt; than"}
771+
%{"anchor" => "more-than", "id" => "more > than"}
772772
]
773773
}
774774
] = Jason.decode!(content)["extras"]

0 commit comments

Comments
 (0)