From dcad862c24f8bca320e369fb553b06049543d74b Mon Sep 17 00:00:00 2001 From: tmbb Date: Wed, 2 Aug 2017 10:40:42 +0100 Subject: [PATCH 1/6] Markdown modules are now 100% responsible for code Code blocks are now handled by the markdown modules, and ExDoc doesn't do any post-processing. The modules bundled with ExDoc were updated to reflect this. The goal of this change is to make it easier to use alternative Markdown implementations. This might break compatibility for people that are already usin Markdown extensions. --- lib/ex_doc/markdown.ex | 2 +- lib/ex_doc/markdown/cmark.ex | 2 +- lib/ex_doc/markdown/earmark.ex | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ex_doc/markdown.ex b/lib/ex_doc/markdown.ex index 62b457868..f747ab2c6 100644 --- a/lib/ex_doc/markdown.ex +++ b/lib/ex_doc/markdown.ex @@ -29,7 +29,7 @@ defmodule ExDoc.Markdown do Converts the given markdown document to HTML. """ def to_html(text, opts \\ []) when is_binary(text) do - pretty_codeblocks(get_markdown_processor().to_html(text, opts)) + get_markdown_processor().to_html(text, opts) end @doc """ diff --git a/lib/ex_doc/markdown/cmark.ex b/lib/ex_doc/markdown/cmark.ex index 5b92166f5..031810f55 100644 --- a/lib/ex_doc/markdown/cmark.ex +++ b/lib/ex_doc/markdown/cmark.ex @@ -15,6 +15,6 @@ defmodule ExDoc.Markdown.Cmark do Generate HTML output. Cmark takes no options. """ def to_html(text, _opts) do - Cmark.to_html(text) + Cmark.to_html(text) |> ExDoc.Markdown.pretty_codeblocks end end diff --git a/lib/ex_doc/markdown/earmark.ex b/lib/ex_doc/markdown/earmark.ex index 05a79b7c6..d9eb07458 100644 --- a/lib/ex_doc/markdown/earmark.ex +++ b/lib/ex_doc/markdown/earmark.ex @@ -32,6 +32,6 @@ defmodule ExDoc.Markdown.Earmark do file: Keyword.get(opts, :file), breaks: Keyword.get(opts, :breaks, false), smartypants: Keyword.get(opts, :smartypants, true)) - Earmark.as_html!(text, options) + Earmark.as_html!(text, options) |> ExDoc.Markdown.pretty_codeblocks end end From 34ae4f26c633e3139cff6293972caed71d6f52b2 Mon Sep 17 00:00:00 2001 From: tmbb Date: Wed, 2 Aug 2017 12:03:53 +0100 Subject: [PATCH 2/6] Added options to customize templates Docs still missing --- lib/ex_doc.ex | 4 +++ .../html/templates/footer_template.eex | 1 + .../html/templates/head_template.eex | 1 + test/ex_doc/formatter/html_test.exs | 25 ++++++++++++++++++- 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/ex_doc.ex b/lib/ex_doc.ex index 26e86d2cc..fbe26f60f 100644 --- a/lib/ex_doc.ex +++ b/lib/ex_doc.ex @@ -25,6 +25,8 @@ defmodule ExDoc do defstruct [ assets: nil, + before_closing_head_tag: "", + before_closing_body_tag: "", canonical: nil, debug: false, deps: [], @@ -51,6 +53,8 @@ defmodule ExDoc do @type t :: %__MODULE__{ assets: nil | String.t, + before_closing_head_tag: String.t, + before_closing_body_tag: String.t, canonical: nil | String.t, debug: boolean(), deps: [{ebin_path :: String.t, doc_url :: String.t}], diff --git a/lib/ex_doc/formatter/html/templates/footer_template.eex b/lib/ex_doc/formatter/html/templates/footer_template.eex index 92c222e5f..2bdffd672 100644 --- a/lib/ex_doc/formatter/html/templates/footer_template.eex +++ b/lib/ex_doc/formatter/html/templates/footer_template.eex @@ -16,5 +16,6 @@ + <%= config.before_closing_body_tag %> diff --git a/lib/ex_doc/formatter/html/templates/head_template.eex b/lib/ex_doc/formatter/html/templates/head_template.eex index 9b61c4979..3ec59ac7a 100644 --- a/lib/ex_doc/formatter/html/templates/head_template.eex +++ b/lib/ex_doc/formatter/html/templates/head_template.eex @@ -11,6 +11,7 @@ <% end %> + <%= config.before_closing_head_tag %> diff --git a/test/ex_doc/formatter/html_test.exs b/test/ex_doc/formatter/html_test.exs index a4f5b6713..59e342e90 100644 --- a/test/ex_doc/formatter/html_test.exs +++ b/test/ex_doc/formatter/html_test.exs @@ -20,6 +20,9 @@ defmodule ExDoc.Formatter.HTMLTest do File.read!(file) end + @before_closing_head_tag "UNIQUE:©BEFORE-CLOSING-HEAD-TAG" + @before_closing_body_tag "UNIQUE:©BEFORE-CLOSING-BODY-TAG" + defp doc_config do [project: "Elixir", version: "1.0.1", @@ -29,7 +32,9 @@ defmodule ExDoc.Formatter.HTMLTest do source_root: beam_dir(), source_beam: beam_dir(), logo: "test/fixtures/elixir.png", - extras: ["test/fixtures/README.md"]] + extras: ["test/fixtures/README.md"], + before_closing_head_tag: @before_closing_head_tag, + before_closing_body_tag: @before_closing_body_tag] end defp doc_config(config) do @@ -46,18 +51,30 @@ defmodule ExDoc.Formatter.HTMLTest do generate_docs doc_config(source_url: "#{scheme}://github.com/elixir-lang/ex_doc", source_root: File.cwd!) content = File.read!(file_path) assert content =~ "https://github.com/elixir-lang/ex_doc/blob/master/test/fixtures/compiled_with_docs.ex#L14" + # before_closing_*_tag comes is just before the respective tag + assert content =~ ~r[#{@before_closing_head_tag}\s*] + assert content =~ ~r[#{@before_closing_body_tag}\s*] generate_docs doc_config(source_url: "#{scheme}://gitlab.com/elixir-lang/ex_doc", source_root: File.cwd!) content = File.read!(file_path) assert content =~ "https://gitlab.com/elixir-lang/ex_doc/blob/master/test/fixtures/compiled_with_docs.ex#L14" + # before_closing_*_tag comes is just before the respective tag + assert content =~ ~r[#{@before_closing_head_tag}\s*] + assert content =~ ~r[#{@before_closing_body_tag}\s*] generate_docs doc_config(source_url: "#{scheme}://bitbucket.org/elixir-lang/ex_doc", source_root: File.cwd!) content = File.read!(file_path) assert content =~ "https://bitbucket.org/elixir-lang/ex_doc/src/master/test/fixtures/compiled_with_docs.ex#cl-14" + # before_closing_*_tag comes is just before the respective tag + assert content =~ ~r[#{@before_closing_head_tag}\s*] + assert content =~ ~r[#{@before_closing_body_tag}\s*] generate_docs doc_config(source_url: "#{scheme}://example.com/elixir-lang/ex_doc", source_root: File.cwd!) content = File.read!(file_path) assert content =~ "#{scheme}://example.com/elixir-lang/ex_doc" + # before_closing_*_tag comes is just before the respective tag + assert content =~ ~r[#{@before_closing_head_tag}\s*] + assert content =~ ~r[#{@before_closing_body_tag}\s*] end end @@ -186,6 +203,9 @@ defmodule ExDoc.Formatter.HTMLTest do assert content =~ ~r{

moduledoc

} assert content =~ ~r{CompiledWithDocs.Nested} assert content =~ ~r{task_with_docs} + # before_closing_*_tag comes is just before the respective tag + assert content =~ ~r[#{@before_closing_head_tag}\s*] + assert content =~ ~r[#{@before_closing_body_tag}\s*] end test "run generates pages" do @@ -202,6 +222,9 @@ defmodule ExDoc.Formatter.HTMLTest do assert content =~ ~r{RandomError} assert content =~ ~r{CustomBehaviourImpl.hello/1} assert content =~ ~r{TypesAndSpecs.Sub} + # before_closing_*_tag comes is just before the respective tag + assert content =~ ~r[#{@before_closing_head_tag}\s*] + assert content =~ ~r[#{@before_closing_body_tag}\s*] end test "run generates pages with custom names" do From 1559a654af42d95079f35b640af982404569708a Mon Sep 17 00:00:00 2001 From: tmbb Date: Wed, 2 Aug 2017 12:14:16 +0100 Subject: [PATCH 3/6] Added new options to `mix help docs` --- lib/mix/tasks/docs.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/mix/tasks/docs.ex b/lib/mix/tasks/docs.ex index 46d14340f..24300fd34 100644 --- a/lib/mix/tasks/docs.ex +++ b/lib/mix/tasks/docs.ex @@ -54,6 +54,14 @@ defmodule Mix.Tasks.Docs do directory in the output path. Its entries may be referenced in your docs under "assets/ASSET.EXTENSION"; defaults to no assets directory. + * `:before_closing_body_tag` - Literal HTML to be included just before the closing body tag (``) + Useful to inject custom assets, such as Javascript. + Only works with the HTML formatter. + + * `:before_closing_head_tag` - Literal HTML to be included just before the closing head tag (``); + Useful to inject custom assets, such as CSS. + Only works with the HTML formatter. + * `:canonical` - String that defines the preferred URL with the rel="canonical" element; defaults to no canonical path. From feb2c781da486eebf65da9fa1a47a80b9861ad29 Mon Sep 17 00:00:00 2001 From: tmbb Date: Wed, 2 Aug 2017 12:49:22 +0100 Subject: [PATCH 4/6] before_closing_*_tags have their own test now --- test/ex_doc/formatter/html_test.exs | 31 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/test/ex_doc/formatter/html_test.exs b/test/ex_doc/formatter/html_test.exs index 59e342e90..cda64549b 100644 --- a/test/ex_doc/formatter/html_test.exs +++ b/test/ex_doc/formatter/html_test.exs @@ -51,30 +51,18 @@ defmodule ExDoc.Formatter.HTMLTest do generate_docs doc_config(source_url: "#{scheme}://github.com/elixir-lang/ex_doc", source_root: File.cwd!) content = File.read!(file_path) assert content =~ "https://github.com/elixir-lang/ex_doc/blob/master/test/fixtures/compiled_with_docs.ex#L14" - # before_closing_*_tag comes is just before the respective tag - assert content =~ ~r[#{@before_closing_head_tag}\s*] - assert content =~ ~r[#{@before_closing_body_tag}\s*] generate_docs doc_config(source_url: "#{scheme}://gitlab.com/elixir-lang/ex_doc", source_root: File.cwd!) content = File.read!(file_path) assert content =~ "https://gitlab.com/elixir-lang/ex_doc/blob/master/test/fixtures/compiled_with_docs.ex#L14" - # before_closing_*_tag comes is just before the respective tag - assert content =~ ~r[#{@before_closing_head_tag}\s*] - assert content =~ ~r[#{@before_closing_body_tag}\s*] generate_docs doc_config(source_url: "#{scheme}://bitbucket.org/elixir-lang/ex_doc", source_root: File.cwd!) content = File.read!(file_path) assert content =~ "https://bitbucket.org/elixir-lang/ex_doc/src/master/test/fixtures/compiled_with_docs.ex#cl-14" - # before_closing_*_tag comes is just before the respective tag - assert content =~ ~r[#{@before_closing_head_tag}\s*] - assert content =~ ~r[#{@before_closing_body_tag}\s*] generate_docs doc_config(source_url: "#{scheme}://example.com/elixir-lang/ex_doc", source_root: File.cwd!) content = File.read!(file_path) assert content =~ "#{scheme}://example.com/elixir-lang/ex_doc" - # before_closing_*_tag comes is just before the respective tag - assert content =~ ~r[#{@before_closing_head_tag}\s*] - assert content =~ ~r[#{@before_closing_body_tag}\s*] end end @@ -203,9 +191,6 @@ defmodule ExDoc.Formatter.HTMLTest do assert content =~ ~r{

moduledoc

} assert content =~ ~r{CompiledWithDocs.Nested} assert content =~ ~r{task_with_docs} - # before_closing_*_tag comes is just before the respective tag - assert content =~ ~r[#{@before_closing_head_tag}\s*] - assert content =~ ~r[#{@before_closing_body_tag}\s*] end test "run generates pages" do @@ -222,7 +207,21 @@ defmodule ExDoc.Formatter.HTMLTest do assert content =~ ~r{RandomError} assert content =~ ~r{CustomBehaviourImpl.hello/1} assert content =~ ~r{TypesAndSpecs.Sub} - # before_closing_*_tag comes is just before the respective tag + end + + test "before_closing_*_tags are placed in the right place - api reference file" do + generate_docs(doc_config()) + + content = File.read!("#{output_dir()}/api-reference.html") + assert content =~ ~r[#{@before_closing_head_tag}\s*] + assert content =~ ~r[#{@before_closing_body_tag}\s*] + end + + test "before_closing_*_tags are placed in the right place - generated pages" do + config = doc_config([main: "readme"]) + generate_docs(config) + + content = File.read!("#{output_dir()}/readme.html") assert content =~ ~r[#{@before_closing_head_tag}\s*] assert content =~ ~r[#{@before_closing_body_tag}\s*] end From f4b68a73e897c5d2552399f5c9d129f348e5115e Mon Sep 17 00:00:00 2001 From: tmbb Date: Wed, 2 Aug 2017 17:24:12 +0100 Subject: [PATCH 5/6] :markdown_processor is now an option Users can now pass the :markdown_option from the mix project config as well as from the app's environment. The app environment overrides the choice in the mix project config. --- lib/ex_doc.ex | 10 +++ lib/ex_doc/markdown.ex | 12 ++- lib/ex_doc/markdown/mocks.ex | 18 ++++ .../ex_doc/markdown_processor_option_test.exs | 84 +++++++++++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 lib/ex_doc/markdown/mocks.ex create mode 100644 test/ex_doc/markdown_processor_option_test.exs diff --git a/lib/ex_doc.ex b/lib/ex_doc.ex index fbe26f60f..320733c02 100644 --- a/lib/ex_doc.ex +++ b/lib/ex_doc.ex @@ -3,6 +3,8 @@ defmodule ExDoc do Elixir Documentation System. ExDoc produces documentation for Elixir projects """ + alias ExDoc.Markdown + defmodule Config do @moduledoc """ Configuration structure that holds all the available options for ExDoc @@ -39,6 +41,7 @@ defmodule ExDoc do language: @default.language, logo: nil, main: nil, + markdown_processor: nil, output: @default.output, project: nil, retriever: @default.retriever, @@ -67,6 +70,7 @@ defmodule ExDoc do language: String.t, logo: nil | Path.t, main: nil | String.t, + markdown_processor: nil | atom, output: nil | Path.t, project: nil | String.t, retriever: :atom, @@ -102,6 +106,12 @@ defmodule ExDoc do # Builds configuration by merging `options`, and normalizing the options. @spec build_config(String.t, String.t, Keyword.t) :: ExDoc.Config.t defp build_config(project, vsn, options) do + # The app environment has priority over the mix config + processor_from_env = Markdown.get_markdown_processor_from_app_env() + if !processor_from_env && options[:markdown_processor] do + Markdown.set_markdown_processor(options[:markdown_processor]) + end + # After this, we have no use for the :markdown processor option options = normalize_options(options) preconfig = %Config{ project: project, diff --git a/lib/ex_doc/markdown.ex b/lib/ex_doc/markdown.ex index f747ab2c6..8030e3787 100644 --- a/lib/ex_doc/markdown.ex +++ b/lib/ex_doc/markdown.ex @@ -47,13 +47,23 @@ defmodule ExDoc.Markdown do bin end + @doc false + def get_markdown_processor_from_app_env do + Application.get_env(:ex_doc, @markdown_processor_key) + end + + @doc false + def set_markdown_processor(processor) do + Application.put_env(:ex_doc, @markdown_processor_key, processor) + end + defp get_markdown_processor do case Application.fetch_env(:ex_doc, @markdown_processor_key) do {:ok, processor} -> processor :error -> processor = find_markdown_processor() || raise_no_markdown_processor() - Application.put_env(:ex_doc, @markdown_processor_key, processor) + set_markdown_processor(processor) processor end end diff --git a/lib/ex_doc/markdown/mocks.ex b/lib/ex_doc/markdown/mocks.ex new file mode 100644 index 000000000..f91e0f450 --- /dev/null +++ b/lib/ex_doc/markdown/mocks.ex @@ -0,0 +1,18 @@ +defmodule ExDoc.Markdown.MockMarkdownProcessor.MockMarkdownProcessorError do + @moduledoc false + + defexception message: "You're using a mock markdown processor" + end + +defmodule ExDoc.Markdown.MockMarkdownProcessor do + @moduledoc false + + # To be used in only in testing + + def to_html(_, _) do + # When we get an exception, we know this processor's been used + # This seems to be the easiest way to ensure we're choosing the right processor + # without instrumenting the code + raise ExDoc.Markdown.MockMarkdownProcessor.MockMarkdownProcessorError + end +end \ No newline at end of file diff --git a/test/ex_doc/markdown_processor_option_test.exs b/test/ex_doc/markdown_processor_option_test.exs new file mode 100644 index 000000000..16db7febe --- /dev/null +++ b/test/ex_doc/markdown_processor_option_test.exs @@ -0,0 +1,84 @@ +defmodule ExDoc.MarkdownProcessorOptionTest do + use ExUnit.Case + + alias ExDoc.Markdown.MockMarkdownProcessor + alias ExDoc.Markdown.MockMarkdownProcessor.MockMarkdownProcessorError + + setup do + File.rm_rf(output_dir()) + File.mkdir_p!(output_dir()) + end + + defp output_dir do + Path.expand("../../tmp/html", __DIR__) + end + + defp beam_dir do + Path.expand("../../tmp/beam", __DIR__) + end + + defp doc_config_markdown_processor_not_set do + [project: "Elixir", + version: "1.0.1", + formatter: "html", + assets: "test/tmp/html_assets", + output: output_dir(), + source_root: beam_dir(), + source_beam: beam_dir(), + logo: "test/fixtures/elixir.png", + extras: ["test/fixtures/README.md"]] + end + + defp doc_config_markdown_processor_set do + doc_config_markdown_processor_not_set() + # MockMarkdownProcessor will raise an exception if used to process files + # This exception will be our way to tell that someting's not right. + |> Keyword.put(:markdown_processor, MockMarkdownProcessor) + end + + def generate_docs(config) do + # A wrapper around the child process, so that we can use assert_raise below. + # Propagates the MockMarkdownProcessorError exception from the chid process. + # Raises a MockMarkdownProcessorError if the exception causes the child process to exit. + Process.flag(:trap_exit, true) + try do + ExDoc.generate_docs(config[:project], config[:version], config) + catch + # Raise the exception so that it can be captured by assert_raise + :exit, {{%MockMarkdownProcessorError{} = exception, _}, _} -> raise exception + # The purpose of these tests is to detect if the right markdown processor is used. + _ -> :ok + end + end + + test "app env not set, markdown_processor option set" do + Application.delete_env(:ex_doc, :markdown_processor) + # ExDoc.Markdown should respect our choice and run the mock processor + assert_raise MockMarkdownProcessorError, fn -> + generate_docs doc_config_markdown_processor_set() + end + end + + test "app env not set, markdown_processor option not set" do + Application.delete_env(:ex_doc, :markdown_processor) + generate_docs doc_config_markdown_processor_not_set() + end + + test "app env set, markdown_processor option set" do + Application.put_env(:ex_doc, :markdown_processor, MockMarkdownProcessor) + # It's been given two mock processors + assert_raise MockMarkdownProcessorError, fn -> + generate_docs doc_config_markdown_processor_set() + end + end + + test "app env set, markdown_processor option not set" do + Application.put_env(:ex_doc, :markdown_processor, MockMarkdownProcessor) + # The app env has priority + assert_raise MockMarkdownProcessorError, fn -> + generate_docs doc_config_markdown_processor_not_set() + end + end + +end + From 1aa790cad15c91327e597ebca774a820b7f39ba8 Mon Sep 17 00:00:00 2001 From: tmbb Date: Wed, 2 Aug 2017 18:06:53 +0100 Subject: [PATCH 6/6] style issues --- lib/ex_doc/markdown/mocks.ex | 4 ++-- test/ex_doc/markdown_processor_option_test.exs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/ex_doc/markdown/mocks.ex b/lib/ex_doc/markdown/mocks.ex index f91e0f450..0c7fac1d0 100644 --- a/lib/ex_doc/markdown/mocks.ex +++ b/lib/ex_doc/markdown/mocks.ex @@ -12,7 +12,7 @@ defmodule ExDoc.Markdown.MockMarkdownProcessor do def to_html(_, _) do # When we get an exception, we know this processor's been used # This seems to be the easiest way to ensure we're choosing the right processor - # without instrumenting the code + # without instrumenting the code raise ExDoc.Markdown.MockMarkdownProcessor.MockMarkdownProcessorError end -end \ No newline at end of file +end diff --git a/test/ex_doc/markdown_processor_option_test.exs b/test/ex_doc/markdown_processor_option_test.exs index 16db7febe..f5a9d707c 100644 --- a/test/ex_doc/markdown_processor_option_test.exs +++ b/test/ex_doc/markdown_processor_option_test.exs @@ -1,6 +1,6 @@ defmodule ExDoc.MarkdownProcessorOptionTest do use ExUnit.Case - + alias ExDoc.Markdown.MockMarkdownProcessor alias ExDoc.Markdown.MockMarkdownProcessor.MockMarkdownProcessorError @@ -30,10 +30,11 @@ defmodule ExDoc.MarkdownProcessorOptionTest do end defp doc_config_markdown_processor_set do - doc_config_markdown_processor_not_set() # MockMarkdownProcessor will raise an exception if used to process files # This exception will be our way to tell that someting's not right. - |> Keyword.put(:markdown_processor, MockMarkdownProcessor) + Keyword.put(doc_config_markdown_processor_not_set(), + :markdown_processor, + MockMarkdownProcessor) end def generate_docs(config) do @@ -81,4 +82,3 @@ defmodule ExDoc.MarkdownProcessorOptionTest do end end -