Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c6e237a
feat: add Markdown formatter
yordis Oct 2, 2025
7297cd3
fix
yordis Oct 29, 2025
1e5c09f
refactor: rename to_markdown_string to to_markdown and update related…
yordis Oct 29, 2025
b4a4ba5
refactor: simplify to_markdown function by removing post-processing a…
yordis Oct 29, 2025
9770ce7
feat: implement node_synopsis function to extract and format document…
yordis Oct 29, 2025
97cdfa0
style: update comments for clarity and improve code readability in do…
yordis Oct 29, 2025
c1970a7
style: add period to documentation comments for consistency in EPUB a…
yordis Oct 29, 2025
abe0e26
style: add period to documentation comment for module_page function i…
yordis Oct 29, 2025
68d0b02
Update lib/ex_doc/formatter/markdown/templates/detail_template.eex
yordis Oct 31, 2025
2c69df3
Update lib/ex_doc/formatter/markdown/templates/module_template.eex
yordis Oct 31, 2025
e21f6f3
Update lib/ex_doc/formatter/markdown/templates/module_template.eex
yordis Oct 31, 2025
daf8d8d
Update lib/ex_doc/formatter/markdown/templates/summary_template.eex
yordis Oct 31, 2025
fb2cb64
Update lib/ex_doc/formatter/markdown/templates.ex
yordis Oct 31, 2025
06cf685
Update lib/ex_doc/formatter/markdown/templates.ex
yordis Oct 31, 2025
4180268
Update lib/ex_doc/formatter/markdown/templates.ex
yordis Oct 31, 2025
71bf958
Update lib/ex_doc/formatter/markdown/templates.ex
yordis Oct 31, 2025
8bd9430
Update test/ex_doc/formatter/markdown_test.exs
yordis Oct 31, 2025
7e5d02b
Update lib/ex_doc/formatter/markdown/templates/nav_template.eex
yordis Oct 31, 2025
431d6ad
Update lib/ex_doc/formatter/markdown/templates.ex
yordis Oct 31, 2025
066af42
Update lib/ex_doc/formatter/markdown/templates.ex
yordis Oct 31, 2025
76f7228
Update lib/ex_doc/formatter/markdown/templates.ex
yordis Oct 31, 2025
bdf6826
Update lib/ex_doc/formatter/markdown/templates.ex
yordis Oct 31, 2025
6bcdbf4
Update lib/ex_doc/formatter/markdown/templates.ex
yordis Oct 31, 2025
9906ad5
Update lib/ex_doc/formatter/markdown.ex
yordis Oct 31, 2025
4059a8d
Update lib/ex_doc/formatter/markdown/templates.ex
yordis Oct 31, 2025
5f451e2
Update lib/ex_doc/formatter/markdown/templates.ex
yordis Oct 31, 2025
0e8111b
Update lib/ex_doc/doc_ast.ex
yordis Oct 31, 2025
c9ab715
Update test/ex_doc/formatter/markdown_test.exs
yordis Oct 31, 2025
6cdb643
Update test/ex_doc/formatter/markdown_test.exs
yordis Oct 31, 2025
f71c480
Refactor normalize_output function in markdown formatter to use regex…
yordis Oct 31, 2025
512fac1
Refactor formatter module name handling and improve markdown template…
yordis Oct 31, 2025
2994e6c
Add tests for DocAST.to_markdown/1 functionality
yordis Oct 31, 2025
2286b77
Refactor text truncation logic in markdown formatter for improved rea…
yordis Oct 31, 2025
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
62 changes: 62 additions & 0 deletions lib/ex_doc/doc_ast.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,68 @@ defmodule ExDoc.DocAST do
Enum.map(attrs, fn {key, val} -> " #{key}=\"#{ExDoc.Utils.h(val)}\"" end)
end

@doc """
Transform AST into markdown string.
"""
def to_markdown(ast)

def to_markdown(binary) when is_binary(binary) do
ExDoc.Utils.h(binary)
end

def to_markdown(list) when is_list(list) do
Enum.map_join(list, "", &to_markdown/1)
end

def to_markdown({:comment, _attrs, inner, _meta}) do
"<!--#{inner}-->"
end

def to_markdown({:code, attrs, inner, _meta}) do
lang = attrs[:class] || ""

"""
```#{lang}
#{inner}
```
"""
end

def to_markdown({:a, attrs, inner, _meta}) do
"[#{to_markdown(inner)}](#{attrs[:href]})"
end

def to_markdown({:hr, _attrs, _inner, _meta}) do
"\n\n---\n\n"
end

def to_markdown({:p, _attrs, inner, _meta}) do
to_markdown(inner) <> "\n\n"
end

def to_markdown({:br, _attrs, _inner, _meta}) do
"\n\n"
end

def to_markdown({:img, attrs, _inner, _meta}) do
alt = attrs[:alt] || ""
title = attrs[:title] || ""
"![#{alt}](#{attrs[:src]} \"#{title}\")"
end

# Ignoring these: area base col command embed input keygen link meta param source track wbr
def to_markdown({tag, _attrs, _inner, _meta}) when tag in @void_elements do
""
end

def to_markdown({_tag, _attrs, inner, %{verbatim: true}}) do
Enum.join(inner, "")
end

def to_markdown({_tag, _attrs, inner, _meta}) do
to_markdown(inner)
end

## parse markdown

defp parse_markdown(markdown, opts) do
Expand Down
23 changes: 15 additions & 8 deletions lib/ex_doc/formatter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ defmodule ExDoc.Formatter do

specs = Enum.map(child_node.specs, &language.autolink_spec(&1, autolink_opts))
child_node = %{child_node | specs: specs}
render_doc(child_node, language, autolink_opts, opts)
render_doc(child_node, ext, language, autolink_opts, opts)
end

%{render_doc(group, language, autolink_opts, opts) | docs: docs}
%{render_doc(group, ext, language, autolink_opts, opts) | docs: docs}
end

%{
render_doc(node, language, [{:id, node.id} | autolink_opts], opts)
render_doc(node, ext, language, [{:id, node.id} | autolink_opts], opts)
| docs_groups: docs_groups
}
end,
Expand Down Expand Up @@ -117,11 +117,11 @@ defmodule ExDoc.Formatter do

# Helper functions

defp render_doc(%{doc: nil} = node, _language, _autolink_opts, _opts),
defp render_doc(%{doc: nil} = node, _ext, _language, _autolink_opts, _opts),
do: node

defp render_doc(%{doc: doc} = node, language, autolink_opts, opts) do
doc = autolink_and_highlight(doc, language, autolink_opts, opts)
defp render_doc(%{doc: doc} = node, ext, language, autolink_opts, opts) do
doc = autolink_and_render(doc, ext, language, autolink_opts, opts)
%{node | doc: doc}
end

Expand All @@ -137,7 +137,13 @@ defmodule ExDoc.Formatter do
mod_id <> "." <> id
end

defp autolink_and_highlight(doc, language, autolink_opts, opts) do
defp autolink_and_render(doc, ".md", language, autolink_opts, opts) do
doc
|> language.autolink_doc(autolink_opts)
|> ExDoc.DocAST.highlight(language, opts)
end

defp autolink_and_render(doc, _html_ext, language, autolink_opts, opts) do
doc
|> language.autolink_doc(autolink_opts)
|> ExDoc.DocAST.highlight(language, opts)
Expand Down Expand Up @@ -187,6 +193,7 @@ defmodule ExDoc.Formatter do

source_file = validate_extra_string!(input_options, :source) || input
opts = [file: source_file, line: 1]
ext = Keyword.fetch!(autolink_opts, :ext)

{extension, source, ast} =
case extension_name(input) do
Expand All @@ -202,7 +209,7 @@ defmodule ExDoc.Formatter do
source
|> Markdown.to_ast(opts)
|> ExDoc.DocAST.add_ids_to_headers([:h2, :h3])
|> autolink_and_highlight(language, [file: input] ++ autolink_opts, opts)
|> autolink_and_render(ext, language, [file: input] ++ autolink_opts, opts)

{extension, source, ast}

Expand Down
4 changes: 2 additions & 2 deletions lib/ex_doc/formatter/epub/templates.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ defmodule ExDoc.Formatter.EPUB.Templates do
defp render_doc(ast), do: ast && ExDoc.DocAST.to_string(ast)

@doc """
Generate content from the module template for a given `node`
Generate content from the module template for a given `node`.
"""
def module_page(config, module_node) do
module_template(config, module_node)
end

@doc """
Generated ID for static file
Generated ID for static file.
"""
def static_file_to_id(static_file) do
static_file |> Path.basename() |> text_to_id()
Expand Down
2 changes: 1 addition & 1 deletion lib/ex_doc/formatter/html/templates.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ defmodule ExDoc.Formatter.HTML.Templates do
]

@doc """
Generate content from the module template for a given `node`
Generate content from the module template for a given `node`.
"""
def module_page(module_node, config) do
module_template(config, module_node)
Expand Down
177 changes: 177 additions & 0 deletions lib/ex_doc/formatter/markdown.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
defmodule ExDoc.Formatter.MARKDOWN do
@moduledoc false

alias __MODULE__.{Templates}
alias ExDoc.Formatter
alias ExDoc.Utils

@doc """
Generates Markdown documentation for the given modules.
"""
@spec run([ExDoc.ModuleNode.t()], [ExDoc.ModuleNode.t()], ExDoc.Config.t()) :: String.t()
def run(project_nodes, filtered_modules, config) when is_map(config) do
Utils.unset_warned()

config = normalize_config(config)
File.rm_rf!(config.output)
File.mkdir_p!(config.output)

extras = Formatter.build_extras(config, ".md")

project_nodes =
project_nodes
|> Formatter.render_all(filtered_modules, ".md", config, highlight_tag: "samp")

nodes_map = %{
modules: Formatter.filter_list(:module, project_nodes),
tasks: Formatter.filter_list(:task, project_nodes)
}

config = %{config | extras: extras}

generate_nav(config, nodes_map)
generate_extras(config)
generate_list(config, nodes_map.modules)
generate_list(config, nodes_map.tasks)
generate_llm_index(config, nodes_map)

config.output |> Path.join("index.md") |> Path.relative_to_cwd()
end

defp normalize_config(config) do
output =
config.output
|> Path.expand()
|> Path.join("markdown")

%{config | output: output}
end

defp normalize_output(output) do
output
|> String.replace(["\r\n", "\n"], "\n")
|> String.replace(~r/\n{3,}/, "\n\n")
end

defp generate_nav(config, nodes) do
nodes =
Map.update!(nodes, :modules, fn modules ->
modules |> Enum.chunk_by(& &1.group) |> Enum.map(&{hd(&1).group, &1})
end)

content =
Templates.nav_template(config, nodes)
|> normalize_output()

File.write("#{config.output}/index.md", content)
end

defp generate_extras(config) do
for {_title, extras} <- config.extras do
Enum.each(extras, fn %{id: id, source: content} ->
output = "#{config.output}/#{id}.md"

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

File.write!(output, normalize_output(content))
end)
end
end

defp generate_list(config, nodes) do
nodes
|> Task.async_stream(&generate_module_page(&1, config), timeout: :infinity)
|> Enum.map(&elem(&1, 1))
end

## Helpers

defp generate_module_page(module_node, config) do
content =
Templates.module_page(config, module_node)
|> normalize_output()

File.write("#{config.output}/#{module_node.id}.md", content)
end

defp generate_llm_index(config, nodes_map) do
content = generate_llm_index_content(config, nodes_map)
File.write("#{config.output}/llms.txt", content)
end

defp generate_llm_index_content(config, nodes_map) do
project_info = """
# #{config.project} #{config.version}
#{config.project} documentation index for Large Language Models.
## Modules
"""

modules_info =
nodes_map.modules
|> Enum.map(fn module_node ->
"- **#{module_node.title}** (#{module_node.id}.md): #{module_node.doc |> ExDoc.DocAST.synopsis() |> extract_plain_text()}"
end)
|> Enum.join("\n")

tasks_info =
if length(nodes_map.tasks) > 0 do
tasks_list =
nodes_map.tasks
|> Enum.map(fn task_node ->
"- **#{task_node.title}** (#{task_node.id}.md): #{task_node.doc |> ExDoc.DocAST.synopsis() |> extract_plain_text()}"
end)
|> Enum.join("\n")

"\n\n## Mix Tasks\n\n" <> tasks_list
else
""
end

extras_info =
if is_list(config.extras) and length(config.extras) > 0 do
extras_list =
config.extras
|> Enum.flat_map(fn
{_group, extras} when is_list(extras) -> extras
_ -> []
end)
|> Enum.map(fn extra ->
"- **#{extra.title}** (#{extra.id}.md): #{extra.title}"
end)
|> Enum.join("\n")

if extras_list == "" do
""
else
"\n\n## Guides\n\n" <> extras_list
end
else
""
end

project_info <> modules_info <> tasks_info <> extras_info
end

defp extract_plain_text(html) when is_binary(html) do
html
|> String.replace(~r/<[^>]*>/, "")
|> String.replace(~r/\s+/, " ")
|> String.trim()
|> case do
"" ->
"No documentation available"

text ->
text
|> String.slice(0, 150)
|> then(fn s -> if String.length(s) == 150, do: s <> "...", else: s end)
end
end

defp extract_plain_text(_), do: "No documentation available"
end
Loading