Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/ex_doc/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ defmodule ExDoc.CLI do
default: "PAGES"
-i, --filter-prefix Include only modules that match the given prefix in
the generated documentation.
-f, --formatter Docs formatter to use (html or epub), default: "html"
-f, --formatter Docs formatter to use (html, epub, or json), default: "html"
-p, --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"
Expand Down
152 changes: 152 additions & 0 deletions lib/ex_doc/formatter/json.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
defmodule ExDoc.Formatter.JSON do
@moduledoc """
Generate JSON documentation for Elixir projects

The ExDoc JSON formatter starts with some information at the top:

## Top-level information

* `about` - Indicates the version of the JSON structure format.
* `name` - Project name
* `version` - Project version
* `description` - Project summary description
* `homepage_url` - Specifies the project's home page
* `language` - Identifies the primary language of the documents.
* `icon` - Identifies the URL of the project's logo
* `items` - This JSON object contains modules, exceptions, protocols,
Mix tasks, extras, and attachments details.

## Modules, Exceptions, Protocols, Mix tasks

Each component is an array that includes:

* `module` - Module name
* `title` - Module title
* `doc` - Module documentation summary
* `doc_line` - Line number where the module documentation starts
* `source_path` - Path to the source code file in the project
* `source_url` - URL to the source code
* `types` - List of types
* `functions` - List of functions
* `callbacks` - List of callbacks

### Function, callback, and type details

* `name` - Function name
* `arity`- Function arity
* `defaults` - Default argument values
* `doc` - Function documentation
* `doc_line` - Line number where the module documentation starts
* `source_path` - Path to the source code file in the project
* `source_url` - URL to the source code
* `signature` - Indicates the function signature
* `annotations` - Show annotations

## Extras

This JSON object include the following fields:

* `id` - Identifier
* `title` - Document title
* `group` - Specifies the group
* `content` - The document content in HTML format

## Attachments

* `path` - Relative path
* `title` - Attachment title
* `size_in_bytes` - File size in bytes

"""

alias Mix.Project
alias ExDoc.Formatter.HTML

@spec run(list, ExDoc.Config.t) :: String.t
def run(project_nodes, config) when is_map(config) do
config =
config
|> normalize_config()
|> output_setup()

config
|> create_project_node(project_nodes)
|> Poison.encode!()
|> write!(config.output)

Path.relative_to_cwd(config.output)
end

defp normalize_config(config) do
config
|> Map.put(:output, Path.expand(config.output))
|> Map.put(:name, (Project.config[:name] || config.project))
|> Map.put(:description, Project.config[:description])
end

defp output_setup(config) do
file_name = config.name |> String.downcase() |> Kernel.<>(".json")
output = Path.join(config.output, file_name)

if File.exists?(output) do
File.rm!(output)
else
File.mkdir_p!(config.output)
end

%{config | output: output}
end

defp create_project_node(config, project_nodes) do
%ExDoc.ProjectNode{
name: config.name,
version: config.version,
homepage_url: config.homepage_url,
description: config.description,
icon: config.logo,
items: %{
modules: filter_by_type(:module, project_nodes),
exceptions: filter_by_type(:exception, project_nodes),
protocols: filter_by_type(:protocol, project_nodes),
tasks: filter_by_type(:task, project_nodes),
extras: HTML.build_extras(project_nodes, config, ".html"),
attachments: extract_attachments_info(config),
},
language: config.language,
}
end

defp filter_by_type(type, project_nodes) do
type
|> HTML.filter_list(project_nodes)
|> Enum.map(fn mod ->
mod =
mod
|> Map.from_struct()
|> Map.merge(HTML.Templates.group_summary(mod))

struct(ExDoc.LeanModuleNode, mod)
end)
end

defp write!(content, output) do
File.write!(output, content)
end

defp extract_attachments_info(config) do
if path = config.assets do
path
|> Path.join("**/*")
|> Path.wildcard()
|> Enum.map(fn source ->
%{
path: Path.join("assets", Path.relative_to(source, path)),
title: HTML.title_to_id(source),
size_in_bytes: File.stat!(source).size,
}
end)
else
[]
end
end
end
61 changes: 53 additions & 8 deletions lib/ex_doc/retriever.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,55 @@
defmodule ExDoc.ProjectNode do
@moduledoc """
Structure that represents a *project*
"""

@derive [Poison.Encoder]
defstruct [:name, :version, :homepage_url, :description, :icon, :language,
:items, :attachments, :extras, about: "ExDoc/version/1"]

@type t :: %__MODULE__{
about: String.t,
name: String.t,
version: String.t,
homepage_url: String.t,
description: String.t,
icon: String.t,
language: String.t,
items: map
}
end

defmodule ExDoc.LeanModuleNode do
@moduledoc """
Alternative structure to represent a *module*

See: `ExDoc.ModuleNode` is the original structure.
"""

@derive [Poison.Encoder]
defstruct [:module, :doc, :doc_line, :source_path, :source_url, :title,
types: [], functions: [], callbacks: []]

@type t :: %__MODULE__{
title: nil | String.t,
module: nil | String.t,
types: list(),
functions: list(),
callbacks: list(),
doc: nil | String.t,
doc_line: non_neg_integer(),
source_path: nil | String.t,
source_url: nil | String.t
}
end

defmodule ExDoc.ModuleNode do
@moduledoc """
Structure that represents a *module*
"""

defstruct id: nil, title: nil, module: nil, doc: nil, doc_line: nil,
docs: [], typespecs: [], source_path: nil, source_url: nil, type: nil
defstruct [:id, :module, :doc, :doc_line, :source_path, :source_url, :type,
:title, docs: [], typespecs: []]

@type t :: %__MODULE__{
id: nil | String.t,
Expand All @@ -25,9 +70,9 @@ defmodule ExDoc.FunctionNode do
Structure that holds all the elements of an individual *function*
"""

defstruct id: nil, name: nil, arity: 0, defaults: [], doc: [],
type: nil, signature: nil, specs: [], annotations: [],
doc_line: nil, source_path: nil, source_url: nil
@derive {Poison.Encoder, except: [:id, :specs, :type]}
defstruct [:id, :name, :type, :signature, :doc_line, :source_path,
:source_url, arity: 0, defaults: [], doc: [], specs: [], annotations: []]

@type t :: %__MODULE__{
id: nil | String.t,
Expand All @@ -50,9 +95,9 @@ defmodule ExDoc.TypeNode do
Structure that holds all the elements of an individual *type*
"""

defstruct id: nil, name: nil, arity: 0, type: nil, doc_line: nil,
source_path: nil, source_url: nil, spec: nil, doc: nil,
signature: nil, annotations: []
@derive {Poison.Encoder, except: [:id, :spec, :type]}
defstruct [:id, :name, :type, :signature, :doc_line, :source_path,
:source_url, :spec, :doc, annotations: [], arity: 0]

@type t :: %__MODULE__{
id: nil | String.t,
Expand Down
2 changes: 1 addition & 1 deletion lib/mix/tasks/docs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ defmodule Mix.Tasks.Docs do
the generated documentation. Example: "MyApp.Core"

* `:formatters` - Formatter to use; default: ["html"],
options: "html", "epub".
options: "html", "epub", "json".

* `: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"
Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ defmodule ExDoc.Mixfile do

defp deps do
[{:earmark, "~> 1.1"},
{:poison, "~> 3.0"},
{:markdown, github: "devinus/markdown", only: :test},
{:cmark, "~> 0.5", only: :test},
{:excoveralls, "~> 0.3", only: :test}]
Expand Down
21 changes: 11 additions & 10 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
%{"certifi": {:hex, :certifi, "1.0.0", "1c787a85b1855ba354f0b8920392c19aa1d06b0ee1362f9141279620a5be2039", [], [], "hexpm"},
"cmark": {:hex, :cmark, "0.7.0", "cf20106714b35801df3a2250a8ca478366f93ad6067df9d6815c80b2606ae01e", [], [], "hexpm"},
%{"certifi": {:hex, :certifi, "1.0.0", "1c787a85b1855ba354f0b8920392c19aa1d06b0ee1362f9141279620a5be2039", [:rebar3], [], "hexpm"},
"cmark": {:hex, :cmark, "0.7.0", "cf20106714b35801df3a2250a8ca478366f93ad6067df9d6815c80b2606ae01e", [:make, :mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.1.1", "433136b7f2e99cde88b745b3a0cfc3fbc81fe58b918a09b40fce7f00db4d8187", [:mix], [], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.6.2", "0e993d096f1fbb6e70a3daced5c89aac066bda6bce57829622aa2d1e2b338cfb", [], [{:exjsx, "~> 3.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
"hackney": {:hex, :hackney, "1.7.1", "e238c52c5df3c3b16ce613d3a51c7220a784d734879b1e231c9babd433ac1cb4", [], [{:certifi, "1.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "4.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.6.2", "0e993d096f1fbb6e70a3daced5c89aac066bda6bce57829622aa2d1e2b338cfb", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
"hackney": {:hex, :hackney, "1.7.1", "e238c52c5df3c3b16ce613d3a51c7220a784d734879b1e231c9babd433ac1cb4", [:rebar3], [{:certifi, "1.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "4.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"hoedown": {:git, "https://github.com/hoedown/hoedown.git", "980b9c549b4348d50b683ecee6abee470b98acda", []},
"idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [], [], "hexpm"},
"jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [], [], "hexpm"},
"idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], [], "hexpm"},
"jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], [], "hexpm"},
"markdown": {:git, "https://github.com/devinus/markdown.git", "d065dbcc4e242a85ca2516fdadd0082712871fd8", []},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"},
"ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5", "2e73e068cd6393526f9fa6d399353d7c9477d6886ba005f323b592d389fb47be", [:make], []}}
62 changes: 62 additions & 0 deletions test/ex_doc/formatter/json_tests.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
defmodule ExDoc.Formatter.JSONTest do
use ExUnit.Case, async: true

setup do
File.rm_rf(output_dir())
File.mkdir_p!(output_dir())
end

def output_dir() do
Path.expand("../../tmp/json", __DIR__)
end

def beam_dir() do
Path.expand("../../tmp/beam", __DIR__)
end

defp doc_config() do
[project: "Elixir",
version: "1.0.1",
formatter: "json",
output: output_dir(),
source_root: beam_dir(),
source_beam: beam_dir(),
extras: ["test/fixtures/README.md"]]
end

defp doc_config(config) do
Keyword.merge(doc_config(), config)
end

defp generate_docs(config) do
ExDoc.generate_docs(config[:project], config[:version], config)
end

defp decode(content) do
tree = %ExDoc.ProjectNode{
items: %{
modules: module_tree(),
exceptions: module_tree(),
protocols: module_tree(),
tasks: module_tree(),
}
}
Poison.decode!(content, as: tree)
end

defp module_tree() do
[
%ExDoc.LeanModuleNode{
types: [%ExDoc.TypeNode{}],
functions: [%ExDoc.FunctionNode{}],
callbacks: [%ExDoc.FunctionNode{}],
}
]
end

test "run generates JSON content" do
generate_docs(doc_config([language: "fr"]))

assert %ExDoc.ProjectNode{version: "1.0.1", name: "Elixir", language: "fr"} = output_dir() |> Path.join("elixir.json") |> File.read!() |> decode()
end
end