doc_ast.ex (5339B)
1 defmodule ExDoc.DocAST do 2 @moduledoc false 3 4 @type t :: term() 5 6 alias ExDoc.Markdown 7 8 @doc """ 9 Parses given `doc_content` according to `doc_format`. 10 """ 11 def parse!(doc_content, doc_format, options \\ []) 12 13 def parse!(markdown, "text/markdown", opts) do 14 parse_markdown(markdown, opts) 15 end 16 17 def parse!(ast, "application/erlang+html", _options) do 18 parse_erl_ast(ast) 19 end 20 21 def parse!(_ast, other, _opts) do 22 raise "content type #{inspect(other)} is not supported" 23 end 24 25 # https://www.w3.org/TR/2011/WD-html-markup-20110113/syntax.html#void-element 26 @void_elements ~W(area base br col command embed hr img input keygen link 27 meta param source track wbr)a 28 29 @doc """ 30 Transform AST into string. 31 """ 32 def to_string(ast, fun \\ fn _ast, string -> string end) 33 34 def to_string(binary, _fun) when is_binary(binary) do 35 ExDoc.Utils.h(binary) 36 end 37 38 def to_string(list, fun) when is_list(list) do 39 result = Enum.map_join(list, "", &to_string(&1, fun)) 40 fun.(list, result) 41 end 42 43 def to_string({tag, attrs, _inner, _meta} = ast, fun) when tag in @void_elements do 44 result = "<#{tag}#{ast_attributes_to_string(attrs)}/>" 45 fun.(ast, result) 46 end 47 48 def to_string({tag, attrs, inner, %{verbatim: true}} = ast, fun) do 49 inner = Enum.join(inner, "") 50 result = "<#{tag}#{ast_attributes_to_string(attrs)}>" <> inner <> "</#{tag}>" 51 fun.(ast, result) 52 end 53 54 def to_string({tag, attrs, inner, _meta} = ast, fun) do 55 result = "<#{tag}#{ast_attributes_to_string(attrs)}>" <> to_string(inner, fun) <> "</#{tag}>" 56 fun.(ast, result) 57 end 58 59 defp ast_attributes_to_string(attrs) do 60 Enum.map(attrs, fn {key, val} -> " #{key}=\"#{val}\"" end) 61 end 62 63 ## parse markdown 64 65 defp parse_markdown(markdown, opts) do 66 Markdown.to_ast(markdown, opts) 67 end 68 69 ## parse erlang+html 70 71 defp parse_erl_ast(binary) when is_binary(binary) do 72 binary 73 end 74 75 defp parse_erl_ast(list) when is_list(list) do 76 Enum.map(list, &parse_erl_ast/1) 77 end 78 79 defp parse_erl_ast({:pre, attrs, content}) do 80 case content do 81 # if we already have <pre><code>...</code></pre>, carry on 82 [{:code, _, _}] -> 83 {:pre, attrs, parse_erl_ast(content), %{}} 84 85 # otherwise, turn <pre>...</pre> into <pre><code>...</code></pre> 86 _ -> 87 content = [{:code, [], parse_erl_ast(content), %{}}] 88 {:pre, attrs, content, %{}} 89 end 90 end 91 92 defp parse_erl_ast({tag, attrs, content}) when is_atom(tag) do 93 {tag, attrs, parse_erl_ast(content), %{}} 94 end 95 96 @doc """ 97 Extracts leading title element from the given AST. 98 99 If found, the title element is stripped from the resulting AST. 100 """ 101 def extract_title(ast) 102 103 def extract_title([{:h1, _attrs, inner, _meta} | ast]) do 104 {:ok, inner, ast} 105 end 106 107 def extract_title(_ast) do 108 :error 109 end 110 111 @doc """ 112 Returns text content from the given AST. 113 """ 114 def text_from_ast(ast) do 115 ast 116 |> do_text_from_ast() 117 |> IO.iodata_to_binary() 118 |> String.trim() 119 end 120 121 def do_text_from_ast(ast) when is_list(ast) do 122 Enum.map(ast, &do_text_from_ast/1) 123 end 124 125 def do_text_from_ast(ast) when is_binary(ast), do: ast 126 def do_text_from_ast({_tag, _attr, ast, _meta}), do: text_from_ast(ast) 127 128 @doc """ 129 Highlights a DocAST converted to string. 130 """ 131 def highlight(html, language, opts \\ []) do 132 highlight_info = language.highlight_info() 133 134 Regex.replace( 135 ~r/<pre(\s+class="\w*")?><code(?:\s+class="(\w*)")?>([^<]*)<\/code><\/pre>/, 136 html, 137 &highlight_code_block(&1, &2, &3, &4, highlight_info, opts) 138 ) 139 end 140 141 defp highlight_code_block(full_block, pre_attr, lang, code, highlight_info, outer_opts) do 142 case pick_language_and_lexer(lang, highlight_info, code) do 143 {_language, nil, _opts} -> full_block 144 {lang, lexer, opts} -> render_code(pre_attr, lang, lexer, opts, code, outer_opts) 145 end 146 end 147 148 defp pick_language_and_lexer("", _highlight_info, "$ " <> _) do 149 {"shell", ExDoc.ShellLexer, []} 150 end 151 152 defp pick_language_and_lexer("output", highlight_info, _code) do 153 {"output", highlight_info.lexer, highlight_info.opts} 154 end 155 156 defp pick_language_and_lexer("", highlight_info, _code) do 157 {highlight_info.language_name, highlight_info.lexer, highlight_info.opts} 158 end 159 160 defp pick_language_and_lexer(lang, _highlight_info, _code) do 161 case Makeup.Registry.fetch_lexer_by_name(lang) do 162 {:ok, {lexer, opts}} -> {lang, lexer, opts} 163 :error -> {lang, nil, []} 164 end 165 end 166 167 defp render_code(pre_attr, lang, lexer, lexer_opts, code, opts) do 168 highlight_tag = Keyword.get(opts, :highlight_tag, "span") 169 170 highlighted = 171 code 172 |> unescape_html() 173 |> IO.iodata_to_binary() 174 |> Makeup.highlight_inner_html( 175 lexer: lexer, 176 lexer_options: lexer_opts, 177 formatter_options: [highlight_tag: highlight_tag] 178 ) 179 180 ~s(<pre#{pre_attr}><code class="makeup #{lang}" translate="no">#{highlighted}</code></pre>) 181 end 182 183 entities = [{"&", ?&}, {"<", ?<}, {">", ?>}, {""", ?"}, {"'", ?'}] 184 185 for {encoded, decoded} <- entities do 186 defp unescape_html(unquote(encoded) <> rest) do 187 [unquote(decoded) | unescape_html(rest)] 188 end 189 end 190 191 defp unescape_html(<<c, rest::binary>>) do 192 [c | unescape_html(rest)] 193 end 194 195 defp unescape_html(<<>>) do 196 [] 197 end 198 end