From 6265dd4b00dc8fc438e5cb15f2c9b7541fe6ca5a Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Tue, 26 Nov 2024 02:10:08 -0500 Subject: [PATCH 01/12] Port Markdown formatter from HTML --- lib/ex_doc/formatter/markdown.ex | 507 ++++++++++++++++++ lib/ex_doc/formatter/markdown/templates.ex | 356 ++++++++++++ .../api_reference_entry_template.eex | 11 + .../templates/api_reference_template.eex | 21 + .../markdown/templates/detail_template.eex | 36 ++ .../markdown/templates/extra_template.eex | 60 +++ .../markdown/templates/footer_template.eex | 44 ++ .../markdown/templates/head_template.eex | 43 ++ .../markdown/templates/module_template.eex | 57 ++ .../markdown/templates/not_found_template.eex | 15 + .../markdown/templates/redirect_template.eex | 10 + .../markdown/templates/search_template.eex | 12 + .../markdown/templates/sidebar_template.eex | 92 ++++ .../markdown/templates/summary_template.eex | 18 + lib/ex_doc/markdown/assets.ex | 14 + 15 files changed, 1296 insertions(+) create mode 100644 lib/ex_doc/formatter/markdown.ex create mode 100644 lib/ex_doc/formatter/markdown/templates.ex create mode 100644 lib/ex_doc/formatter/markdown/templates/api_reference_entry_template.eex create mode 100644 lib/ex_doc/formatter/markdown/templates/api_reference_template.eex create mode 100644 lib/ex_doc/formatter/markdown/templates/detail_template.eex create mode 100644 lib/ex_doc/formatter/markdown/templates/extra_template.eex create mode 100644 lib/ex_doc/formatter/markdown/templates/footer_template.eex create mode 100644 lib/ex_doc/formatter/markdown/templates/head_template.eex create mode 100644 lib/ex_doc/formatter/markdown/templates/module_template.eex create mode 100644 lib/ex_doc/formatter/markdown/templates/not_found_template.eex create mode 100644 lib/ex_doc/formatter/markdown/templates/redirect_template.eex create mode 100644 lib/ex_doc/formatter/markdown/templates/search_template.eex create mode 100644 lib/ex_doc/formatter/markdown/templates/sidebar_template.eex create mode 100644 lib/ex_doc/formatter/markdown/templates/summary_template.eex create mode 100644 lib/ex_doc/markdown/assets.ex diff --git a/lib/ex_doc/formatter/markdown.ex b/lib/ex_doc/formatter/markdown.ex new file mode 100644 index 000000000..be566de92 --- /dev/null +++ b/lib/ex_doc/formatter/markdown.ex @@ -0,0 +1,507 @@ +defmodule ExDoc.Formatter.Markdown do + @moduledoc false + + alias __MODULE__.Assets + alias __MODULE__.Templates + alias ExDoc.GroupMatcher + alias ExDoc.Markdown + alias ExDoc.Utils + + # @main "api-reference" + @assets_dir "assets" + + @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 = %{config | output: Path.expand(config.output)} + + build = Path.join(config.output, ".build") + output_setup(build, config) + + project_nodes = render_all(project_nodes, filtered_modules, ".md", config, []) + extras = build_extras(config, ".md") + + # Generate search early on without api reference in extras + static_files = generate_assets(".", default_assets(config), config) + + # TODO: Move this categorization to the language + nodes_map = %{ + modules: filter_list(:module, project_nodes), + tasks: filter_list(:task, project_nodes) + } + + extras = + if config.api_reference do + [build_api_reference(nodes_map, config) | extras] + else + extras + end + + all_files = + static_files ++ + generate_extras(nodes_map, extras, config) ++ + generate_logo(@assets_dir, config) ++ + generate_list(nodes_map.modules, nodes_map, config) ++ + generate_list(nodes_map.tasks, nodes_map, config) + + generate_build(Enum.sort(all_files), build) + config.output |> Path.join("index.md") |> Path.relative_to_cwd() + end + + @doc """ + Autolinks and renders all docs. + """ + def render_all(project_nodes, filtered_modules, ext, config, opts) do + base = [ + apps: config.apps, + deps: config.deps, + ext: ext, + extras: extra_paths(config), + skip_undefined_reference_warnings_on: config.skip_undefined_reference_warnings_on, + skip_code_autolink_to: config.skip_code_autolink_to, + filtered_modules: filtered_modules + ] + + project_nodes + |> Task.async_stream( + fn node -> + language = node.language + + autolink_opts = + [ + current_module: node.module, + file: node.moduledoc_file, + line: node.moduledoc_line, + module_id: node.id, + language: language + ] ++ base + + docs = + for child_node <- node.docs do + id = id(node, child_node) + + autolink_opts = + autolink_opts ++ + [ + id: id, + line: child_node.doc_line, + file: child_node.doc_file, + current_kfa: {:function, child_node.name, child_node.arity} + ] + + 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) + end + + typespecs = + for child_node <- node.typespecs do + id = id(node, child_node) + + autolink_opts = + autolink_opts ++ + [ + id: id, + line: child_node.doc_line, + file: child_node.doc_file, + current_kfa: {child_node.type, child_node.name, child_node.arity} + ] + + child_node = %{ + child_node + | spec: language.autolink_spec(child_node.spec, autolink_opts) + } + + render_doc(child_node, language, autolink_opts, opts) + end + + %{ + render_doc(node, language, [{:id, node.id} | autolink_opts], opts) + | docs: docs, + typespecs: typespecs + } + end, + timeout: :infinity + ) + |> Enum.map(&elem(&1, 1)) + end + + defp render_doc(%{doc: nil} = node, _language, _autolink_opts, _opts), + do: node + + defp render_doc(%{doc: doc} = node, language, autolink_opts, opts) do + rendered = autolink_and_render(doc, language, autolink_opts, opts) + %{node | rendered_doc: rendered} + end + + defp id(%{id: mod_id}, %{id: "c:" <> id}) do + "c:" <> mod_id <> "." <> id + end + + defp id(%{id: mod_id}, %{id: "t:" <> id}) do + "t:" <> mod_id <> "." <> id + end + + defp id(%{id: mod_id}, %{id: id}) do + mod_id <> "." <> id + end + + defp autolink_and_render(doc, language, autolink_opts, opts) do + doc + |> language.autolink_doc(autolink_opts) + |> ExDoc.DocAST.to_string() + |> ExDoc.DocAST.highlight(language, opts) + end + + defp output_setup(build, config) do + if File.exists?(build) do + build + |> File.read!() + |> String.split("\n", trim: true) + |> Enum.map(&Path.join(config.output, &1)) + |> Enum.each(&File.rm/1) + + File.rm(build) + else + File.rm_rf!(config.output) + File.mkdir_p!(config.output) + end + end + + defp generate_build(files, build) do + entries = Enum.map(files, &[&1, "\n"]) + File.write!(build, entries) + end + + defp generate_extras(nodes_map, extras, config) do + generated_extras = + extras + |> with_prev_next() + |> Enum.map(fn {node, prev, next} -> + filename = "#{node.id}.md" + output = "#{config.output}/#{filename}" + config = set_canonical_url(config, filename) + + refs = %{ + prev: prev && %{path: "#{prev.id}.md", title: prev.title}, + next: next && %{path: "#{next.id}.md", title: next.title} + } + + extension = node.source_path && Path.extname(node.source_path) + markdown = Templates.extra_template(config, node, extra_type(extension), nodes_map, refs) + + if File.regular?(output) do + Utils.warn("file #{Path.relative_to_cwd(output)} already exists", []) + end + + File.write!(output, markdown) + filename + end) + + generated_extras ++ copy_extras(config, extras) + end + + defp extra_type(".cheatmd"), do: :cheatmd + defp extra_type(".livemd"), do: :livemd + defp extra_type(_), do: :extra + + defp copy_extras(config, extras) do + for %{source_path: source_path, id: id} when source_path != nil <- extras, + ext = extension_name(source_path), + ext == ".livemd" do + output = "#{config.output}/#{id}#{ext}" + + File.copy!(source_path, output) + + output + end + end + + defp with_prev_next([]), do: [] + + defp with_prev_next([head | tail]) do + Enum.zip([[head | tail], [nil, head | tail], tail ++ [nil]]) + end + + @doc """ + Generate assets from configs with the given default assets. + """ + def generate_assets(namespace, defaults, %{output: output, assets: assets}) do + namespaced_assets = + if is_map(assets) do + Enum.map(assets, fn {source, target} -> {source, Path.join(namespace, target)} end) + else + IO.warn(""" + giving a binary to :assets is deprecated, please give a map from source to target instead: + + #{inspect(assets: %{assets => "assets"})} + """) + + [{assets, Path.join(namespace, "assets")}] + end + + Enum.flat_map(defaults ++ namespaced_assets, fn {dir_or_files, relative_target_dir} -> + target_dir = Path.join(output, relative_target_dir) + File.mkdir_p!(target_dir) + + cond do + is_list(dir_or_files) -> + Enum.map(dir_or_files, fn {name, content} -> + target = Path.join(target_dir, name) + File.write(target, content) + Path.relative_to(target, output) + end) + + is_binary(dir_or_files) and File.dir?(dir_or_files) -> + dir_or_files + |> File.cp_r!(target_dir, dereference_symlinks: true) + |> Enum.map(&Path.relative_to(&1, output)) + + is_binary(dir_or_files) -> + [] + + true -> + raise ":assets must be a map of source directories to target directories" + end + end) + end + + defp default_assets(config) do + [ + {Assets.dist(config.proglang), "dist"} + ] + end + + defp build_api_reference(nodes_map, config) do + api_reference = Templates.api_reference_template(nodes_map) + + title_content = + ~s{API Reference #{config.project} v#{config.version}} + + %{ + content: api_reference, + group: nil, + id: "api-reference", + source_path: nil, + source_url: config.source_url, + title: "API Reference", + title_content: title_content + } + end + + @doc """ + Builds extra nodes by normalizing the config entries. + """ + def build_extras(config, ext) do + groups = config.groups_for_extras + + language = + case config.proglang do + :erlang -> ExDoc.Language.Erlang + _ -> ExDoc.Language.Elixir + end + + source_url_pattern = config.source_url_pattern + + autolink_opts = [ + apps: config.apps, + deps: config.deps, + ext: ext, + extras: extra_paths(config), + language: language, + skip_undefined_reference_warnings_on: config.skip_undefined_reference_warnings_on, + skip_code_autolink_to: config.skip_code_autolink_to + ] + + extras = + config.extras + |> Task.async_stream( + &build_extra(&1, groups, language, autolink_opts, source_url_pattern), + timeout: :infinity + ) + |> Enum.map(&elem(&1, 1)) + + ids_count = Enum.reduce(extras, %{}, &Map.update(&2, &1.id, 1, fn c -> c + 1 end)) + + extras + |> Enum.map_reduce(1, fn extra, idx -> + if ids_count[extra.id] > 1, do: {disambiguate_id(extra, idx), idx + 1}, else: {extra, idx} + end) + |> elem(0) + |> Enum.sort_by(fn extra -> GroupMatcher.group_index(groups, extra.group) end) + end + + defp disambiguate_id(extra, discriminator) do + Map.put(extra, :id, "#{extra.id}-#{discriminator}") + end + + defp build_extra({input, input_options}, groups, language, autolink_opts, source_url_pattern) do + input = to_string(input) + id = input_options[:filename] || input |> filename_to_title() |> Utils.text_to_id() + source_file = input_options[:source] || input + opts = [file: source_file, line: 1] + + {source, ast} = + case extension_name(input) do + extension when extension in ["", ".txt"] -> + source = File.read!(input) + ast = [{:pre, [], "\n" <> source, %{}}] + {source, ast} + + extension when extension in [".md", ".livemd", ".cheatmd"] -> + source = File.read!(input) + + ast = + source + |> Markdown.to_ast(opts) + |> sectionize(extension) + + {source, ast} + + _ -> + raise ArgumentError, + "file extension not recognized, allowed extension is either .cheatmd, .livemd, .md, .txt or no extension" + end + + {title_ast, ast} = + case ExDoc.DocAST.extract_title(ast) do + {:ok, title_ast, ast} -> {title_ast, ast} + :error -> {nil, ast} + end + + title_text = title_ast && ExDoc.DocAST.text_from_ast(title_ast) + title_markdown = title_ast && ExDoc.DocAST.to_string(title_ast) + content_markdown = autolink_and_render(ast, language, [file: input] ++ autolink_opts, opts) + + group = GroupMatcher.match_extra(groups, input) + title = input_options[:title] || title_text || filename_to_title(input) + + source_path = source_file |> Path.relative_to(File.cwd!()) |> String.replace_leading("./", "") + source_url = Utils.source_url_pattern(source_url_pattern, source_path, 1) + + %{ + source: source, + content: content_markdown, + group: group, + id: id, + source_path: source_path, + source_url: source_url, + title: title, + title_content: title_markdown || title + } + end + + defp build_extra(input, groups, language, autolink_opts, source_url_pattern) do + build_extra({input, []}, groups, language, autolink_opts, source_url_pattern) + end + + defp extension_name(input) do + input + |> Path.extname() + |> String.downcase() + end + + defp sectionize(ast, ".cheatmd") do + ExDoc.DocAST.sectionize(ast, fn + {:h2, _, _, _} -> true + {:h3, _, _, _} -> true + _ -> false + end) + end + + defp sectionize(ast, _), do: ast + + defp filename_to_title(input) do + input |> Path.basename() |> Path.rootname() + end + + @doc """ + Generates the logo from config into the given directory. + """ + def generate_logo(_dir, %{logo: nil}) do + [] + end + + def generate_logo(dir, %{output: output, logo: logo}) do + generate_image(output, dir, logo, "logo") + end + + @doc """ + Generates the cover from config into the given directory. + """ + def generate_cover(_dir, %{cover: nil}) do + [] + end + + def generate_cover(dir, %{output: output, cover: cover}) do + generate_image(output, dir, cover, "cover") + end + + defp generate_image(output, dir, image, name) do + extname = + image + |> Path.extname() + |> String.downcase() + + if extname in ~w(.png .jpg .jpeg .svg) do + filename = Path.join(dir, "#{name}#{extname}") + target = Path.join(output, filename) + File.mkdir_p!(Path.dirname(target)) + File.copy!(image, target) + [filename] + else + raise ArgumentError, "image format not recognized, allowed formats are: .png, .jpg, .svg" + end + end + + def filter_list(:module, nodes) do + Enum.filter(nodes, &(&1.type != :task)) + end + + def filter_list(type, nodes) do + Enum.filter(nodes, &(&1.type == type)) + end + + defp generate_list(nodes, nodes_map, config) do + nodes + |> Task.async_stream(&generate_module_page(&1, nodes_map, config), timeout: :infinity) + |> Enum.map(&elem(&1, 1)) + end + + defp generate_module_page(module_node, nodes_map, config) do + filename = "#{module_node.id}.md" + config = set_canonical_url(config, filename) + content = Templates.module_page(module_node, nodes_map, config) + File.write!("#{config.output}/#{filename}", content) + filename + end + + defp set_canonical_url(config, filename) do + if config.canonical do + canonical_url = + config.canonical + |> String.trim_trailing("/") + |> Kernel.<>("/" <> filename) + + Map.put(config, :canonical, canonical_url) + else + config + end + end + + defp extra_paths(config) do + Map.new(config.extras, fn + path when is_binary(path) -> + base = Path.basename(path) + {base, Utils.text_to_id(Path.rootname(base))} + + {path, opts} -> + base = path |> to_string() |> Path.basename() + {base, opts[:filename] || Utils.text_to_id(Path.rootname(base))} + end) + end +end diff --git a/lib/ex_doc/formatter/markdown/templates.ex b/lib/ex_doc/formatter/markdown/templates.ex new file mode 100644 index 000000000..085c59ac7 --- /dev/null +++ b/lib/ex_doc/formatter/markdown/templates.ex @@ -0,0 +1,356 @@ +defmodule ExDoc.Formatter.Markdown.Templates do + @moduledoc false + require EEx + + import ExDoc.Utils, + only: [ + h: 1, + before_closing_body_tag: 2, + before_closing_footer_tag: 2, + before_closing_head_tag: 2, + text_to_id: 1 + ] + + @doc """ + Generate content from the module template for a given `node` + """ + def module_page(module_node, nodes_map, config) do + summary = module_summary(module_node) + module_template(config, module_node, summary, nodes_map) + end + + @doc """ + Get the full specs from a function, already in HTML form. + """ + def get_specs(%ExDoc.TypeNode{spec: spec}) do + [spec] + end + + def get_specs(%ExDoc.FunctionNode{specs: specs}) when is_list(specs) do + presence(specs) + end + + def get_specs(_node) do + nil + end + + @doc """ + Format the attribute type used to define the spec of the given `node`. + """ + def format_spec_attribute(module, node) do + module.language.format_spec_attribute(node) + end + + @doc """ + Get defaults clauses. + """ + def get_defaults(%{defaults: defaults}) do + defaults + end + + def get_defaults(_) do + [] + end + + @doc """ + Get the pretty name of a function node + """ + def pretty_type(%{type: t}) do + Atom.to_string(t) + end + + @doc """ + Returns the HTML formatted title for the module page. + """ + def module_type(%{type: :task}), do: "" + def module_type(%{type: :module}), do: "" + def module_type(%{type: type}), do: "#{type}" + + @doc """ + Gets the first paragraph of the documentation of a node. It strips + surrounding white-spaces and trailing `:`. + + If `doc` is `nil`, it returns `nil`. + """ + @spec synopsis(String.t()) :: String.t() + @spec synopsis(nil) :: nil + def synopsis(nil), do: nil + + def synopsis(doc) when is_binary(doc) do + doc = + case :binary.split(doc, "

") do + [left, _] -> String.trim_trailing(left, ":") <> "

" + [all] -> all + end + + # Remove any anchors found in synopsis. + # Old Erlang docs placed anchors at the top of the documentation + # for links. Ideally they would have been removed but meanwhile + # it is simpler to guarantee they won't be duplicated in docs. + Regex.replace(~r|(<[^>]*) id="[^"]*"([^>]*>)|, doc, ~S"\1\2", []) + end + + defp presence([]), do: nil + defp presence(other), do: other + + defp enc(binary), do: URI.encode(binary) + + @doc """ + Create a JS object which holds all the items displayed in the sidebar area + """ + def create_sidebar_items(nodes_map, extras) do + nodes = + nodes_map + |> Enum.map(&sidebar_module/1) + |> Map.new() + |> Map.put(:extras, sidebar_extras(extras)) + + ["sidebarNodes=" | ExDoc.Utils.to_json(nodes)] + end + + defp sidebar_extras(extras) do + for extra <- extras do + %{id: id, title: title, group: group, content: content} = extra + + %{ + id: to_string(id), + title: to_string(title), + group: to_string(group), + headers: extract_headers(content) + } + end + end + + defp sidebar_module({id, modules}) do + modules = + for module <- modules do + extra = + module + |> module_summary() + |> case do + [] -> [] + entries -> [nodeGroups: Enum.map(entries, &sidebar_entries/1)] + end + + sections = module_sections(module) + + deprecated? = not is_nil(module.deprecated) + + pairs = + for key <- [:id, :title, :nested_title, :nested_context], + value = Map.get(module, key), + do: {key, value} + + pairs = [{:deprecated, deprecated?} | pairs] + + Map.new([group: to_string(module.group)] ++ extra ++ pairs ++ sections) + end + + {id, modules} + end + + defp sidebar_entries({group, nodes}) do + nodes = + for node <- nodes do + id = + if "struct" in node.annotations do + node.signature + else + if node.name == nil do + "nil/#{node.arity}" + else + "#{node.name}/#{node.arity}" + end + end + + deprecated? = not is_nil(node.deprecated) + + %{id: id, title: node.signature, anchor: URI.encode(node.id), deprecated: deprecated?} + end + + %{key: text_to_id(group), name: group, nodes: nodes} + end + + defp module_sections(%ExDoc.ModuleNode{rendered_doc: nil}), do: [sections: []] + + defp module_sections(module) do + {sections, _} = + module.rendered_doc + |> extract_headers() + |> Enum.map_reduce(%{}, fn header, acc -> + # TODO Duplicates some of the logic of link_headings/3 + case Map.fetch(acc, header.id) do + {:ok, id} -> + {%{header | anchor: "module-#{header.anchor}-#{id}"}, Map.put(acc, header.id, id + 1)} + + :error -> + {%{header | anchor: "module-#{header.anchor}"}, Map.put(acc, header.id, 1)} + end + end) + + [sections: sections] + end + + # TODO: split into sections in Formatter.HTML instead. + @h2_regex ~r/(.*?)<\/h2>/m + defp extract_headers(content) do + @h2_regex + |> Regex.scan(content, capture: :all_but_first) + |> List.flatten() + |> Enum.filter(&(&1 != "")) + |> Enum.map(&ExDoc.Utils.strip_tags/1) + |> Enum.map(&%{id: &1, anchor: URI.encode(text_to_id(&1))}) + end + + def module_summary(module_node) do + entries = docs_groups(module_node.docs_groups, module_node.docs ++ module_node.typespecs) + + Enum.reject(entries, fn {_type, nodes} -> nodes == [] end) + end + + defp docs_groups(groups, docs) do + for group <- groups, do: {group, Enum.filter(docs, &(&1.group == group))} + end + + defp logo_path(%{logo: nil}), do: nil + defp logo_path(%{logo: logo}), do: "assets/logo#{Path.extname(logo)}" + + defp sidebar_type(:exception), do: "modules" + defp sidebar_type(:module), do: "modules" + defp sidebar_type(:behaviour), do: "modules" + defp sidebar_type(:protocol), do: "modules" + defp sidebar_type(:task), do: "tasks" + + defp sidebar_type(:search), do: "search" + defp sidebar_type(:cheatmd), do: "extras" + defp sidebar_type(:livemd), do: "extras" + defp sidebar_type(:extra), do: "extras" + + def asset_rev(output, pattern) do + output = Path.expand(output) + + output + |> Path.join(pattern) + |> Path.wildcard() + |> relative_asset(output, pattern) + end + + defp relative_asset([], output, pattern), + do: raise("could not find matching #{output}/#{pattern}") + + defp relative_asset([h | _], output, _pattern), do: Path.relative_to(h, output) + + # TODO: Move link_headings and friends to html.ex or even to autolinking code, + # so content is built with it upfront instead of added at the template level. + + @doc """ + Add link headings for the given `content`. + + IDs are prefixed with `prefix`. + + We only link `h2` and `h3` headers. This is kept consistent in ExDoc.SearchData. + """ + @heading_regex ~r/<(h[23]).*?>(.*?)<\/\1>/m + @spec link_headings(String.t() | nil, String.t()) :: String.t() | nil + def link_headings(content, prefix \\ "") + def link_headings(nil, _), do: nil + + def link_headings(content, prefix) do + @heading_regex + |> Regex.scan(content) + |> Enum.reduce({content, %{}}, fn [match, tag, title], {content, occurrences} -> + possible_id = text_to_id(title) + id_occurred = Map.get(occurrences, possible_id, 0) + + anchor_id = if id_occurred >= 1, do: "#{possible_id}-#{id_occurred}", else: possible_id + replacement = link_heading(match, tag, title, anchor_id, prefix) + linked_content = String.replace(content, match, replacement, global: false) + incremented_occs = Map.put(occurrences, possible_id, id_occurred + 1) + {linked_content, incremented_occs} + end) + |> elem(0) + end + + @class_regex ~r/[^"]+)")?.*?>/ + @class_separator " " + defp link_heading(match, _tag, _title, "", _prefix), do: match + + defp link_heading(match, tag, title, id, prefix) do + section_header_class_name = "section-heading" + + # NOTE: This addition is mainly to preserve the previous `class` attributes + # from the headers, in case there is one. Now with the _admonition_ text + # block, we inject CSS classes. So far, the supported classes are: + # `warning`, `info`, `error`, and `neutral`. + # + # The Markdown syntax that we support for the admonition text + # blocks is something like this: + # + # > ### Never open this door! {: .warning} + # > + # > ... + # + # That should produce the following HTML: + # + #
+ #

Never open this door!

+ #

...

+ #
+ # + # The original implementation discarded the previous CSS classes. Instead, + # it was setting `#{section_header_class_name}` as the only CSS class + # associated with the given header. + class_attribute = + case Regex.named_captures(@class_regex, match) do + %{"class" => ""} -> + section_header_class_name + + %{"class" => previous_classes} -> + # Let's make sure that the `section_header_class_name` is not already + # included in the previous classes for the header + previous_classes + |> String.split(@class_separator) + |> Enum.reject(&(&1 == section_header_class_name)) + |> Enum.join(@class_separator) + |> Kernel.<>(" #{section_header_class_name}") + end + + """ + <#{tag} id="#{prefix}#{id}" class="#{class_attribute}"> + + + + #{title} + + """ + end + + def link_moduledoc_headings(content) do + link_headings(content, "module-") + end + + def link_detail_headings(content, prefix) do + link_headings(content, prefix <> "-") + end + + templates = [ + detail_template: [:node, :module], + footer_template: [:config, :node], + head_template: [:config, :page], + module_template: [:config, :module, :summary, :nodes_map], + not_found_template: [:config, :nodes_map], + api_reference_entry_template: [:module_node], + api_reference_template: [:nodes_map], + extra_template: [:config, :node, :type, :nodes_map, :refs], + search_template: [:config, :nodes_map], + sidebar_template: [:config, :nodes_map], + summary_template: [:name, :nodes], + redirect_template: [:config, :redirect_to] + ] + + Enum.each(templates, fn {name, args} -> + filename = Path.expand("templates/#{name}.eex", __DIR__) + @doc false + EEx.function_from_file(:def, name, filename, args, trim: true) + end) +end diff --git a/lib/ex_doc/formatter/markdown/templates/api_reference_entry_template.eex b/lib/ex_doc/formatter/markdown/templates/api_reference_entry_template.eex new file mode 100644 index 000000000..ec17835f7 --- /dev/null +++ b/lib/ex_doc/formatter/markdown/templates/api_reference_entry_template.eex @@ -0,0 +1,11 @@ +
+
+ <%=h module_node.title %> + <%= if deprecated = module_node.deprecated do %> + deprecated + <% end %> +
+ <%= if doc = module_node.rendered_doc do %> +
<%= synopsis(doc) %>
+ <% end %> +
diff --git a/lib/ex_doc/formatter/markdown/templates/api_reference_template.eex b/lib/ex_doc/formatter/markdown/templates/api_reference_template.eex new file mode 100644 index 000000000..523a28964 --- /dev/null +++ b/lib/ex_doc/formatter/markdown/templates/api_reference_template.eex @@ -0,0 +1,21 @@ +<%= if nodes_map.modules != [] do %> +
+

Modules

+
+ <%= for module_node <- Enum.sort_by(nodes_map.modules, & &1.id) do + api_reference_entry_template(module_node) + end %> +
+
+<% end %> + +<%= if nodes_map.tasks != [] do %> +
+

Mix Tasks

+
+ <%= for task_node <- nodes_map.tasks do + api_reference_entry_template(task_node) + end %> +
+
+<% end %> diff --git a/lib/ex_doc/formatter/markdown/templates/detail_template.eex b/lib/ex_doc/formatter/markdown/templates/detail_template.eex new file mode 100644 index 000000000..450b45390 --- /dev/null +++ b/lib/ex_doc/formatter/markdown/templates/detail_template.eex @@ -0,0 +1,36 @@ +
+ <%= for {default_name, default_arity} <- get_defaults(node) do %> + "> + <% end %> +
+ + + +

<%=h node.signature %>

+ <%= if node.source_url do %> + + + + <% end %> + <%= for annotation <- node.annotations do %> + (<%= annotation %>) + <% end %> +
+ <%= if deprecated = node.deprecated do %> +
+ This <%= node.type %> is deprecated. <%= h(deprecated) %>. +
+ <% end %> + +
+ <%= if specs = get_specs(node) do %> +
+ <%= for spec <- specs do %> +
<%= format_spec_attribute(module, node) %> <%= spec %>
+ <% end %> +
+ <% end %> + + <%= link_detail_headings(node.rendered_doc, enc(node.id)) %> +
+
diff --git a/lib/ex_doc/formatter/markdown/templates/extra_template.eex b/lib/ex_doc/formatter/markdown/templates/extra_template.eex new file mode 100644 index 000000000..e7d22f9df --- /dev/null +++ b/lib/ex_doc/formatter/markdown/templates/extra_template.eex @@ -0,0 +1,60 @@ +<%= head_template(config, %{title: node.title, type: type, noindex: false}) %> +<%= sidebar_template(config, nodes_map) %> + +
+

+ <%= if node.source_url do %> + + + View Source + + <% end %> + <%= if type == :cheatmd do %> + + <% end %> + + <%= node.title_content %> +

+ + <%= if type == :livemd do %> +
+ + Run in Livebook + +
+ <% end %> + + <%= link_headings(node.content) %> +
+ +
+
+ <%= if refs.prev do %> + + <% end %> +
+
+ <%= if refs.next do %> + + <% end %> +
+
+ +<%= footer_template(config, node) %> diff --git a/lib/ex_doc/formatter/markdown/templates/footer_template.eex b/lib/ex_doc/formatter/markdown/templates/footer_template.eex new file mode 100644 index 000000000..5488a0212 --- /dev/null +++ b/lib/ex_doc/formatter/markdown/templates/footer_template.eex @@ -0,0 +1,44 @@ + + + + + <%= before_closing_body_tag(config, :html) %> + + diff --git a/lib/ex_doc/formatter/markdown/templates/head_template.eex b/lib/ex_doc/formatter/markdown/templates/head_template.eex new file mode 100644 index 000000000..939607423 --- /dev/null +++ b/lib/ex_doc/formatter/markdown/templates/head_template.eex @@ -0,0 +1,43 @@ + + + + + + + + + <%= if config.authors do %> + "> + <% end %> + <%= if page.noindex do %> + + <% end %> + <%= page.title %> — <%= config.project %> v<%= config.version %> + " /> + <%= if page.type == :cheatmd do %> + + <% end %> + <%= if config.canonical do %> + + <% end %> + + + + + + <%= before_closing_head_tag(config, :html) %> + + + diff --git a/lib/ex_doc/formatter/markdown/templates/module_template.eex b/lib/ex_doc/formatter/markdown/templates/module_template.eex new file mode 100644 index 000000000..4f2824c16 --- /dev/null +++ b/lib/ex_doc/formatter/markdown/templates/module_template.eex @@ -0,0 +1,57 @@ +<%= head_template(config, %{title: module.title, type: module.type, noindex: false}) %> +<%= sidebar_template(config, nodes_map) %> + +
+

+ <%= if module.source_url do %> + + + View Source + + <% end %> + <%= module.title %> <%= module_type(module) %> + (<%= config.project %> v<%= config.version %>) + <%= for annotation <- module.annotations do %> + (<%= annotation %>) + <% end %> +

+ + <%= if deprecated = module.deprecated do %> +
+ This <%= module.type %> is deprecated. <%= h(deprecated) %>. +
+ <% end %> + + <%= if doc = module.rendered_doc do %> +
+ <%= link_moduledoc_headings(doc) %> +
+ <% end %> +
+ +<%= if summary != [] do %> +
+

+ + + + Summary +

+ <%= for {name, nodes} <- summary, do: summary_template(name, nodes) %> +
+<% end %> + +<%= for {name, nodes} <- summary, key = text_to_id(name) do %> +
+

+ + + + <%= name %> +

+
+ <%= for node <- nodes, do: detail_template(node, module) %> +
+
+<% end %> +<%= footer_template(config, module) %> diff --git a/lib/ex_doc/formatter/markdown/templates/not_found_template.eex b/lib/ex_doc/formatter/markdown/templates/not_found_template.eex new file mode 100644 index 000000000..ad9179760 --- /dev/null +++ b/lib/ex_doc/formatter/markdown/templates/not_found_template.eex @@ -0,0 +1,15 @@ +<%= head_template(config, %{title: "404", type: :extra, noindex: true}) %> +<%= sidebar_template(config, nodes_map) %> + +

+ Page not found +

+ +

Sorry, but the page you were trying to get to, does not exist. You +may want to try searching this site using the sidebar +<%= if config.api_reference do %> + or using our API Reference page +<% end %> +to find what you were looking for.

+ +<%= footer_template(config, nil) %> diff --git a/lib/ex_doc/formatter/markdown/templates/redirect_template.eex b/lib/ex_doc/formatter/markdown/templates/redirect_template.eex new file mode 100644 index 000000000..a6a9c7cef --- /dev/null +++ b/lib/ex_doc/formatter/markdown/templates/redirect_template.eex @@ -0,0 +1,10 @@ + + + + + <%= config.project %> v<%= config.version %> — Documentation + + + + + diff --git a/lib/ex_doc/formatter/markdown/templates/search_template.eex b/lib/ex_doc/formatter/markdown/templates/search_template.eex new file mode 100644 index 000000000..54f553df0 --- /dev/null +++ b/lib/ex_doc/formatter/markdown/templates/search_template.eex @@ -0,0 +1,12 @@ +<%= head_template(config, %{title: "Search", type: :search, noindex: true}) %> +<%= sidebar_template(config, nodes_map) %> + + + +<%= footer_template(config, nil) %> diff --git a/lib/ex_doc/formatter/markdown/templates/sidebar_template.eex b/lib/ex_doc/formatter/markdown/templates/sidebar_template.eex new file mode 100644 index 000000000..44790bb8f --- /dev/null +++ b/lib/ex_doc/formatter/markdown/templates/sidebar_template.eex @@ -0,0 +1,92 @@ +
+ + + + + +
+ + +
+ diff --git a/lib/ex_doc/formatter/markdown/templates/summary_template.eex b/lib/ex_doc/formatter/markdown/templates/summary_template.eex new file mode 100644 index 000000000..90fcd3569 --- /dev/null +++ b/lib/ex_doc/formatter/markdown/templates/summary_template.eex @@ -0,0 +1,18 @@ +
+

+ <%= name %> +

+ <%= for node <- nodes do %> +
+
+ <%=h node.signature %> + <%= if deprecated = node.deprecated do %> + deprecated + <% end %> +
+ <%= if doc = node.rendered_doc do %> +
<%= synopsis(doc) %>
+ <% end %> +
+ <% end %> +
diff --git a/lib/ex_doc/markdown/assets.ex b/lib/ex_doc/markdown/assets.ex new file mode 100644 index 000000000..a83e3c648 --- /dev/null +++ b/lib/ex_doc/markdown/assets.ex @@ -0,0 +1,14 @@ +defmodule ExDoc.Formatter.Markdown.Assets do + @moduledoc false + + defmacrop embed_pattern(pattern) do + ["formatters/html", pattern] + |> Path.join() + |> Path.wildcard() + |> Enum.map(&{Path.basename(&1), File.read!(&1)}) + end + + def dist(_proglang), do: dist_license() + + defp dist_license(), do: embed_pattern("dist/*.LICENSE.txt") +end From 80c42dbacd317a62214892e467f68a95639b64ec Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Tue, 26 Nov 2024 02:56:18 -0500 Subject: [PATCH 02/12] Add Markdown formatter option --- lib/ex_doc.ex | 6 +++++- lib/ex_doc/cli.ex | 2 +- lib/ex_doc/formatter/markdown/templates/head_template.eex | 6 ------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/ex_doc.ex b/lib/ex_doc.ex index c108c3560..406d061ca 100644 --- a/lib/ex_doc.ex +++ b/lib/ex_doc.ex @@ -35,11 +35,15 @@ defmodule ExDoc do end defp find_formatter(name) do - [ExDoc.Formatter, String.upcase(name)] + [ExDoc.Formatter, modname(name)] |> Module.concat() |> check_formatter_module(name) end + defp modname("epub"), do: EPUB + defp modname("html"), do: HTML + defp modname("markdown"), do: Markdown + defp check_formatter_module(modname, argname) do if Code.ensure_loaded?(modname) do modname diff --git a/lib/ex_doc/cli.ex b/lib/ex_doc/cli.ex index 91403ff30..30678757e 100644 --- a/lib/ex_doc/cli.ex +++ b/lib/ex_doc/cli.ex @@ -195,7 +195,7 @@ defmodule ExDoc.CLI do --canonical Indicate the preferred URL with rel="canonical" link element -c, --config Give configuration through a file instead of a command line. See "Custom config" section below for more information. - -f, --formatter Docs formatter to use (html or epub), default: html and epub + -f, --formatter Docs formatter to use (html, epub or markdown), default: html and epub --homepage-url URL to link to for the site name --language Identify the primary language of the documents, its value must be a valid [BCP 47](https://tools.ietf.org/html/bcp47) language tag, default: "en" diff --git a/lib/ex_doc/formatter/markdown/templates/head_template.eex b/lib/ex_doc/formatter/markdown/templates/head_template.eex index 939607423..9e6dd58aa 100644 --- a/lib/ex_doc/formatter/markdown/templates/head_template.eex +++ b/lib/ex_doc/formatter/markdown/templates/head_template.eex @@ -13,18 +13,12 @@ <% end %> <%= page.title %> — <%= config.project %> v<%= config.version %> - " /> <%= if page.type == :cheatmd do %> <% end %> <%= if config.canonical do %> <% end %> - - - - - <%= before_closing_head_tag(config, :html) %> From df17551ddf478983d59a14b7ba210ce7f3a45420 Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Tue, 26 Nov 2024 16:38:54 -0500 Subject: [PATCH 03/12] First working version of Markdown documentation --- bin/ex_doc | 3 + lib/ex_doc.ex | 1 + lib/ex_doc/formatter/markdown.ex | 144 +++++++--- lib/ex_doc/formatter/markdown/templates.ex | 256 +----------------- .../api_reference_entry_template.eex | 14 +- .../templates/api_reference_template.eex | 30 +- .../markdown/templates/detail_template.eex | 41 +-- .../markdown/templates/extra_template.eex | 64 +---- .../markdown/templates/footer_template.eex | 45 +-- .../markdown/templates/head_template.eex | 38 +-- .../formatter/markdown/templates/metadata.eex | 6 + .../markdown/templates/module_template.eex | 64 +---- .../markdown/templates/not_found_template.eex | 15 - .../markdown/templates/redirect_template.eex | 10 - .../markdown/templates/search_template.eex | 12 - .../markdown/templates/sidebar_template.eex | 92 ------- .../markdown/templates/summary_template.eex | 23 +- lib/ex_doc/language/elixir.ex | 14 +- lib/ex_doc/markdown/assets.ex | 2 +- mix.exs | 3 +- mix.lock | 3 + 21 files changed, 182 insertions(+), 698 deletions(-) create mode 100644 lib/ex_doc/formatter/markdown/templates/metadata.eex delete mode 100644 lib/ex_doc/formatter/markdown/templates/not_found_template.eex delete mode 100644 lib/ex_doc/formatter/markdown/templates/redirect_template.eex delete mode 100644 lib/ex_doc/formatter/markdown/templates/search_template.eex delete mode 100644 lib/ex_doc/formatter/markdown/templates/sidebar_template.eex diff --git a/bin/ex_doc b/bin/ex_doc index 4655e163f..983245f3f 100755 --- a/bin/ex_doc +++ b/bin/ex_doc @@ -6,6 +6,9 @@ Code.prepend_path Path.expand("../_build/#{mix_env}/lib/makeup_elixir/ebin", __D Code.prepend_path Path.expand("../_build/#{mix_env}/lib/makeup_erlang/ebin", __DIR__) Code.prepend_path Path.expand("../_build/#{mix_env}/lib/makeup_html/ebin", __DIR__) Code.prepend_path Path.expand("../_build/#{mix_env}/lib/earmark_parser/ebin", __DIR__) +Code.prepend_path Path.expand("../_build/#{mix_env}/lib/rustler_precompiled/ebin", __DIR__) +Code.prepend_path Path.expand("../_build/#{mix_env}/lib/castore/ebin", __DIR__) +Code.prepend_path Path.expand("../_build/#{mix_env}/lib/mdex/ebin", __DIR__) Code.prepend_path Path.expand("../_build/#{mix_env}/lib/ex_doc/ebin", __DIR__) if Code.ensure_loaded?(ExDoc.CLI) do diff --git a/lib/ex_doc.ex b/lib/ex_doc.ex index 406d061ca..f797133e1 100644 --- a/lib/ex_doc.ex +++ b/lib/ex_doc.ex @@ -43,6 +43,7 @@ defmodule ExDoc do defp modname("epub"), do: EPUB defp modname("html"), do: HTML defp modname("markdown"), do: Markdown + defp modname(_), do: nil defp check_formatter_module(modname, argname) do if Code.ensure_loaded?(modname) do diff --git a/lib/ex_doc/formatter/markdown.ex b/lib/ex_doc/formatter/markdown.ex index be566de92..e16c63d6e 100644 --- a/lib/ex_doc/formatter/markdown.ex +++ b/lib/ex_doc/formatter/markdown.ex @@ -18,13 +18,11 @@ defmodule ExDoc.Formatter.Markdown do Utils.unset_warned() config = %{config | output: Path.expand(config.output)} - build = Path.join(config.output, ".build") output_setup(build, config) project_nodes = render_all(project_nodes, filtered_modules, ".md", config, []) extras = build_extras(config, ".md") - # Generate search early on without api reference in extras static_files = generate_assets(".", default_assets(config), config) @@ -43,13 +41,16 @@ defmodule ExDoc.Formatter.Markdown do all_files = static_files ++ - generate_extras(nodes_map, extras, config) ++ + generate_extras(extras, config) ++ generate_logo(@assets_dir, config) ++ - generate_list(nodes_map.modules, nodes_map, config) ++ - generate_list(nodes_map.tasks, nodes_map, config) + generate_list(nodes_map.modules, config) ++ + generate_list(nodes_map.tasks, config) generate_build(Enum.sort(all_files), build) - config.output |> Path.join("index.md") |> Path.relative_to_cwd() + + config.output + |> Path.join("index.md") + |> Path.relative_to_cwd() end @doc """ @@ -93,9 +94,9 @@ defmodule ExDoc.Formatter.Markdown do current_kfa: {:function, child_node.name, child_node.arity} ] - specs = Enum.map(child_node.specs, &language.autolink_spec(&1, autolink_opts)) + specs = Enum.map(child_node.specs, &language.format_spec(&1)) child_node = %{child_node | specs: specs} - render_doc(child_node, language, autolink_opts, opts) + render_doc(child_node, language, autolink_opts, opts, 4) end typespecs = @@ -113,14 +114,14 @@ defmodule ExDoc.Formatter.Markdown do child_node = %{ child_node - | spec: language.autolink_spec(child_node.spec, autolink_opts) + | spec: language.format_spec(child_node.spec) } - render_doc(child_node, language, autolink_opts, opts) + render_doc(child_node, language, autolink_opts, opts, 3) end %{ - render_doc(node, language, [{:id, node.id} | autolink_opts], opts) + render_doc(node, language, [{:id, node.id} | autolink_opts], opts, 2) | docs: docs, typespecs: typespecs } @@ -130,14 +131,75 @@ defmodule ExDoc.Formatter.Markdown do |> Enum.map(&elem(&1, 1)) end - defp render_doc(%{doc: nil} = node, _language, _autolink_opts, _opts), + defp render_doc(%{doc: nil} = node, _language, _autolink_opts, _opts, _base_heading), do: node - defp render_doc(%{doc: doc} = node, language, autolink_opts, opts) do - rendered = autolink_and_render(doc, language, autolink_opts, opts) + defp render_doc( + %{doc: _doc, source_doc: source_doc} = node, + _language, + _autolink_opts, + _opts, + base_heading + ) do + # rendered = autolink_and_render(doc, language, autolink_opts, opts) + rendered = rewrite_headings(source_doc["en"], base_heading) %{node | rendered_doc: rendered} end + defp rewrite_headings(markdown, base_heading) + when is_binary(markdown) and is_integer(base_heading) and base_heading >= 1 do + {:ok, [{"document", attributes, document}] = _ast} = MDEx.parse_document(markdown) + + document = + case find_lowest_heading(document) do + lowest_heading when lowest_heading >= base_heading -> + document + + lowest_heading -> + levels_to_bump = base_heading - lowest_heading + + bump_levels(document, levels_to_bump) + end + + ast = [{"document", attributes, document}] + + MDEx.to_commonmark!(ast) + end + + defp find_lowest_heading(document) when is_list(document) do + Enum.reduce_while(document, 6, fn + {"heading", [{"level", 1}, _rest_attributes], _children}, _lowest_level -> + {:halt, 1} + + {"heading", [{"level", level}, _rest_attributes], _children}, lowest_level + when level < lowest_level -> + {:cont, level} + + _, lowest_level -> + {:cont, lowest_level} + end) + end + + defp bump_levels(document, levels_to_bump) when is_list(document) do + document + |> Enum.reduce([], fn + {"heading", [{"level", level}, rest_attributes], children}, acc -> + updated_element = + {"heading", [{"level", increase_level(level, levels_to_bump)}, rest_attributes], + children} + + [updated_element | acc] + + elem, acc -> + [elem | acc] + end) + |> Enum.reverse() + end + + defp increase_level(level, levels_to_bump) do + min(level + levels_to_bump, 6) + end + defp id(%{id: mod_id}, %{id: "c:" <> id}) do "c:" <> mod_id <> "." <> id end @@ -150,13 +212,6 @@ defmodule ExDoc.Formatter.Markdown do mod_id <> "." <> id end - defp autolink_and_render(doc, language, autolink_opts, opts) do - doc - |> language.autolink_doc(autolink_opts) - |> ExDoc.DocAST.to_string() - |> ExDoc.DocAST.highlight(language, opts) - end - defp output_setup(build, config) do if File.exists?(build) do build @@ -177,7 +232,7 @@ defmodule ExDoc.Formatter.Markdown do File.write!(build, entries) end - defp generate_extras(nodes_map, extras, config) do + defp generate_extras(extras, config) do generated_extras = extras |> with_prev_next() @@ -192,7 +247,7 @@ defmodule ExDoc.Formatter.Markdown do } extension = node.source_path && Path.extname(node.source_path) - markdown = Templates.extra_template(config, node, extra_type(extension), nodes_map, refs) + markdown = Templates.extra_template(config, node, extra_type(extension), refs) if File.regular?(output) do Utils.warn("file #{Path.relative_to_cwd(output)} already exists", []) @@ -277,10 +332,8 @@ defmodule ExDoc.Formatter.Markdown do end defp build_api_reference(nodes_map, config) do - api_reference = Templates.api_reference_template(nodes_map) - - title_content = - ~s{API Reference #{config.project} v#{config.version}} + title = "API Reference" + api_reference = Templates.api_reference_template(nodes_map, title) %{ content: api_reference, @@ -288,8 +341,7 @@ defmodule ExDoc.Formatter.Markdown do id: "api-reference", source_path: nil, source_url: config.source_url, - title: "API Reference", - title_content: title_content + title: title } end @@ -339,7 +391,7 @@ defmodule ExDoc.Formatter.Markdown do Map.put(extra, :id, "#{extra.id}-#{discriminator}") end - defp build_extra({input, input_options}, groups, language, autolink_opts, source_url_pattern) do + defp build_extra({input, input_options}, groups, _language, _autolink_opts, source_url_pattern) do input = to_string(input) id = input_options[:filename] || input |> filename_to_title() |> Utils.text_to_id() source_file = input_options[:source] || input @@ -358,7 +410,8 @@ defmodule ExDoc.Formatter.Markdown do ast = source |> Markdown.to_ast(opts) - |> sectionize(extension) + + # |> sectionize(extension) {source, ast} @@ -367,7 +420,7 @@ defmodule ExDoc.Formatter.Markdown do "file extension not recognized, allowed extension is either .cheatmd, .livemd, .md, .txt or no extension" end - {title_ast, ast} = + {title_ast, _ast} = case ExDoc.DocAST.extract_title(ast) do {:ok, title_ast, ast} -> {title_ast, ast} :error -> {nil, ast} @@ -375,7 +428,8 @@ defmodule ExDoc.Formatter.Markdown do title_text = title_ast && ExDoc.DocAST.text_from_ast(title_ast) title_markdown = title_ast && ExDoc.DocAST.to_string(title_ast) - content_markdown = autolink_and_render(ast, language, [file: input] ++ autolink_opts, opts) + # content_markdown = autolink_and_render(ast, language, [file: input] ++ autolink_opts, opts) + content_markdown = source group = GroupMatcher.match_extra(groups, input) title = input_options[:title] || title_text || filename_to_title(input) @@ -405,15 +459,15 @@ defmodule ExDoc.Formatter.Markdown do |> String.downcase() end - defp sectionize(ast, ".cheatmd") do - ExDoc.DocAST.sectionize(ast, fn - {:h2, _, _, _} -> true - {:h3, _, _, _} -> true - _ -> false - end) - end + # defp sectionize(ast, ".cheatmd") do + # ExDoc.DocAST.sectionize(ast, fn + # {:h2, _, _, _} -> true + # {:h3, _, _, _} -> true + # _ -> false + # end) + # end - defp sectionize(ast, _), do: ast + # defp sectionize(ast, _), do: ast defp filename_to_title(input) do input |> Path.basename() |> Path.rootname() @@ -466,16 +520,16 @@ defmodule ExDoc.Formatter.Markdown do Enum.filter(nodes, &(&1.type == type)) end - defp generate_list(nodes, nodes_map, config) do + defp generate_list(nodes, config) do nodes - |> Task.async_stream(&generate_module_page(&1, nodes_map, config), timeout: :infinity) + |> Task.async_stream(&generate_module_page(&1, config), timeout: :infinity) |> Enum.map(&elem(&1, 1)) end - defp generate_module_page(module_node, nodes_map, config) do + defp generate_module_page(module_node, config) do filename = "#{module_node.id}.md" config = set_canonical_url(config, filename) - content = Templates.module_page(module_node, nodes_map, config) + content = Templates.module_page(module_node, config) File.write!("#{config.output}/#{filename}", content) filename end diff --git a/lib/ex_doc/formatter/markdown/templates.ex b/lib/ex_doc/formatter/markdown/templates.ex index 085c59ac7..cbc41754a 100644 --- a/lib/ex_doc/formatter/markdown/templates.ex +++ b/lib/ex_doc/formatter/markdown/templates.ex @@ -5,18 +5,15 @@ defmodule ExDoc.Formatter.Markdown.Templates do import ExDoc.Utils, only: [ h: 1, - before_closing_body_tag: 2, - before_closing_footer_tag: 2, - before_closing_head_tag: 2, text_to_id: 1 ] @doc """ Generate content from the module template for a given `node` """ - def module_page(module_node, nodes_map, config) do + def module_page(module_node, config) do summary = module_summary(module_node) - module_template(config, module_node, summary, nodes_map) + module_template(config, module_node, summary) end @doc """ @@ -64,7 +61,7 @@ defmodule ExDoc.Formatter.Markdown.Templates do """ def module_type(%{type: :task}), do: "" def module_type(%{type: :module}), do: "" - def module_type(%{type: type}), do: "#{type}" + def module_type(%{type: type}), do: to_string(type) @doc """ Gets the first paragraph of the documentation of a node. It strips @@ -77,130 +74,13 @@ defmodule ExDoc.Formatter.Markdown.Templates do def synopsis(nil), do: nil def synopsis(doc) when is_binary(doc) do - doc = - case :binary.split(doc, "

") do - [left, _] -> String.trim_trailing(left, ":") <> "

" - [all] -> all - end - - # Remove any anchors found in synopsis. - # Old Erlang docs placed anchors at the top of the documentation - # for links. Ideally they would have been removed but meanwhile - # it is simpler to guarantee they won't be duplicated in docs. - Regex.replace(~r|(<[^>]*) id="[^"]*"([^>]*>)|, doc, ~S"\1\2", []) - end - - defp presence([]), do: nil - defp presence(other), do: other - - defp enc(binary), do: URI.encode(binary) - - @doc """ - Create a JS object which holds all the items displayed in the sidebar area - """ - def create_sidebar_items(nodes_map, extras) do - nodes = - nodes_map - |> Enum.map(&sidebar_module/1) - |> Map.new() - |> Map.put(:extras, sidebar_extras(extras)) - - ["sidebarNodes=" | ExDoc.Utils.to_json(nodes)] - end - - defp sidebar_extras(extras) do - for extra <- extras do - %{id: id, title: title, group: group, content: content} = extra - - %{ - id: to_string(id), - title: to_string(title), - group: to_string(group), - headers: extract_headers(content) - } + case :binary.split(doc, "\n\n") do + [left, _] -> String.trim_trailing(left, ":") <> "\n\n" + [all] -> all end end - defp sidebar_module({id, modules}) do - modules = - for module <- modules do - extra = - module - |> module_summary() - |> case do - [] -> [] - entries -> [nodeGroups: Enum.map(entries, &sidebar_entries/1)] - end - - sections = module_sections(module) - - deprecated? = not is_nil(module.deprecated) - - pairs = - for key <- [:id, :title, :nested_title, :nested_context], - value = Map.get(module, key), - do: {key, value} - - pairs = [{:deprecated, deprecated?} | pairs] - - Map.new([group: to_string(module.group)] ++ extra ++ pairs ++ sections) - end - - {id, modules} - end - - defp sidebar_entries({group, nodes}) do - nodes = - for node <- nodes do - id = - if "struct" in node.annotations do - node.signature - else - if node.name == nil do - "nil/#{node.arity}" - else - "#{node.name}/#{node.arity}" - end - end - - deprecated? = not is_nil(node.deprecated) - - %{id: id, title: node.signature, anchor: URI.encode(node.id), deprecated: deprecated?} - end - - %{key: text_to_id(group), name: group, nodes: nodes} - end - - defp module_sections(%ExDoc.ModuleNode{rendered_doc: nil}), do: [sections: []] - - defp module_sections(module) do - {sections, _} = - module.rendered_doc - |> extract_headers() - |> Enum.map_reduce(%{}, fn header, acc -> - # TODO Duplicates some of the logic of link_headings/3 - case Map.fetch(acc, header.id) do - {:ok, id} -> - {%{header | anchor: "module-#{header.anchor}-#{id}"}, Map.put(acc, header.id, id + 1)} - - :error -> - {%{header | anchor: "module-#{header.anchor}"}, Map.put(acc, header.id, 1)} - end - end) - - [sections: sections] - end - - # TODO: split into sections in Formatter.HTML instead. - @h2_regex ~r/(.*?)<\/h2>/m - defp extract_headers(content) do - @h2_regex - |> Regex.scan(content, capture: :all_but_first) - |> List.flatten() - |> Enum.filter(&(&1 != "")) - |> Enum.map(&ExDoc.Utils.strip_tags/1) - |> Enum.map(&%{id: &1, anchor: URI.encode(text_to_id(&1))}) - end + defp enc(binary), do: URI.encode(binary) def module_summary(module_node) do entries = docs_groups(module_node.docs_groups, module_node.docs ++ module_node.typespecs) @@ -212,20 +92,6 @@ defmodule ExDoc.Formatter.Markdown.Templates do for group <- groups, do: {group, Enum.filter(docs, &(&1.group == group))} end - defp logo_path(%{logo: nil}), do: nil - defp logo_path(%{logo: logo}), do: "assets/logo#{Path.extname(logo)}" - - defp sidebar_type(:exception), do: "modules" - defp sidebar_type(:module), do: "modules" - defp sidebar_type(:behaviour), do: "modules" - defp sidebar_type(:protocol), do: "modules" - defp sidebar_type(:task), do: "tasks" - - defp sidebar_type(:search), do: "search" - defp sidebar_type(:cheatmd), do: "extras" - defp sidebar_type(:livemd), do: "extras" - defp sidebar_type(:extra), do: "extras" - def asset_rev(output, pattern) do output = Path.expand(output) @@ -240,112 +106,14 @@ defmodule ExDoc.Formatter.Markdown.Templates do defp relative_asset([h | _], output, _pattern), do: Path.relative_to(h, output) - # TODO: Move link_headings and friends to html.ex or even to autolinking code, - # so content is built with it upfront instead of added at the template level. - - @doc """ - Add link headings for the given `content`. - - IDs are prefixed with `prefix`. - - We only link `h2` and `h3` headers. This is kept consistent in ExDoc.SearchData. - """ - @heading_regex ~r/<(h[23]).*?>(.*?)<\/\1>/m - @spec link_headings(String.t() | nil, String.t()) :: String.t() | nil - def link_headings(content, prefix \\ "") - def link_headings(nil, _), do: nil - - def link_headings(content, prefix) do - @heading_regex - |> Regex.scan(content) - |> Enum.reduce({content, %{}}, fn [match, tag, title], {content, occurrences} -> - possible_id = text_to_id(title) - id_occurred = Map.get(occurrences, possible_id, 0) - - anchor_id = if id_occurred >= 1, do: "#{possible_id}-#{id_occurred}", else: possible_id - replacement = link_heading(match, tag, title, anchor_id, prefix) - linked_content = String.replace(content, match, replacement, global: false) - incremented_occs = Map.put(occurrences, possible_id, id_occurred + 1) - {linked_content, incremented_occs} - end) - |> elem(0) - end - - @class_regex ~r/[^"]+)")?.*?>/ - @class_separator " " - defp link_heading(match, _tag, _title, "", _prefix), do: match - - defp link_heading(match, tag, title, id, prefix) do - section_header_class_name = "section-heading" - - # NOTE: This addition is mainly to preserve the previous `class` attributes - # from the headers, in case there is one. Now with the _admonition_ text - # block, we inject CSS classes. So far, the supported classes are: - # `warning`, `info`, `error`, and `neutral`. - # - # The Markdown syntax that we support for the admonition text - # blocks is something like this: - # - # > ### Never open this door! {: .warning} - # > - # > ... - # - # That should produce the following HTML: - # - #
- #

Never open this door!

- #

...

- #
- # - # The original implementation discarded the previous CSS classes. Instead, - # it was setting `#{section_header_class_name}` as the only CSS class - # associated with the given header. - class_attribute = - case Regex.named_captures(@class_regex, match) do - %{"class" => ""} -> - section_header_class_name - - %{"class" => previous_classes} -> - # Let's make sure that the `section_header_class_name` is not already - # included in the previous classes for the header - previous_classes - |> String.split(@class_separator) - |> Enum.reject(&(&1 == section_header_class_name)) - |> Enum.join(@class_separator) - |> Kernel.<>(" #{section_header_class_name}") - end - - """ - <#{tag} id="#{prefix}#{id}" class="#{class_attribute}"> - - - - #{title} - - """ - end - - def link_moduledoc_headings(content) do - link_headings(content, "module-") - end - - def link_detail_headings(content, prefix) do - link_headings(content, prefix <> "-") - end - templates = [ - detail_template: [:node, :module], - footer_template: [:config, :node], + detail_template: [:config, :node, :module], + footer_template: [:config], head_template: [:config, :page], - module_template: [:config, :module, :summary, :nodes_map], - not_found_template: [:config, :nodes_map], + module_template: [:config, :module, :summary], api_reference_entry_template: [:module_node], - api_reference_template: [:nodes_map], - extra_template: [:config, :node, :type, :nodes_map, :refs], - search_template: [:config, :nodes_map], - sidebar_template: [:config, :nodes_map], - summary_template: [:name, :nodes], - redirect_template: [:config, :redirect_to] + api_reference_template: [:nodes_map, :title], + extra_template: [:config, :node, :type, :refs] ] Enum.each(templates, fn {name, args} -> diff --git a/lib/ex_doc/formatter/markdown/templates/api_reference_entry_template.eex b/lib/ex_doc/formatter/markdown/templates/api_reference_entry_template.eex index ec17835f7..763b67548 100644 --- a/lib/ex_doc/formatter/markdown/templates/api_reference_entry_template.eex +++ b/lib/ex_doc/formatter/markdown/templates/api_reference_entry_template.eex @@ -1,11 +1,3 @@ -
-
- <%=h module_node.title %> - <%= if deprecated = module_node.deprecated do %> - deprecated - <% end %> -
- <%= if doc = module_node.rendered_doc do %> -
<%= synopsis(doc) %>
- <% end %> -
+**[<%=h module_node.title %>](<%=enc module_node.id %>.md)**<%= if module_node.deprecated do %> *deprecated*<% end %> +<%= if doc = module_node.rendered_doc do %><%= String.trim(synopsis(doc)) %> +<% end %> diff --git a/lib/ex_doc/formatter/markdown/templates/api_reference_template.eex b/lib/ex_doc/formatter/markdown/templates/api_reference_template.eex index 523a28964..669d03b1f 100644 --- a/lib/ex_doc/formatter/markdown/templates/api_reference_template.eex +++ b/lib/ex_doc/formatter/markdown/templates/api_reference_template.eex @@ -1,21 +1,19 @@ +# <%= title %> + <%= if nodes_map.modules != [] do %> -
-

Modules

-
- <%= for module_node <- Enum.sort_by(nodes_map.modules, & &1.id) do - api_reference_entry_template(module_node) - end %> -
-
+## Modules + +<%= for module_node <- Enum.sort_by(nodes_map.modules, & &1.id) do +api_reference_entry_template(module_node) <> "\n" +end %> <% end %> <%= if nodes_map.tasks != [] do %> -
-

Mix Tasks

-
- <%= for task_node <- nodes_map.tasks do - api_reference_entry_template(task_node) - end %> -
-
+## Mix Tasks + +<%= for task_node <- nodes_map.tasks do +api_reference_entry_template(task_node) + +end %> + <% end %> diff --git a/lib/ex_doc/formatter/markdown/templates/detail_template.eex b/lib/ex_doc/formatter/markdown/templates/detail_template.eex index 450b45390..b075ed2aa 100644 --- a/lib/ex_doc/formatter/markdown/templates/detail_template.eex +++ b/lib/ex_doc/formatter/markdown/templates/detail_template.eex @@ -1,36 +1,9 @@ -
- <%= for {default_name, default_arity} <- get_defaults(node) do %> - "> - <% end %> -
- - - -

<%=h node.signature %>

- <%= if node.source_url do %> - - - - <% end %> - <%= for annotation <- node.annotations do %> - (<%= annotation %>) - <% end %> -
- <%= if deprecated = node.deprecated do %> -
- This <%= node.type %> is deprecated. <%= h(deprecated) %>. -
- <% end %> +### <%=h node.signature %> -
- <%= if specs = get_specs(node) do %> -
- <%= for spec <- specs do %> -
<%= format_spec_attribute(module, node) %> <%= spec %>
- <% end %> -
- <% end %> +<%= for annotation <- node.annotations do %>*(<%= annotation %>)* <% end %> +<%= if specs = get_specs(node) do %>```<%= config.proglang %> +<%= for spec <- specs do %><%= format_spec_attribute(module, node) %> <%= spec %> +<% end %>```<% end %> +<%= if deprecated = node.deprecated do %>This <%= node.type %> is deprecated. <%= h(deprecated) %>.<% end %> - <%= link_detail_headings(node.rendered_doc, enc(node.id)) %> -
-
+<%= if doc = node.rendered_doc do %><%= doc %><% end %> diff --git a/lib/ex_doc/formatter/markdown/templates/extra_template.eex b/lib/ex_doc/formatter/markdown/templates/extra_template.eex index e7d22f9df..986118a0f 100644 --- a/lib/ex_doc/formatter/markdown/templates/extra_template.eex +++ b/lib/ex_doc/formatter/markdown/templates/extra_template.eex @@ -1,60 +1,6 @@ -<%= head_template(config, %{title: node.title, type: type, noindex: false}) %> -<%= sidebar_template(config, nodes_map) %> +<%= if type == :livemd do %>![Livebook badge](https://livebook.dev/badge/v1/blue.svg "Run in Livebook") +<% end %><%= node.content %> -
-

- <%= if node.source_url do %> - - - View Source - - <% end %> - <%= if type == :cheatmd do %> - - <% end %> - - <%= node.title_content %> -

- - <%= if type == :livemd do %> -
- - Run in Livebook - -
- <% end %> - - <%= link_headings(node.content) %> -
- -
-
- <%= if refs.prev do %> - - <% end %> -
-
- <%= if refs.next do %> - - <% end %> -
-
- -<%= footer_template(config, node) %> +--- +<%= if refs.prev do %>[← Previous Page](<%= refs.prev.path %> "<%= refs.prev.title %>")<% end %><%= if refs.prev && refs.next do %> - <% end %><%= if refs.next do %>[Next Page →](<%= refs.next.path %> "<%= refs.next.title %>")<% end %> +<%= footer_template(config) %> diff --git a/lib/ex_doc/formatter/markdown/templates/footer_template.eex b/lib/ex_doc/formatter/markdown/templates/footer_template.eex index 5488a0212..44ceb497a 100644 --- a/lib/ex_doc/formatter/markdown/templates/footer_template.eex +++ b/lib/ex_doc/formatter/markdown/templates/footer_template.eex @@ -1,44 +1,3 @@ - -
-
-
- <%= before_closing_body_tag(config, :html) %> - - +--- +Built using [ExDoc](https://github.com/elixir-lang/ex_doc "ExDoc") (v<%= ExDoc.version() %>) for the <%= if config.proglang == :erlang do %>[Erlang programming language](href="https://erlang.org" "Erlang")<% else %>[Elixir programming language](href="https://elixir-lang.org" "Elixir")<% end %>. diff --git a/lib/ex_doc/formatter/markdown/templates/head_template.eex b/lib/ex_doc/formatter/markdown/templates/head_template.eex index 9e6dd58aa..6b1dddd9d 100644 --- a/lib/ex_doc/formatter/markdown/templates/head_template.eex +++ b/lib/ex_doc/formatter/markdown/templates/head_template.eex @@ -1,37 +1 @@ - - - - - - - - - <%= if config.authors do %> - "> - <% end %> - <%= if page.noindex do %> - - <% end %> - <%= page.title %> — <%= config.project %> v<%= config.version %> - <%= if page.type == :cheatmd do %> - - <% end %> - <%= if config.canonical do %> - - <% end %> - <%= before_closing_head_tag(config, :html) %> - - - +<%= page.title %> — <%= config.project %> v<%= config.version %> diff --git a/lib/ex_doc/formatter/markdown/templates/metadata.eex b/lib/ex_doc/formatter/markdown/templates/metadata.eex new file mode 100644 index 000000000..37f39b976 --- /dev/null +++ b/lib/ex_doc/formatter/markdown/templates/metadata.eex @@ -0,0 +1,6 @@ +- Language: <%= config.language %> +- Generator: ExDoc v<%= ExDoc.version() %> +- Project: <%= config.project %> v<%= config.version%> +<%= if config.authors do %> +- Authors: <%= Enum.join(config.authors, ", ") %> +<% end %> diff --git a/lib/ex_doc/formatter/markdown/templates/module_template.eex b/lib/ex_doc/formatter/markdown/templates/module_template.eex index 4f2824c16..c822efcd6 100644 --- a/lib/ex_doc/formatter/markdown/templates/module_template.eex +++ b/lib/ex_doc/formatter/markdown/templates/module_template.eex @@ -1,57 +1,13 @@ -<%= head_template(config, %{title: module.title, type: module.type, noindex: false}) %> -<%= sidebar_template(config, nodes_map) %> +# <%= module.title %> <%= module_type(module) %> +(<%= config.project %> v<%= config.version %>) -
-

- <%= if module.source_url do %> - - - View Source - - <% end %> - <%= module.title %> <%= module_type(module) %> - (<%= config.project %> v<%= config.version %>) - <%= for annotation <- module.annotations do %> - (<%= annotation %>) - <% end %> -

- - <%= if deprecated = module.deprecated do %> -
- This <%= module.type %> is deprecated. <%= h(deprecated) %>. -
- <% end %> - - <%= if doc = module.rendered_doc do %> -
- <%= link_moduledoc_headings(doc) %> -
- <% end %> -
- -<%= if summary != [] do %> -
-

- - - - Summary -

- <%= for {name, nodes} <- summary, do: summary_template(name, nodes) %> -
+<%= for annotation <- module.annotations do %>*(<%= annotation %>)* <% end %> +<%= if deprecated = module.deprecated do %>This <%= module.type %> is deprecated. <%= h(deprecated) %>. +<% end %><%= if doc = module.rendered_doc do %><%= doc %> +<% end %> +<%= for {name, nodes} <- summary, _key = text_to_id(name) do %>## <%= name %> +<%= for node <- nodes do %> +<%= detail_template(config, node, module) %> <% end %> - -<%= for {name, nodes} <- summary, key = text_to_id(name) do %> -
-

- - - - <%= name %> -

-
- <%= for node <- nodes, do: detail_template(node, module) %> -
-
<% end %> -<%= footer_template(config, module) %> +<%= footer_template(config) %> diff --git a/lib/ex_doc/formatter/markdown/templates/not_found_template.eex b/lib/ex_doc/formatter/markdown/templates/not_found_template.eex deleted file mode 100644 index ad9179760..000000000 --- a/lib/ex_doc/formatter/markdown/templates/not_found_template.eex +++ /dev/null @@ -1,15 +0,0 @@ -<%= head_template(config, %{title: "404", type: :extra, noindex: true}) %> -<%= sidebar_template(config, nodes_map) %> - -

- Page not found -

- -

Sorry, but the page you were trying to get to, does not exist. You -may want to try searching this site using the sidebar -<%= if config.api_reference do %> - or using our API Reference page -<% end %> -to find what you were looking for.

- -<%= footer_template(config, nil) %> diff --git a/lib/ex_doc/formatter/markdown/templates/redirect_template.eex b/lib/ex_doc/formatter/markdown/templates/redirect_template.eex deleted file mode 100644 index a6a9c7cef..000000000 --- a/lib/ex_doc/formatter/markdown/templates/redirect_template.eex +++ /dev/null @@ -1,10 +0,0 @@ - - - - - <%= config.project %> v<%= config.version %> — Documentation - - - - - diff --git a/lib/ex_doc/formatter/markdown/templates/search_template.eex b/lib/ex_doc/formatter/markdown/templates/search_template.eex deleted file mode 100644 index 54f553df0..000000000 --- a/lib/ex_doc/formatter/markdown/templates/search_template.eex +++ /dev/null @@ -1,12 +0,0 @@ -<%= head_template(config, %{title: "Search", type: :search, noindex: true}) %> -<%= sidebar_template(config, nodes_map) %> - - - -<%= footer_template(config, nil) %> diff --git a/lib/ex_doc/formatter/markdown/templates/sidebar_template.eex b/lib/ex_doc/formatter/markdown/templates/sidebar_template.eex deleted file mode 100644 index 44790bb8f..000000000 --- a/lib/ex_doc/formatter/markdown/templates/sidebar_template.eex +++ /dev/null @@ -1,92 +0,0 @@ -
- - - - - -
- - -
- diff --git a/lib/ex_doc/formatter/markdown/templates/summary_template.eex b/lib/ex_doc/formatter/markdown/templates/summary_template.eex index 90fcd3569..74c85d50f 100644 --- a/lib/ex_doc/formatter/markdown/templates/summary_template.eex +++ b/lib/ex_doc/formatter/markdown/templates/summary_template.eex @@ -1,18 +1,5 @@ -
-

- <%= name %> -

- <%= for node <- nodes do %> -
-
- <%=h node.signature %> - <%= if deprecated = node.deprecated do %> - deprecated - <% end %> -
- <%= if doc = node.rendered_doc do %> -
<%= synopsis(doc) %>
- <% end %> -
- <% end %> -
+## <%= name %> + +<%= for node <- nodes do %> +<%=h node.signature %> +<%= if deprecated = node.deprecated do %>(deprecated)<% end %><%= synopsis(doc) %><% end %> diff --git a/lib/ex_doc/language/elixir.ex b/lib/ex_doc/language/elixir.ex index 2600849cc..1ab6bcc11 100644 --- a/lib/ex_doc/language/elixir.ex +++ b/lib/ex_doc/language/elixir.ex @@ -380,18 +380,20 @@ defmodule ExDoc.Language.Elixir do def autolink_spec(ast, opts) do config = struct!(Autolink, opts) - string = - ast - |> Macro.to_string() - |> safe_format_string!() - |> ExDoc.Utils.h() - + string = format_spec(ast) name = typespec_name(ast) {name, rest} = split_name(string, name) name <> do_typespec(rest, config) end + def format_spec(ast) do + ast + |> Macro.to_string() + |> safe_format_string!() + |> ExDoc.Utils.h() + end + @impl true def highlight_info() do %{ diff --git a/lib/ex_doc/markdown/assets.ex b/lib/ex_doc/markdown/assets.ex index a83e3c648..42a123e89 100644 --- a/lib/ex_doc/markdown/assets.ex +++ b/lib/ex_doc/markdown/assets.ex @@ -2,7 +2,7 @@ defmodule ExDoc.Formatter.Markdown.Assets do @moduledoc false defmacrop embed_pattern(pattern) do - ["formatters/html", pattern] + ["formatters/markdown", pattern] |> Path.join() |> Path.wildcard() |> Enum.map(&{Path.basename(&1), File.read!(&1)}) diff --git a/mix.exs b/mix.exs index 2c3fcf449..729d833f7 100644 --- a/mix.exs +++ b/mix.exs @@ -46,7 +46,8 @@ defmodule ExDoc.Mixfile do {:makeup_html, ">= 0.1.0", optional: true}, {:jason, "~> 1.2", only: :test}, {:floki, "~> 0.0", only: :test}, - {:easyhtml, "~> 0.0", only: :test} + {:easyhtml, "~> 0.0", only: :test}, + {:mdex, "~> 0.2"} ] end diff --git a/mix.lock b/mix.lock index 5f62324c6..602f17c8e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ %{ + "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, "earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"}, "easyhtml": {:hex, :easyhtml, "0.3.2", "050adfc8074f53b261f7dfe83303d864f1fbf5988245b369f8fdff1bf4c4b3e6", [:mix], [{:floki, "~> 0.35", [hex: :floki, repo: "hexpm", optional: false]}], "hexpm", "b6a936f91612a4870aa3e828cd8da5a08d9e3b6221b4d3012b6ec70b87845d06"}, "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, @@ -8,5 +9,7 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "makeup_html": {:hex, :makeup_html, "0.1.1", "c3d4abd39d5f7e925faca72ada6e9cc5c6f5fa7cd5bc0158315832656cf14d7f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "44f2a61bc5243645dd7fafeaa6cc28793cd22f3c76b861e066168f9a5b2c26a4"}, + "mdex": {:hex, :mdex, "0.2.0", "af93e03bc964f2628c3940d22ba03435b119e070bd423fd62d31772d428a7e6c", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "d8d21d3d6ecb0b2a10f88b539f3a61df974f9570226bf6bd24404c9e361c8089"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.2", "5f25cbe220a8fac3e7ad62e6f950fcdca5a5a5f8501835d2823e8c74bf4268d5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "63d1bd5f8e23096d1ff851839923162096364bac8656a4a3c00d1fff8e83ee0a"}, } From d0cca8db1f50edf6017d40227c2a4e82274fdfb4 Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Mon, 30 Dec 2024 12:22:13 -0500 Subject: [PATCH 04/12] Update Markdown formatter to be aligned with changes in HTML formatter introduced in 72e211c --- lib/ex_doc/formatter/markdown.ex | 28 ++------------- lib/ex_doc/formatter/markdown/templates.ex | 35 ++----------------- .../markdown/templates/detail_template.eex | 4 +-- 3 files changed, 7 insertions(+), 60 deletions(-) diff --git a/lib/ex_doc/formatter/markdown.ex b/lib/ex_doc/formatter/markdown.ex index e16c63d6e..7b660f61c 100644 --- a/lib/ex_doc/formatter/markdown.ex +++ b/lib/ex_doc/formatter/markdown.ex @@ -91,7 +91,7 @@ defmodule ExDoc.Formatter.Markdown do id: id, line: child_node.doc_line, file: child_node.doc_file, - current_kfa: {:function, child_node.name, child_node.arity} + current_kfa: {child_node.type, child_node.name, child_node.arity} ] specs = Enum.map(child_node.specs, &language.format_spec(&1)) @@ -99,31 +99,9 @@ defmodule ExDoc.Formatter.Markdown do render_doc(child_node, language, autolink_opts, opts, 4) end - typespecs = - for child_node <- node.typespecs do - id = id(node, child_node) - - autolink_opts = - autolink_opts ++ - [ - id: id, - line: child_node.doc_line, - file: child_node.doc_file, - current_kfa: {child_node.type, child_node.name, child_node.arity} - ] - - child_node = %{ - child_node - | spec: language.format_spec(child_node.spec) - } - - render_doc(child_node, language, autolink_opts, opts, 3) - end - %{ render_doc(node, language, [{:id, node.id} | autolink_opts], opts, 2) - | docs: docs, - typespecs: typespecs + | docs: docs } end, timeout: :infinity @@ -384,7 +362,7 @@ defmodule ExDoc.Formatter.Markdown do if ids_count[extra.id] > 1, do: {disambiguate_id(extra, idx), idx + 1}, else: {extra, idx} end) |> elem(0) - |> Enum.sort_by(fn extra -> GroupMatcher.group_index(groups, extra.group) end) + |> Enum.sort_by(fn extra -> GroupMatcher.index(groups, extra.group) end) end defp disambiguate_id(extra, discriminator) do diff --git a/lib/ex_doc/formatter/markdown/templates.ex b/lib/ex_doc/formatter/markdown/templates.ex index cbc41754a..2b64993e6 100644 --- a/lib/ex_doc/formatter/markdown/templates.ex +++ b/lib/ex_doc/formatter/markdown/templates.ex @@ -16,21 +16,6 @@ defmodule ExDoc.Formatter.Markdown.Templates do module_template(config, module_node, summary) end - @doc """ - Get the full specs from a function, already in HTML form. - """ - def get_specs(%ExDoc.TypeNode{spec: spec}) do - [spec] - end - - def get_specs(%ExDoc.FunctionNode{specs: specs}) when is_list(specs) do - presence(specs) - end - - def get_specs(_node) do - nil - end - @doc """ Format the attribute type used to define the spec of the given `node`. """ @@ -38,17 +23,6 @@ defmodule ExDoc.Formatter.Markdown.Templates do module.language.format_spec_attribute(node) end - @doc """ - Get defaults clauses. - """ - def get_defaults(%{defaults: defaults}) do - defaults - end - - def get_defaults(_) do - [] - end - @doc """ Get the pretty name of a function node """ @@ -83,13 +57,8 @@ defmodule ExDoc.Formatter.Markdown.Templates do defp enc(binary), do: URI.encode(binary) def module_summary(module_node) do - entries = docs_groups(module_node.docs_groups, module_node.docs ++ module_node.typespecs) - - Enum.reject(entries, fn {_type, nodes} -> nodes == [] end) - end - - defp docs_groups(groups, docs) do - for group <- groups, do: {group, Enum.filter(docs, &(&1.group == group))} + # TODO: Maybe it should be moved to retriever and it already returned grouped metadata + ExDoc.GroupMatcher.group_by(module_node.docs_groups, module_node.docs, & &1.group) end def asset_rev(output, pattern) do diff --git a/lib/ex_doc/formatter/markdown/templates/detail_template.eex b/lib/ex_doc/formatter/markdown/templates/detail_template.eex index b075ed2aa..ebc2e74ad 100644 --- a/lib/ex_doc/formatter/markdown/templates/detail_template.eex +++ b/lib/ex_doc/formatter/markdown/templates/detail_template.eex @@ -1,8 +1,8 @@ ### <%=h node.signature %> <%= for annotation <- node.annotations do %>*(<%= annotation %>)* <% end %> -<%= if specs = get_specs(node) do %>```<%= config.proglang %> -<%= for spec <- specs do %><%= format_spec_attribute(module, node) %> <%= spec %> +<%= if node.specs != [] do %>```<%= config.proglang %> +<%= for spec <- node.specs do %><%= format_spec_attribute(module, node) %> <%= spec %> <% end %>```<% end %> <%= if deprecated = node.deprecated do %>This <%= node.type %> is deprecated. <%= h(deprecated) %>.<% end %> From ce76b7e42fae5e2ae6fde3f6926b372520235d96 Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Tue, 31 Dec 2024 00:08:39 -0500 Subject: [PATCH 05/12] Remove duplicate entries and assets folder in .build file --- lib/ex_doc/formatter/markdown.ex | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/ex_doc/formatter/markdown.ex b/lib/ex_doc/formatter/markdown.ex index 7b660f61c..fbe086a59 100644 --- a/lib/ex_doc/formatter/markdown.ex +++ b/lib/ex_doc/formatter/markdown.ex @@ -40,13 +40,16 @@ defmodule ExDoc.Formatter.Markdown do end all_files = - static_files ++ - generate_extras(extras, config) ++ - generate_logo(@assets_dir, config) ++ - generate_list(nodes_map.modules, config) ++ - generate_list(nodes_map.tasks, config) - - generate_build(Enum.sort(all_files), build) + (static_files ++ + generate_extras(extras, config) ++ + generate_logo(@assets_dir, config) ++ + generate_list(nodes_map.modules, config) ++ + generate_list(nodes_map.tasks, config)) + |> Enum.uniq() + |> Kernel.--([@assets_dir]) + |> Enum.sort() + + generate_build(all_files, build) config.output |> Path.join("index.md") From 44bcc9f2361cb8da4d7b5865d8df72224bd52392 Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Tue, 31 Dec 2024 04:02:28 -0500 Subject: [PATCH 06/12] Update :mdex to v0.3.0 --- bin/ex_doc | 1 + lib/ex_doc/formatter/markdown.ex | 37 +++++++++++++++----------------- mix.exs | 2 +- mix.lock | 12 ++++++++++- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/bin/ex_doc b/bin/ex_doc index 983245f3f..a2d1e9e6e 100755 --- a/bin/ex_doc +++ b/bin/ex_doc @@ -8,6 +8,7 @@ Code.prepend_path Path.expand("../_build/#{mix_env}/lib/makeup_html/ebin", __DIR Code.prepend_path Path.expand("../_build/#{mix_env}/lib/earmark_parser/ebin", __DIR__) Code.prepend_path Path.expand("../_build/#{mix_env}/lib/rustler_precompiled/ebin", __DIR__) Code.prepend_path Path.expand("../_build/#{mix_env}/lib/castore/ebin", __DIR__) +Code.prepend_path Path.expand("../_build/#{mix_env}/lib/jason/ebin", __DIR__) Code.prepend_path Path.expand("../_build/#{mix_env}/lib/mdex/ebin", __DIR__) Code.prepend_path Path.expand("../_build/#{mix_env}/lib/ex_doc/ebin", __DIR__) diff --git a/lib/ex_doc/formatter/markdown.ex b/lib/ex_doc/formatter/markdown.ex index fbe086a59..8f8ea5bbe 100644 --- a/lib/ex_doc/formatter/markdown.ex +++ b/lib/ex_doc/formatter/markdown.ex @@ -129,7 +129,7 @@ defmodule ExDoc.Formatter.Markdown do defp rewrite_headings(markdown, base_heading) when is_binary(markdown) and is_integer(base_heading) and base_heading >= 1 do - {:ok, [{"document", attributes, document}] = _ast} = MDEx.parse_document(markdown) + {:ok, document} = MDEx.parse_document(markdown) document = case find_lowest_heading(document) do @@ -142,18 +142,15 @@ defmodule ExDoc.Formatter.Markdown do bump_levels(document, levels_to_bump) end - ast = [{"document", attributes, document}] - - MDEx.to_commonmark!(ast) + MDEx.to_commonmark!(document) end - defp find_lowest_heading(document) when is_list(document) do + defp find_lowest_heading(document) when is_struct(document, MDEx.Document) do Enum.reduce_while(document, 6, fn - {"heading", [{"level", 1}, _rest_attributes], _children}, _lowest_level -> + %MDEx.Heading{level: 1}, _lowest_level -> {:halt, 1} - {"heading", [{"level", level}, _rest_attributes], _children}, lowest_level - when level < lowest_level -> + %MDEx.Heading{level: level}, lowest_level when level < lowest_level -> {:cont, level} _, lowest_level -> @@ -161,20 +158,20 @@ defmodule ExDoc.Formatter.Markdown do end) end - defp bump_levels(document, levels_to_bump) when is_list(document) do - document - |> Enum.reduce([], fn - {"heading", [{"level", level}, rest_attributes], children}, acc -> - updated_element = - {"heading", [{"level", increase_level(level, levels_to_bump)}, rest_attributes], - children} + defp bump_levels(%MDEx.Document{nodes: nodes} = document, levels_to_bump) do + nodes_updated = + Enum.reduce(nodes, [], fn + %MDEx.Heading{level: level} = heading, acc -> + updated_element = %{heading | level: increase_level(level, levels_to_bump)} - [updated_element | acc] + [updated_element | acc] - elem, acc -> - [elem | acc] - end) - |> Enum.reverse() + elem, acc -> + [elem | acc] + end) + |> Enum.reverse() + + Map.put(document, :nodes, nodes_updated) end defp increase_level(level, levels_to_bump) do diff --git a/mix.exs b/mix.exs index 729d833f7..ee9fcea97 100644 --- a/mix.exs +++ b/mix.exs @@ -44,7 +44,7 @@ defmodule ExDoc.Mixfile do # Add other makeup lexers as optional for the executable {:makeup_c, ">= 0.1.0", optional: true}, {:makeup_html, ">= 0.1.0", optional: true}, - {:jason, "~> 1.2", only: :test}, + {:jason, "~> 1.4"}, {:floki, "~> 0.0", only: :test}, {:easyhtml, "~> 0.0", only: :test}, {:mdex, "~> 0.2"} diff --git a/mix.lock b/mix.lock index 602f17c8e..06ad4df49 100644 --- a/mix.lock +++ b/mix.lock @@ -2,14 +2,24 @@ "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, "earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"}, "easyhtml": {:hex, :easyhtml, "0.3.2", "050adfc8074f53b261f7dfe83303d864f1fbf5988245b369f8fdff1bf4c4b3e6", [:mix], [{:floki, "~> 0.35", [hex: :floki, repo: "hexpm", optional: false]}], "hexpm", "b6a936f91612a4870aa3e828cd8da5a08d9e3b6221b4d3012b6ec70b87845d06"}, + "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, + "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_c": {:hex, :makeup_c, "0.1.1", "14250b1a69770b1892f4113129417a2df098e2a72b9e1477aa9096e9e6c473a6", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "89e9cf45372822d354c19a7e18d77f84cfd70e2d206ac987eb15a1b8357f2869"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "makeup_html": {:hex, :makeup_html, "0.1.1", "c3d4abd39d5f7e925faca72ada6e9cc5c6f5fa7cd5bc0158315832656cf14d7f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "44f2a61bc5243645dd7fafeaa6cc28793cd22f3c76b861e066168f9a5b2c26a4"}, - "mdex": {:hex, :mdex, "0.2.0", "af93e03bc964f2628c3940d22ba03435b119e070bd423fd62d31772d428a7e6c", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "d8d21d3d6ecb0b2a10f88b539f3a61df974f9570226bf6bd24404c9e361c8089"}, + "mdex": {:hex, :mdex, "0.3.0", "aa40e05465f789e74cd33ea34b02829ad271dd15a3b4cc07b21c487b32af8a08", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: false]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "fe792d522778871df0493303306014b97c53b3ce72681de3175eda94d7f68dfc"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "req": {:hex, :req, "0.5.8", "50d8d65279d6e343a5e46980ac2a70e97136182950833a1968b371e753f6a662", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d7fc5898a566477e174f26887821a3c5082b243885520ee4b45555f5d53f40ef"}, + "rustler": {:hex, :rustler, "0.35.1", "ec81961ef9ee833d721dafb4449cab29b16b969a3063a842bb9e3ea912f6b938", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "3713b2e70e68ec2bfa8291dfd9cb811fe64a770f254cd9c331f8b34fa7989115"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.2", "5f25cbe220a8fac3e7ad62e6f950fcdca5a5a5f8501835d2823e8c74bf4268d5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "63d1bd5f8e23096d1ff851839923162096364bac8656a4a3c00d1fff8e83ee0a"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, } From 97cef3ec390685930522864759ceec48b1462722 Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Tue, 31 Dec 2024 04:13:34 -0500 Subject: [PATCH 07/12] Minor fix --- lib/ex_doc/formatter/markdown/templates/detail_template.eex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ex_doc/formatter/markdown/templates/detail_template.eex b/lib/ex_doc/formatter/markdown/templates/detail_template.eex index ebc2e74ad..d5c84bd1e 100644 --- a/lib/ex_doc/formatter/markdown/templates/detail_template.eex +++ b/lib/ex_doc/formatter/markdown/templates/detail_template.eex @@ -4,6 +4,6 @@ <%= if node.specs != [] do %>```<%= config.proglang %> <%= for spec <- node.specs do %><%= format_spec_attribute(module, node) %> <%= spec %> <% end %>```<% end %> -<%= if deprecated = node.deprecated do %>This <%= node.type %> is deprecated. <%= h(deprecated) %>.<% end %> - +<%= if deprecated = node.deprecated do %>**This <%= node.type %> is deprecated. <%= h(deprecated) %>.** +<% end %> <%= if doc = node.rendered_doc do %><%= doc %><% end %> From a156122d54e1b4f5d73eb51107f25f81c72ba910 Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Tue, 31 Dec 2024 16:42:53 -0500 Subject: [PATCH 08/12] Update dep version for mdex --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index ee9fcea97..8af1c7ce1 100644 --- a/mix.exs +++ b/mix.exs @@ -47,7 +47,7 @@ defmodule ExDoc.Mixfile do {:jason, "~> 1.4"}, {:floki, "~> 0.0", only: :test}, {:easyhtml, "~> 0.0", only: :test}, - {:mdex, "~> 0.2"} + {:mdex, "~> 0.3"} ] end From d666704b839de8305830c5239710c5db12dc6025 Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Fri, 28 Mar 2025 13:18:58 -0500 Subject: [PATCH 09/12] Update mdex dependency --- mix.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mix.lock b/mix.lock index 2033520bb..f805c9c7d 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ - "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, + "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "easyhtml": {:hex, :easyhtml, "0.3.2", "050adfc8074f53b261f7dfe83303d864f1fbf5988245b369f8fdff1bf4c4b3e6", [:mix], [{:floki, "~> 0.35", [hex: :floki, repo: "hexpm", optional: false]}], "hexpm", "b6a936f91612a4870aa3e828cd8da5a08d9e3b6221b4d3012b6ec70b87845d06"}, "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, @@ -11,14 +11,14 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "makeup_html": {:hex, :makeup_html, "0.1.1", "c3d4abd39d5f7e925faca72ada6e9cc5c6f5fa7cd5bc0158315832656cf14d7f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "44f2a61bc5243645dd7fafeaa6cc28793cd22f3c76b861e066168f9a5b2c26a4"}, - "mdex": {:hex, :mdex, "0.3.0", "aa40e05465f789e74cd33ea34b02829ad271dd15a3b4cc07b21c487b32af8a08", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: false]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "fe792d522778871df0493303306014b97c53b3ce72681de3175eda94d7f68dfc"}, + "mdex": {:hex, :mdex, "0.4.2", "df88b558a059312b313214afdf311f74f7f6ecf2726ce20ee851ebb9fc068b74", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: false]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "4f7c36abb6cf98dbbe6872e170520d8c3fa32e3515ebd988d03c79421248058c"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "req": {:hex, :req, "0.5.8", "50d8d65279d6e343a5e46980ac2a70e97136182950833a1968b371e753f6a662", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d7fc5898a566477e174f26887821a3c5082b243885520ee4b45555f5d53f40ef"}, - "rustler": {:hex, :rustler, "0.35.1", "ec81961ef9ee833d721dafb4449cab29b16b969a3063a842bb9e3ea912f6b938", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "3713b2e70e68ec2bfa8291dfd9cb811fe64a770f254cd9c331f8b34fa7989115"}, + "rustler": {:hex, :rustler, "0.36.1", "2d4b1ff57ea2789a44756a40dbb5fbb73c6ee0a13d031dcba96d0a5542598a6a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.7", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "f3fba4ad272970e0d1bc62972fc4a99809651e54a125c5242de9bad4574b2d02"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.2", "5f25cbe220a8fac3e7ad62e6f950fcdca5a5a5f8501835d2823e8c74bf4268d5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "63d1bd5f8e23096d1ff851839923162096364bac8656a4a3c00d1fff8e83ee0a"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, From 044203a372f44bb133e2284e03bf341b5f4a041e Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Sat, 29 Mar 2025 12:56:07 -0500 Subject: [PATCH 10/12] Do not rely on ExDoc.Utils.source_url_pattern/3 --- lib/ex_doc/formatter/markdown.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ex_doc/formatter/markdown.ex b/lib/ex_doc/formatter/markdown.ex index 8f8ea5bbe..cc03ce924 100644 --- a/lib/ex_doc/formatter/markdown.ex +++ b/lib/ex_doc/formatter/markdown.ex @@ -413,7 +413,7 @@ defmodule ExDoc.Formatter.Markdown do title = input_options[:title] || title_text || filename_to_title(input) source_path = source_file |> Path.relative_to(File.cwd!()) |> String.replace_leading("./", "") - source_url = Utils.source_url_pattern(source_url_pattern, source_path, 1) + source_url = source_url_pattern.(source_path, 1) %{ source: source, From 805b8ba3578f81571d017dd4f7860f4fbe3b4f6b Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Sat, 29 Mar 2025 12:56:38 -0500 Subject: [PATCH 11/12] Bump levels using Access protocol As suggested in https://github.com/elixir-lang/ex_doc/pull/1992#discussion_r2014045032 --- lib/ex_doc/formatter/markdown.ex | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/lib/ex_doc/formatter/markdown.ex b/lib/ex_doc/formatter/markdown.ex index cc03ce924..6a72114ac 100644 --- a/lib/ex_doc/formatter/markdown.ex +++ b/lib/ex_doc/formatter/markdown.ex @@ -158,20 +158,14 @@ defmodule ExDoc.Formatter.Markdown do end) end - defp bump_levels(%MDEx.Document{nodes: nodes} = document, levels_to_bump) do - nodes_updated = - Enum.reduce(nodes, [], fn - %MDEx.Heading{level: level} = heading, acc -> - updated_element = %{heading | level: increase_level(level, levels_to_bump)} - - [updated_element | acc] - - elem, acc -> - [elem | acc] - end) - |> Enum.reverse() - - Map.put(document, :nodes, nodes_updated) + defp bump_levels(document, levels_to_bump) when is_struct(document, MDEx.Document) do + update_in( + document, + [:document, Access.key!(:nodes), Access.filter(&is_struct(&1, MDEx.Heading))], + fn %MDEx.Heading{level: level} = heading -> + %{heading | level: increase_level(level, levels_to_bump)} + end + ) end defp increase_level(level, levels_to_bump) do From 88590a185f2fa3fc60b16bb246da013ad41a2c0c Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Sat, 29 Mar 2025 13:01:47 -0500 Subject: [PATCH 12/12] Run: mix deps.unlock --unused --- mix.lock | 8 -------- 1 file changed, 8 deletions(-) diff --git a/mix.lock b/mix.lock index f805c9c7d..ac466f31c 100644 --- a/mix.lock +++ b/mix.lock @@ -2,9 +2,7 @@ "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "easyhtml": {:hex, :easyhtml, "0.3.2", "050adfc8074f53b261f7dfe83303d864f1fbf5988245b369f8fdff1bf4c4b3e6", [:mix], [{:floki, "~> 0.35", [hex: :floki, repo: "hexpm", optional: false]}], "hexpm", "b6a936f91612a4870aa3e828cd8da5a08d9e3b6221b4d3012b6ec70b87845d06"}, - "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, - "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_c": {:hex, :makeup_c, "0.1.1", "14250b1a69770b1892f4113129417a2df098e2a72b9e1477aa9096e9e6c473a6", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "89e9cf45372822d354c19a7e18d77f84cfd70e2d206ac987eb15a1b8357f2869"}, @@ -12,14 +10,8 @@ "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "makeup_html": {:hex, :makeup_html, "0.1.1", "c3d4abd39d5f7e925faca72ada6e9cc5c6f5fa7cd5bc0158315832656cf14d7f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "44f2a61bc5243645dd7fafeaa6cc28793cd22f3c76b861e066168f9a5b2c26a4"}, "mdex": {:hex, :mdex, "0.4.2", "df88b558a059312b313214afdf311f74f7f6ecf2726ce20ee851ebb9fc068b74", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: false]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "4f7c36abb6cf98dbbe6872e170520d8c3fa32e3515ebd988d03c79421248058c"}, - "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, - "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, - "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, - "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "req": {:hex, :req, "0.5.8", "50d8d65279d6e343a5e46980ac2a70e97136182950833a1968b371e753f6a662", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d7fc5898a566477e174f26887821a3c5082b243885520ee4b45555f5d53f40ef"}, "rustler": {:hex, :rustler, "0.36.1", "2d4b1ff57ea2789a44756a40dbb5fbb73c6ee0a13d031dcba96d0a5542598a6a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.7", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "f3fba4ad272970e0d1bc62972fc4a99809651e54a125c5242de9bad4574b2d02"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.2", "5f25cbe220a8fac3e7ad62e6f950fcdca5a5a5f8501835d2823e8c74bf4268d5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "63d1bd5f8e23096d1ff851839923162096364bac8656a4a3c00d1fff8e83ee0a"}, - "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, }