Packages
surface
0.9.5
0.12.3
0.12.2
0.12.1
0.12.0
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
0.1.0-alpha.2
0.1.0-alpha.1
0.1.0-alpha.0
A component based library for Phoenix LiveView
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/surface/surface.convert.ex
defmodule Mix.Tasks.Surface.Convert do
@shortdoc "Converts .sface files and ~F sigils from v0.7 to v0.8 syntax"
@moduledoc """
Converts .sface files and ~F sigils from v0.7 to v0.8 syntax.
mix surface.convert "lib/**/*.{ex,exs,sface}" "test/**/*.{ex,exs}"
Please read the [Migration Guide](https://github.com/surface-ui/surface/blob/main/MIGRATING.md)
before running the converter and make sure you follow all required steps for a successful migration.
## Task-specific options
The Surface formatter accepts a couple of the same task-specific options as `mix format`.
Here are some examples of using these options:
```bash
$ mix surface.convert --dot-formatter path/to/.formatter.exs
```
You can also use the same syntax as `mix format` for specifying which files to
convert:
```bash
$ mix surface.convert path/to/file.ex "lib/**/*.{ex,exs,sface}" "test/**/*.{ex,exs}"
```
"""
use Mix.Task
alias Surface.Compiler.Converter
alias Surface.Compiler.Converter_0_8
@converter Converter_0_8
defp format_string(string, converter) do
Converter.convert(string, converter: converter)
end
#
# Functions unique to surface.convert (Everything else is taken from Mix.Tasks.Format)
#
@doc false
def convert_file_contents!(:stdin, input, converter) do
# determine whether the input is Elixir or Surface code by checking if `Code.string_to_quoted` can parse it
case Code.string_to_quoted(input) do
{:ok, _} ->
convert_ex_string!(input, converter)
{:error, _} ->
format_string(input, converter)
end
end
def convert_file_contents!(file, input, converter) do
ext = Path.extname(file)
content =
case ext do
".sface" ->
format_string(input, converter)
_ ->
convert_ex_string!(input, converter)
end
converter.after_convert_file(ext, content)
end
defp convert_ex_string!(input, converter) do
[
{~r/( *)~F"\""(.*?)"""(\s)/s,
fn _match, indent, code, space_after ->
"#{indent}~F\"\"\"#{format_string(code, converter)}\"\"\"#{space_after}"
end},
{~r/~F\"([^\"].*?)\"/s, fn _match, code -> "~F\"#{format_string(code, converter)}\"" end},
{~r/~F\[(.*?)\]/s, fn _match, code -> "~F[#{format_string(code, converter)}]" end},
{~r/~F\((.*?)\)/s, fn _match, code -> "~F(#{format_string(code, converter)})" end},
{~r/~F\{(.*?)\}/s, fn _match, code -> "~F{#{format_string(code, converter)}}" end}
]
|> Enum.reduce(input, fn {regex, replacement}, code ->
Regex.replace(regex, code, replacement)
end)
end
#
# The below functions are taken directly from Mix.Tasks.Format with insignificant modification
#
@switches [
dot_formatter: :string,
dry_run: :boolean
]
@impl true
def run(args) do
{opts, args} = OptionParser.parse!(args, strict: @switches)
{dot_formatter, formatter_opts} = eval_dot_formatter(opts)
args
|> expand_args(dot_formatter, formatter_opts)
|> Task.async_stream(&convert_file(&1, opts, @converter), ordered: false, timeout: 30000)
|> Enum.reduce([], &collect_status/2)
|> check!()
end
defp read_file(:stdin) do
{IO.stream(:stdio, :line) |> Enum.to_list() |> IO.iodata_to_binary(), :stdin}
end
defp read_file(file) do
{File.read!(file), file}
end
defp convert_file({file, _formatter_opts}, task_opts, converter) do
{input, file} = read_file(file)
formatted = convert_file_contents!(file, input, converter)
output = IO.iodata_to_binary([formatted])
dry_run? = Keyword.get(task_opts, :dry_run, false)
cond do
dry_run? ->
:ok
true ->
write_or_print(file, input, output)
end
rescue
exception ->
{:exit, file, exception, __STACKTRACE__}
end
defp eval_dot_formatter(opts) do
cond do
dot_formatter = opts[:dot_formatter] ->
{dot_formatter, eval_file_with_keyword_list(dot_formatter)}
File.regular?(".formatter.exs") ->
{".formatter.exs", eval_file_with_keyword_list(".formatter.exs")}
true ->
{".formatter.exs", []}
end
end
defp eval_file_with_keyword_list(path) do
{opts, _} = Code.eval_file(path)
unless Keyword.keyword?(opts) do
Mix.raise("Expected #{inspect(path)} to return a keyword list, got: #{inspect(opts)}")
end
opts
end
defp expand_args([], dot_formatter, formatter_opts) do
if no_entries_in_formatter_opts?(formatter_opts) do
Mix.raise(
"Expected one or more files/patterns to be given to mix format " <>
"or for a .formatter.exs file to exist with an :inputs, :surface_inputs or :subdirectories key"
)
end
dot_formatter
|> expand_dot_inputs(formatter_opts)
|> Enum.map(fn {file, {_dot_formatter, formatter_opts}} -> {file, formatter_opts} end)
end
defp expand_args(files_and_patterns, _dot_formatter, formatter_opts) do
files =
for file_or_pattern <- files_and_patterns,
file <- stdin_or_wildcard(file_or_pattern),
uniq: true,
do: file
if files == [] do
Mix.raise(
"Could not find a file to format. The files/patterns given to command line " <>
"did not point to any existing file. Got: #{inspect(files_and_patterns)}"
)
end
for file <- files do
{file, formatter_opts}
end
end
defp expand_dot_inputs(dot_formatter, formatter_opts) do
if no_entries_in_formatter_opts?(formatter_opts) do
Mix.raise("Expected :inputs or :surface_inputs key in #{dot_formatter}")
end
for input <- List.wrap(formatter_opts[:surface_inputs] || formatter_opts[:inputs]),
file <- Path.wildcard(Path.join([input]), match_dot: true),
do: {expand_relative_to_cwd(file), {dot_formatter, formatter_opts}},
into: %{}
end
defp expand_relative_to_cwd(path) do
case File.cwd() do
{:ok, cwd} -> Path.expand(path, cwd)
_ -> path
end
end
defp stdin_or_wildcard("-"), do: [:stdin]
defp stdin_or_wildcard(path), do: path |> Path.expand() |> Path.wildcard(match_dot: true)
defp no_entries_in_formatter_opts?(formatter_opts) do
is_nil(formatter_opts[:inputs]) and is_nil(formatter_opts[:surface_inputs])
end
defp write_or_print(file, input, output) do
cond do
file == :stdin -> IO.write(output)
input == output -> :ok
true -> File.write!(file, output)
end
:ok
end
defp collect_status({:ok, :ok}, exits), do: exits
defp collect_status({:ok, {:exit, _, _, _} = exit}, exits) do
[exit | exits]
end
defp check!([]) do
:ok
end
defp check!([{:exit, :stdin, exception, stacktrace} | _]) do
Mix.shell().error("mix surface.convert failed for stdin")
reraise exception, stacktrace
end
defp check!([{:exit, file, exception, stacktrace} | _]) do
Mix.shell().error("mix surface.convert failed for file: #{Path.relative_to_cwd(file)}")
reraise exception, stacktrace
end
end