defmodule ExamplesStyler.Examples do @moduledoc false @behaviour Mix.Tasks.Format alias ExamplesStyler.Utils alias Mix.Tasks.Format # Regex index match on a code example. @typep regex_index() :: [{non_neg_integer(), non_neg_integer()}] # Tags for a span of input text. @typep span_type() :: :code_example | :other # Tagged length of input text. @typep span_length() :: {span_type(), non_neg_integer()} # Tagged section of input text. @typep span() :: {span_type(), String.t()} @doc false @impl Format @spec features(Utils.format_opts()) :: [sigils: [atom()], extensions: [String.t()]] def features(_opts), do: [sigils: [], extensions: [".ex", ".exs", ".md", ".cheatmd"]] @docs_regex ~r/(?>@doc|@moduledoc|@typedoc)\s*""".*"""/msU @code_example_regex ~r/(?>^(?>[\t ]*iex>.*$)(?>{\r\n|\n|\r})?)+/m @doc false @impl Format @spec format(String.t(), Utils.format_opts()) :: String.t() def format(input, opts) do elixir_plugin = Utils.find_elixir_formatter(opts) extension = Keyword.fetch!(opts, :extension) docs_indexes = if extension in [".ex", ".exs"] do Regex.scan(@docs_regex, input, return: :index) else [[{0, byte_size(input)}]] end example_indexes = Regex.scan(@code_example_regex, input, return: :index) span_lengths = calculate_spans(input, example_indexes, docs_indexes) input |> split_spans(span_lengths) |> Enum.map(fn {:code_example, example_string} -> reformat_example(example_string, elixir_plugin, opts) {:other, other_string} -> other_string end) |> IO.iodata_to_binary() end # Created a list of contiguous character lengths and string types for example and # non-example code. @spec calculate_spans(String.t(), [regex_index()], [regex_index()] | nil) :: [span_length()] defp calculate_spans(input, example_indexes, docs_indexes) do example_indexes |> Enum.map(fn [{_, _} = index] -> index end) |> Enum.filter(fn {example_start, example_length} -> example_end = example_start + example_length Enum.any?(docs_indexes, fn [{doc_start, doc_length}] -> doc_end = doc_start + doc_length example_start >= doc_start and example_end <= doc_end end) end) |> Enum.reduce({0, []}, fn {start, length}, {cursor, spans} -> pre_example = start - cursor example = length spans = [{:other, pre_example} | spans] spans = [{:code_example, example} | spans] {cursor + pre_example + example, spans} end) |> then(fn {cursor, spans} -> final_span = byte_size(input) - cursor [{:other, final_span} | spans] end) |> Enum.reverse() end # Split the input string into alternating example and non-example strings. @spec split_spans(String.t(), [span_length()]) :: [span()] defp split_spans(input, span_lengths) do span_lengths |> Enum.reduce({0, []}, fn {type, length}, {cursor, span_strings} -> span_string = binary_part(input, cursor, length) {cursor + length, [{type, span_string} | span_strings]} end) |> then(fn {_, span_strings} -> span_strings end) |> Enum.reverse() end # Reformats a single example. @spec reformat_example(String.t(), module(), Utils.format_opts()) :: String.t() defp reformat_example(example, elixir_plugin, opts) do [indent] = Regex.run(~r/^[\t ]*/, example) prompt = indent <> "iex> " example |> String.replace(~r/^[ \t]*iex>[ \t]*/m, "") |> elixir_plugin.format(opts) |> String.replace(~r/^/m, prompt) |> String.trim_trailing(prompt) end end