docs.ex (17756B)
1 defmodule Mix.Tasks.Docs do 2 use Mix.Task 3 4 @shortdoc "Generate documentation for the project" 5 @requirements ["compile"] 6 7 @moduledoc ~S""" 8 Uses ExDoc to generate a static web page from the project documentation. 9 10 ## Command line options 11 12 * `--canonical`, `-n` - Indicate the preferred URL with 13 rel="canonical" link element, defaults to no canonical path 14 15 * `--formatter`, `-f` - Which formatters to use, "html" or 16 "epub". This option can be given more than once. By default, 17 both html and epub are generated. 18 19 * `--language` - Specifies the language to annotate the 20 EPUB output in valid [BCP 47](https://tools.ietf.org/html/bcp47) 21 22 * `--open` - open browser window pointed to the documentation 23 24 * `--output`, `-o` - Output directory for the generated 25 docs, default: `"doc"` 26 27 * `--proglang` - Chooses the main programming language: "elixir" 28 or "erlang" 29 30 The command line options have higher precedence than the options 31 specified in your `mix.exs` file below. 32 33 ## Configuration 34 35 ExDoc will automatically pull in information from your project, 36 like the application and version. However, you may want to set 37 `:name`, `:source_url` and `:homepage_url` to have a nicer output 38 from ExDoc, for example: 39 40 def project do 41 [ 42 app: :my_app, 43 version: "0.1.0-dev", 44 deps: deps(), 45 46 # Docs 47 name: "My App", 48 source_url: "https://github.com/USER/PROJECT", 49 homepage_url: "http://YOUR_PROJECT_HOMEPAGE", 50 docs: [ 51 main: "MyApp", # The main page in the docs 52 logo: "path/to/logo.png", 53 extras: ["README.md"] 54 ] 55 ] 56 end 57 58 ExDoc also allows configuration specific to the documentation to 59 be set. The following options should be put under the `:docs` key 60 in your project's main configuration. The `:docs` options should 61 be a keyword list or a function returning a keyword list that will 62 be lazily executed. 63 64 * `:api_reference` - Whether to generate `api-reference.html`; default: `true`. 65 If this is set to false, `:main` must also be set. 66 67 * `:assets` - Path to a directory that will be copied as is to the "assets" 68 directory in the output path. Its entries may be referenced in your docs 69 under "assets/ASSET.EXTENSION"; defaults to no assets directory. 70 71 * `:authors` - List of authors for the generated docs or epub. 72 73 * `:before_closing_body_tag` - a function that takes as argument an atom specifying 74 the formatter being used (`:html` or `:epub`) and returns a literal HTML string 75 to be included just before the closing body tag (`</body>`). 76 The atom given as argument can be used to include different content in both formats. 77 Useful to inject custom assets, such as Javascript. 78 79 * `:before_closing_head_tag` - a function that takes as argument an atom specifying 80 the formatter being used (`:html` or `:epub`) and returns a literal HTML string 81 to be included just before the closing head tag (`</head>`). 82 The atom given as argument can be used to include different content in both formats. 83 Useful to inject custom assets, such as CSS stylesheets. 84 85 * `:canonical` - String that defines the preferred URL with the rel="canonical" 86 element; defaults to no canonical path. 87 88 * `:cover` - Path to the epub cover image (only PNG or JPEG accepted) 89 The image size should be around 1600x2400. When specified, the cover will be placed under 90 the "assets" directory in the output path under the name "cover" and the 91 appropriate extension. This option has no effect when using the "html" formatter. 92 93 * `:deps` - A keyword list application names and their documentation URL. 94 ExDoc will by default include all dependencies and assume they are hosted on 95 HexDocs. This can be overridden by your own values. Example: `[plug: "https://myserver/plug/"]` 96 97 * `:extra_section` - String that defines the section title of the additional 98 Markdown and plain text pages; default: "PAGES". Example: "GUIDES" 99 100 * `:extras` - List of paths to additional Markdown (`.md` extension), Live Markdown 101 (`.livemd` extension), and plain text pages to add to the documentation. You can 102 also specify keyword pairs to customize the generated filename and title of each 103 extra page; default: `[]`. Example: 104 `["README.md", "LICENSE", "CONTRIBUTING.md": [filename: "contributing", title: "Contributing"]]` 105 106 * `:filter_modules` - Include only modules that match the given value. The 107 value can be a regex, a string (representing a regex), or a two-arity 108 function that receives the module and its metadata and returns true if the 109 module must be included. If a string or a regex is given, it will be matched 110 against the complete module name (which includes the "Elixir." prefix for 111 Elixir modules). If a module has `@moduledoc false`, then it is always excluded. 112 113 * `:formatters` - Formatter to use; default: ["html", "epub"], options: "html", "epub". 114 115 * `:groups_for_extras`, `:groups_for_modules`, `:groups_for_functions` - See the "Groups" section 116 117 * `:ignore_apps` - Apps to be ignored when generating documentation in an umbrella project. 118 Receives a list of atoms. Example: `[:first_app, :second_app]`. 119 120 * `:javascript_config_path` - Path of an additional JavaScript file to be included on all pages 121 to provide up-to-date data for features like the version dropdown - See the "Additional 122 JavaScript config" section. Example: `"../versions.js"` 123 124 * `:language` - Identify the primary language of the documents, its value must be 125 a valid [BCP 47](https://tools.ietf.org/html/bcp47) language tag; default: "en" 126 127 * `:logo` - Path to the image logo of the project (only PNG or JPEG accepted) 128 The image size will be 64x64. When specified, the logo will be placed under 129 the "assets" directory in the output path under the name "logo" and the 130 appropriate extension. 131 132 * `:main` - Main page of the documentation. It may be a module or a 133 generated page, like "Plug" or "api-reference"; default: "api-reference". 134 135 * `:markdown_processor` - The markdown processor to use, 136 either `module()` or `{module(), keyword()}` to provide configuration options; 137 138 * `:nest_modules_by_prefix` - See the "Nesting" section 139 140 * `:output` - Output directory for the generated docs; default: "doc". 141 May be overridden by command line argument. 142 143 * `:skip_undefined_reference_warnings_on` - ExDoc warns when it can't create a `Mod.fun/arity` 144 reference in the current project docs e.g. because of a typo. This list controls where to 145 skip the warnings, for a given module/function/callback/type (e.g.: `["Foo", "Bar.baz/0"]`) 146 or on a given file (e.g.: `["pages/deprecations.md"]`); default: `[]`. 147 148 * `:source_beam` - Path to the beam directory; default: mix's compile path. 149 150 * `:source_ref` - The branch/commit/tag used for source link inference; 151 default: "main". 152 153 * `:source_url_pattern` - Public URL of the project for source links. This is derived 154 automatically from the project's `:source_url` and `:source_ref` when using one of 155 the supported public hosting services (currently GitHub, GitLab, or Bitbucket). If 156 you are using one of those services with their default public hostname, you do not 157 need to set this configuration. 158 159 However, if using a different solution, or self-hosting, you will need to set this 160 configuration variable to a pattern for source code links. The value must be a string 161 of the full URI to use for links with the following variables available for interpolation: 162 163 * `%{path}`: the path of a file in the repo 164 * `%{line}`: the line number in the file 165 166 For GitLab/GitHub: 167 168 ```text 169 https://mydomain.org/user_or_team/repo_name/blob/main/%{path}#L%{line} 170 ``` 171 172 For Bitbucket: 173 174 ```text 175 https://mydomain.org/user_or_team/repo_name/src/main/%{path}#cl-%{line} 176 ``` 177 178 ## Groups 179 180 ExDoc content can be organized in groups. This is done via the `:groups_for_extras` 181 and `:groups_for_modules`. For example, imagine you are storing extra guides in 182 your documentation which are organized per directory. In the extras section you 183 have: 184 185 extras: [ 186 "guides/introduction/foo.md", 187 "guides/introduction/bar.md", 188 189 ... 190 191 "guides/advanced/baz.md", 192 "guides/advanced/bat.md" 193 ] 194 195 You can have those grouped as follows: 196 197 groups_for_extras: [ 198 "Introduction": Path.wildcard("guides/introduction/*.md"), 199 "Advanced": Path.wildcard("guides/advanced/*.md") 200 ] 201 202 Or via a regex: 203 204 groups_for_extras: [ 205 "Introduction": ~r"/introduction/", 206 "Advanced": ~r"/advanced/" 207 ] 208 209 Similar can be done for modules: 210 211 groups_for_modules: [ 212 "Data types": [Atom, Regex, URI], 213 "Collections": [Enum, MapSet, Stream] 214 ] 215 216 A regex or the string name of the module is also supported. 217 218 ### Grouping functions 219 220 Functions inside a module can also be organized in groups. This is done via 221 the `:groups_for_functions` configuration which is a keyword list of group 222 titles and filtering functions that receive the documentation metadata of 223 functions as argument. 224 225 For example, imagine that you have an API client library with a large surface 226 area for all the API endpoints you need to support. It would be helpful to 227 group the functions with similar responsibilities together. In this case in 228 your module you might have: 229 230 defmodule APIClient do 231 @doc section: :auth 232 def refresh_token(params \\ []) 233 234 @doc subject: :object 235 def update_status(id, new_status) 236 237 @doc permission: :grant 238 def grant_privilege(resource, privilege) 239 end 240 241 And then in the configuration you can group these with: 242 243 groups_for_functions: [ 244 Authentication: & &1[:section] == :auth, 245 Resource: & &1[:subject] == :object, 246 Admin: & &1[:permission] in [:grant, :write] 247 ] 248 249 A function can belong to a single group only. If multiple group filters match, 250 the first will take precedence. Functions that don't have a custom group will 251 be listed under the default "Functions" group. 252 253 ## Additional JavaScript config 254 255 Since version `0.20.0` ExDoc includes a way to enrich the documentation 256 with new information without having to re-generate it, through a JavaScript 257 file that can be shared across documentation for multiple versions of the 258 package. If `:javascript_config_path` is set when building the documentation, 259 this script will be referenced in each page's `<head>` using a `<script>` tag. 260 The script should define data in global JavaScript variables that will be 261 interpreted by `ex_doc` when viewing the documentation. 262 263 Currently supported variables: 264 265 ### `versionNodes` 266 267 This global JavaScript variable should be providing an array of objects that 268 define all versions of this Mix package which should appear in the package 269 versions dropdown in the documentation sidebar. The versions dropdown allows 270 for switching between package versions' documentation. 271 272 Example: 273 274 ```javascript 275 var versionNodes = [ 276 { 277 version: "v0.0.0", // version number or name (required) 278 url: "https://hexdocs.pm/ex_doc/0.19.3/" // documentation URL (required) 279 } 280 ] 281 ``` 282 283 ## Nesting 284 285 ExDoc also allows module names in the sidebar to appear nested under a given 286 prefix. The `:nest_modules_by_prefix` expects a list of module names, such as 287 `[Foo.Bar, Bar.Baz]`. In this case, a module named `Foo.Bar.Baz` will appear 288 nested within `Foo.Bar` and only the name `Baz` will be shown in the sidebar. 289 Note the `Foo.Bar` module itself is not affected. 290 291 This option is mainly intended to improve the display of long module names in 292 the sidebar, particularly when they are too long for the sidebar or when many 293 modules share a long prefix. If you mean to group modules logically or call 294 attention to them in the docs, you should probably use `:groups_for_modules` 295 (which can be used in conjunction with `:nest_modules_by_prefix`). 296 297 ## Umbrella project 298 299 ExDoc can be used in an umbrella project and generates a single documentation 300 for all child apps. You can use the `:ignore_apps` configuration to exclude 301 certain projects in the umbrella from documentation. 302 303 Generating documentation per each child app can be achieved by running: 304 305 mix cmd mix docs 306 307 See `mix help cmd` for more information. 308 """ 309 310 @switches [ 311 canonical: :string, 312 formatter: :keep, 313 language: :string, 314 open: :boolean, 315 output: :string, 316 proglang: :string 317 ] 318 319 @aliases [ 320 f: :formatter, 321 n: :canonical, 322 o: :output 323 ] 324 325 @doc false 326 def run(args, config \\ Mix.Project.config(), generator \\ &ExDoc.generate_docs/3) do 327 {:ok, _} = Application.ensure_all_started(:ex_doc) 328 329 unless Code.ensure_loaded?(ExDoc.Config) do 330 Mix.raise( 331 "Could not load ExDoc configuration. Please make sure you are running the " <> 332 "docs task in the same Mix environment it is listed in your deps" 333 ) 334 end 335 336 {cli_opts, args, _} = OptionParser.parse(args, aliases: @aliases, switches: @switches) 337 338 if args != [] do 339 Mix.raise("Extraneous arguments on the command line") 340 end 341 342 project = 343 to_string( 344 config[:name] || config[:app] || 345 Mix.raise("expected :name or :app to be found in the project definition in mix.exs") 346 ) 347 348 version = config[:version] || "dev" 349 350 cli_opts = 351 Keyword.update(cli_opts, :proglang, :elixir, fn proglang -> 352 if proglang not in ~w(erlang elixir) do 353 Mix.raise("--proglang must be elixir or erlang") 354 end 355 356 String.to_atom(proglang) 357 end) 358 359 options = 360 config 361 |> get_docs_opts() 362 |> Keyword.merge(cli_opts) 363 # accepted at root level config 364 |> normalize_source_url(config) 365 # accepted at root level config 366 |> normalize_homepage_url(config) 367 |> normalize_source_beam(config) 368 |> normalize_apps(config) 369 |> normalize_main() 370 |> normalize_deps() 371 |> put_package(config) 372 373 Mix.shell().info("Generating docs...") 374 375 for formatter <- get_formatters(options) do 376 index = generator.(project, version, Keyword.put(options, :formatter, formatter)) 377 Mix.shell().info([:green, "View #{inspect(formatter)} docs at #{inspect(index)}"]) 378 379 if cli_opts[:open] do 380 browser_open(index) 381 end 382 383 index 384 end 385 end 386 387 defp get_formatters(options) do 388 case Keyword.get_values(options, :formatter) do 389 [] -> options[:formatters] || ["html", "epub"] 390 values -> values 391 end 392 end 393 394 defp get_docs_opts(config) do 395 docs = config[:docs] 396 397 cond do 398 is_function(docs, 0) -> docs.() 399 is_nil(docs) -> [] 400 true -> docs 401 end 402 end 403 404 defp normalize_source_url(options, config) do 405 if source_url = config[:source_url] do 406 Keyword.put(options, :source_url, source_url) 407 else 408 options 409 end 410 end 411 412 defp normalize_homepage_url(options, config) do 413 if homepage_url = config[:homepage_url] do 414 Keyword.put(options, :homepage_url, homepage_url) 415 else 416 options 417 end 418 end 419 420 defp normalize_source_beam(options, config) do 421 compile_path = 422 if Mix.Project.umbrella?(config) do 423 umbrella_compile_paths(Keyword.get(options, :ignore_apps, [])) 424 else 425 Mix.Project.compile_path() 426 end 427 428 Keyword.put_new(options, :source_beam, compile_path) 429 end 430 431 defp umbrella_compile_paths(ignored_apps) do 432 build = Mix.Project.build_path() 433 434 for {app, _} <- Mix.Project.apps_paths(), 435 app not in ignored_apps do 436 Path.join([build, "lib", Atom.to_string(app), "ebin"]) 437 end 438 end 439 440 defp normalize_apps(options, config) do 441 if Mix.Project.umbrella?(config) do 442 ignore = Keyword.get(options, :ignore_apps, []) 443 444 apps = 445 for {app, _} <- Mix.Project.apps_paths(), app not in ignore do 446 app 447 end 448 449 Keyword.put(options, :apps, apps) 450 else 451 Keyword.put(options, :apps, List.wrap(config[:app])) 452 end 453 end 454 455 defp normalize_main(options) do 456 main = options[:main] 457 458 cond do 459 is_nil(main) -> 460 Keyword.delete(options, :main) 461 462 is_atom(main) -> 463 Keyword.put(options, :main, inspect(main)) 464 465 is_binary(main) -> 466 options 467 end 468 end 469 470 defp normalize_deps(options) do 471 user_deps = Keyword.get(options, :deps, []) 472 473 deps = 474 for {app, doc} <- Keyword.merge(get_deps(), user_deps), 475 lib_dir = :code.lib_dir(app), 476 is_list(lib_dir), 477 do: {app, doc} 478 479 Keyword.put(options, :deps, deps) 480 end 481 482 defp get_deps do 483 for {key, _} <- Mix.Project.deps_paths(), 484 _ = Application.load(key), 485 vsn = Application.spec(key, :vsn) do 486 {key, "https://hexdocs.pm/#{key}/#{vsn}/"} 487 end 488 end 489 490 defp put_package(options, config) do 491 if package = config[:package] do 492 Keyword.put(options, :package, package[:name] || config[:app]) 493 else 494 options 495 end 496 end 497 498 defp browser_open(path) do 499 {cmd, args, options} = 500 case :os.type() do 501 {:win32, _} -> 502 dirname = Path.dirname(path) 503 basename = Path.basename(path) 504 {"cmd", ["/c", "start", basename], [cd: dirname]} 505 506 {:unix, :darwin} -> 507 {"open", [path], []} 508 509 {:unix, _} -> 510 {"xdg-open", [path], []} 511 end 512 513 System.cmd(cmd, args, options) 514 end 515 end