Current section
Files
Jump to
Current section
Files
lib/styler_examples.ex
defmodule Styler.Examples do
@moduledoc """
Styles Code examples in ".ex", ".exs", ".md", and ".cheatmd" files
"""
@behaviour Mix.Tasks.Format
alias Styler.Examples.MultiPlugin
@typedoc false
@typep format_opts() :: [
{:extension, String.t()}
| {:file, Path.t()}
| {:plugins, [module()]}
| {:sigils, [atom()]}
| {:inputs, [String.t()]}
| {atom(), any()}
]
# 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 Mix.Tasks.Format
@spec features(format_opts()) :: [sigils: [atom()], extensions: [String.t()]]
def features(_opts), do: [sigils: [], extensions: [".ex", ".exs", ".md", ".cheatmd"]]
@code_example_regex ~r/(?>^(?>[\t ]*iex>.*$)(?>{\r\n|\n|\r})?)+/m
@doc false
@impl Mix.Tasks.Format
@spec format(String.t(), format_opts()) :: String.t()
def format(input, opts) do
example_indexes = Regex.scan(@code_example_regex, input, return: :index)
span_lengths = calculate_spans(input, example_indexes)
input
|> split_spans(span_lengths)
|> Enum.map(fn
{:code_example, example_string} -> reformat_example(example_string, opts)
{:other, other_string} -> other_string
end)
|> List.to_string()
end
# Created a list of contiguous character lengths and string types for example and
# non-example code.
@spec calculate_spans(String.t(), [regex_index()]) :: [span_length()]
defp calculate_spans(input, example_indexes) do
example_indexes
|> Enum.map(fn [{_, _} = index] -> index 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(), format_opts()) :: String.t()
defp reformat_example(example, opts) do
[indent] = Regex.run(~r/^[\t ]*/, example)
# We need to remove the current formatter from the list to avoid recursively calling
# this function, and update the extension to ".exs" so that we trigger the correct
# formatters, even if we are in a markdown file.
opts =
opts
|> Keyword.update(:plugins, [], &List.delete(&1, __MODULE__))
|> Keyword.put(:extension, ".exs")
prompt = indent <> "iex> "
example
|> String.replace(~r/^[ \t]*iex>[ \t]*/m, "")
|> MultiPlugin.format(opts)
|> String.replace(~r/^/m, prompt)
|> String.trim_trailing(prompt)
end
end