Skip to content

Commit 3efa2d7

Browse files
committed
Convert link headings into DocAST traversals
1 parent bb2a1ad commit 3efa2d7

File tree

17 files changed

+93
-228
lines changed

17 files changed

+93
-228
lines changed

assets/css/content/general.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,10 @@
205205
.content-inner .section-heading i {
206206
font-size: var(--icon-size);
207207
color: var(--mainLight);
208-
margin-top: 0.1em;
208+
top: -2px;
209209
margin-left: calc(-1 * (var(--icon-size) + var(--icon-spacing)));
210210
padding-right: var(--icon-spacing);
211+
position: relative;
211212
opacity: 0;
212213
}
213214

formatters/html/dist/html-elixir-J3PIVQVA.css

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

formatters/html/dist/html-elixir-M6JNNWMH.css

Lines changed: 0 additions & 6 deletions
This file was deleted.

formatters/html/dist/html-erlang-5OIFJN4X.css

Lines changed: 0 additions & 6 deletions
This file was deleted.

formatters/html/dist/html-erlang-ZK43ZOAC.css

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/ex_doc/formatter/epub.ex

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,10 @@ defmodule ExDoc.Formatter.EPUB do
6262

6363
defp generate_extras(config) do
6464
for {_title, extras} <- config.extras,
65-
extra_config <- extras,
66-
not is_map_key(extra_config, :url) do
67-
%{id: id, title: title, title_content: title_content, content: content} = extra_config
68-
69-
output = "#{config.output}/OEBPS/#{id}.xhtml"
70-
html = Templates.extra_template(config, title, title_content, content)
65+
node <- extras,
66+
not is_map_key(node, :url) do
67+
output = "#{config.output}/OEBPS/#{node.id}.xhtml"
68+
html = Templates.extra_template(config, node)
7169

7270
if File.regular?(output) do
7371
Utils.warn("file #{Path.relative_to_cwd(output)} already exists", [])

lib/ex_doc/formatter/epub/templates.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ defmodule ExDoc.Formatter.EPUB.Templates do
99
alias ExDoc.Formatter.HTML.Templates, as: H
1010
alias ExDoc.Formatter.EPUB.Assets
1111

12+
# The actual rendering happens here
13+
defp render_doc(ast), do: ast && ExDoc.DocAST.to_string(ast)
14+
1215
@doc """
1316
Generate content from the module template for a given `node`
1417
"""
@@ -76,7 +79,7 @@ defmodule ExDoc.Formatter.EPUB.Templates do
7679
:def,
7780
:extra_template,
7881
Path.expand("templates/extra_template.eex", __DIR__),
79-
[:config, :title, :title_content, :content],
82+
[:config, :node],
8083
trim: true
8184
)
8285

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
<%= head_template(config, title) %>
1+
<%= head_template(config, node.title) %>
22
<h1 id="content">
3-
<%=h title_content %>
3+
<%=h node.title_content %>
44
</h1>
5-
<%= H.link_headings(content) %>
5+
<%= render_doc(node.doc) %>
66
<%= before_closing_body_tag(config, :epub) %>
77
</body>
88
</html>

lib/ex_doc/formatter/epub/templates/module_template.eex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
</div>
1010
<% end %>
1111

12-
<%= if doc = module.rendered_doc do %>
12+
<%= if doc = module.doc do %>
1313
<section id="moduledoc" class="docstring">
14-
<%= H.link_moduledoc_headings(doc) %>
14+
<%= render_doc(doc) %>
1515
</section>
1616
<% end %>
1717

lib/ex_doc/formatter/html.ex

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ defmodule ExDoc.Formatter.HTML do
3232
tasks: filter_list(:task, project_nodes)
3333
}
3434

35+
# TODO: api reference should not be treated as an extra
3536
extras =
3637
if config.api_reference do
3738
[build_api_reference(nodes_map, config) | extras]
@@ -125,8 +126,8 @@ defmodule ExDoc.Formatter.HTML do
125126
do: node
126127

127128
defp render_doc(%{doc: doc} = node, language, autolink_opts, opts) do
128-
rendered = autolink_and_render(doc, language, autolink_opts, opts)
129-
%{node | rendered_doc: rendered}
129+
doc = autolink_and_highlight(doc, language, autolink_opts, opts)
130+
%{node | doc: doc, rendered_doc: ExDoc.DocAST.to_string(doc)}
130131
end
131132

132133
defp id(%{id: mod_id}, %{id: "c:" <> id}) do
@@ -141,11 +142,10 @@ defmodule ExDoc.Formatter.HTML do
141142
mod_id <> "." <> id
142143
end
143144

144-
defp autolink_and_render(doc, language, autolink_opts, opts) do
145+
defp autolink_and_highlight(doc, language, autolink_opts, opts) do
145146
doc
146147
|> language.autolink_doc(autolink_opts)
147148
|> ExDoc.DocAST.highlight(language, opts)
148-
|> ExDoc.DocAST.to_string()
149149
end
150150

151151
defp output_setup(build, config) do
@@ -314,13 +314,13 @@ defmodule ExDoc.Formatter.HTML do
314314
~s{API Reference <small class="app-vsn">#{config.project} v#{config.version}</small>}
315315

316316
%{
317-
content: api_reference,
318317
group: nil,
319318
id: "api-reference",
320319
source_path: nil,
321320
source_url: config.source_url,
322321
title: "API Reference",
323322
title_content: title_content,
323+
content: api_reference,
324324
headers:
325325
if(nodes_map.modules != [], do: [{:h2, "Modules", "modules"}], else: []) ++
326326
if(nodes_map.tasks != [], do: [{:h2, "Mix Tasks", "mix-tasks"}], else: [])
@@ -425,6 +425,7 @@ defmodule ExDoc.Formatter.HTML do
425425
|> Markdown.to_ast(opts)
426426
|> ExDoc.DocAST.add_ids_to_headers([:h2, :h3])
427427
|> sectionize(extension)
428+
|> autolink_and_highlight(language, [file: input] ++ autolink_opts, opts)
428429

429430
{source, ast}
430431

@@ -441,7 +442,6 @@ defmodule ExDoc.Formatter.HTML do
441442

442443
title_text = title_ast && ExDoc.DocAST.text(title_ast)
443444
title_html = title_ast && ExDoc.DocAST.to_string(title_ast)
444-
content_html = autolink_and_render(ast, language, [file: input] ++ autolink_opts, opts)
445445

446446
group = GroupMatcher.match_extra(groups, input)
447447
title = input_options[:title] || title_text || filename_to_title(input)
@@ -454,13 +454,13 @@ defmodule ExDoc.Formatter.HTML do
454454
source: source,
455455
group: group,
456456
id: id,
457+
doc: ast,
457458
source_path: source_path,
458459
source_url: source_url,
459460
search_data: search_data,
460461
title: title,
461462
title_content: title_html || title,
462-
# TODO: Remove these fields but first we would need to make API reference return DocAST
463-
content: content_html,
463+
# Remove this field when API reference is no longer treated as an extra
464464
headers: ExDoc.DocAST.extract_headers_with_ids(ast, [:h2])
465465
}
466466
end

lib/ex_doc/formatter/html/templates.ex

Lines changed: 36 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -172,94 +172,49 @@ defmodule ExDoc.Formatter.HTML.Templates do
172172
defp sidebar_type(:livemd), do: "extras"
173173
defp sidebar_type(:extra), do: "extras"
174174

175-
# TODO: Adding the link headings must be done via DocAST instead of using regexes.
175+
@section_header_class_name "section-heading"
176176

177177
@doc """
178-
Add link headings for the given `content`.
178+
Renders the document in the page.
179179
180-
IDs are prefixed with `prefix`.
181-
182-
We only link `h2` and `h3` headers. This is kept consistent in ExDoc.SearchData.
180+
For now it enriches the document by adding fancy anchors
181+
around h2 and h3 tags with IDs.
183182
"""
184-
@spec link_headings(String.t() | nil, String.t()) :: String.t() | nil
185-
def link_headings(content, prefix \\ "")
186-
def link_headings(nil, _), do: nil
187-
188-
def link_headings(content, prefix) do
189-
~r/<(h[23]).*?>(.*?)<\/\1>/m
190-
|> Regex.scan(content)
191-
|> Enum.reduce({content, %{}}, fn [match, tag, title], {content, occurrences} ->
192-
possible_id = title |> ExDoc.Utils.strip_tags() |> text_to_id()
193-
id_occurred = Map.get(occurrences, possible_id, 0)
194-
195-
anchor_id = if id_occurred >= 1, do: "#{possible_id}-#{id_occurred}", else: possible_id
196-
replacement = link_heading(match, tag, title, anchor_id, prefix)
197-
linked_content = String.replace(content, match, replacement, global: false)
198-
incremented_occs = Map.put(occurrences, possible_id, id_occurred + 1)
199-
{linked_content, incremented_occs}
200-
end)
201-
|> elem(0)
202-
end
203-
204-
@class_separator " "
205-
defp link_heading(match, _tag, _title, "", _prefix), do: match
206-
207-
defp link_heading(match, tag, title, id, prefix) do
208-
section_header_class_name = "section-heading"
209-
210-
# NOTE: This addition is mainly to preserve the previous `class` attributes
211-
# from the headers, in case there is one. Now with the _admonition_ text
212-
# block, we inject CSS classes. So far, the supported classes are:
213-
# `warning`, `info`, `error`, and `neutral`.
214-
#
215-
# The Markdown syntax that we support for the admonition text
216-
# blocks is something like this:
217-
#
218-
# > ### Never open this door! {: .warning}
219-
# >
220-
# > ...
221-
#
222-
# That should produce the following HTML:
223-
#
224-
# <blockquote>
225-
# <h3 class="warning">Never open this door!</h3>
226-
# <p>...</p>
227-
# </blockquote>
228-
#
229-
# The original implementation discarded the previous CSS classes. Instead,
230-
# it was setting `#{section_header_class_name}` as the only CSS class
231-
# associated with the given header.
232-
class_attribute =
233-
case Regex.named_captures(~r/<h[23].*?(\sclass="(?<class>[^"]+)")?.*?>/, match) do
234-
%{"class" => ""} ->
235-
section_header_class_name
236-
237-
%{"class" => previous_classes} ->
238-
# Let's make sure that the `section_header_class_name` is not already
239-
# included in the previous classes for the header
240-
previous_classes
241-
|> String.split(@class_separator)
242-
|> Enum.reject(&(&1 == section_header_class_name))
243-
|> Enum.join(@class_separator)
244-
|> Kernel.<>(" #{section_header_class_name}")
245-
end
246-
247-
"""
248-
<#{tag} id="#{prefix}#{id}" class="#{class_attribute}">
249-
<a href="##{prefix}#{id}" class="hover-link">
250-
<i class="ri-link-m" aria-hidden="true"></i>
251-
</a>
252-
<span class="text">#{title}</span>
253-
</#{tag}>
254-
"""
255-
end
183+
def render_doc(nil), do: ""
256184

257-
def link_moduledoc_headings(content) do
258-
link_headings(content, "module-")
185+
def render_doc(ast) do
186+
ast
187+
|> add_fancy_anchors()
188+
|> ExDoc.DocAST.to_string()
259189
end
260190

261-
def link_detail_headings(content, prefix) do
262-
link_headings(content, prefix <> "-")
191+
defp add_fancy_anchors(ast) do
192+
ExDoc.DocAST.map_tags(ast, fn
193+
{tag, attrs, inner, meta} = ast when tag in [:h2, :h3] ->
194+
if id = Keyword.get(attrs, :id) do
195+
attrs =
196+
Keyword.update(
197+
attrs,
198+
:class,
199+
@section_header_class_name,
200+
&(&1 <> " " <> @section_header_class_name)
201+
)
202+
203+
{tag, attrs,
204+
[
205+
{:a, [href: "##{id}", class: "hover-link"],
206+
[
207+
{:i, [class: "ri-link-m", "aria-hidden": "true"], [], %{}}
208+
], %{}},
209+
{:span, [class: "text"], inner, %{}}
210+
], meta}
211+
else
212+
ast
213+
end
214+
215+
ast ->
216+
ast
217+
end)
263218
end
264219

265220
templates = [

lib/ex_doc/formatter/html/templates/detail_template.eex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@
3333
</div>
3434
<% end %>
3535

36-
<%= link_detail_headings(node.rendered_doc, enc(node.id)) %>
36+
<%= render_doc(node.doc) %>
3737
</section>
3838
</section>

lib/ex_doc/formatter/html/templates/extra_template.eex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
</div>
2727
<% end %>
2828

29-
<%= link_headings(node.content) %>
29+
<%= node[:content] || render_doc(node.doc) %>
3030
</div>
3131

3232
<div class="bottom-actions" id="bottom-actions">

lib/ex_doc/formatter/html/templates/module_template.eex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
</div>
2525
<% end %>
2626

27-
<%= if doc = module.rendered_doc do %>
27+
<%= if doc = module.doc do %>
2828
<section id="moduledoc">
29-
<%= link_moduledoc_headings(doc) %>
29+
<%= render_doc(doc) %>
3030
</section>
3131
<% end %>
3232
</div>

lib/ex_doc/retriever.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ defmodule ExDoc.Retriever do
177177
nil
178178
end
179179

180-
# TODO: Consider perhaps moving auto-linking here.
180+
# TODO: Consider moving auto-linking here.
181181
defp normalize_doc_ast(doc_ast, prefix) do
182182
doc_ast
183183
|> DocAST.add_ids_to_headers([:h2, :h3], prefix)

test/ex_doc/formatter/epub/templates_test.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ defmodule ExDoc.Formatter.EPUB.TemplatesTest do
108108
assert content =~ ~s{<h1 class="section-heading">Summary</h1>}
109109

110110
assert content =~
111-
~r{<h2 id="module-example-unicode-escaping" class="section-heading">.*Example.*</h2>}ms
111+
~r{<h2 id="module-example-unicode-escaping">.*Example.*</h2>}ms
112112

113113
assert content =~
114-
~r{<h3 id="module-example-h3-heading" class="section-heading">.*Example H3 heading.*</h3>}ms
114+
~r{<h3 id="module-example-h3-heading">.*Example H3 heading.*</h3>}ms
115115

116116
assert content =~
117117
~r{moduledoc.*Example.*<samp class="nc">CompiledWithDocs</samp><samp class="o">\.</samp><samp class="n">example</samp>.*}ms

0 commit comments

Comments
 (0)