Packages
editorconfig_core
0.1.0
EditorConfig Core implementation for Elixir: parser, glob matcher, resolver, and CLI.
Current section
Files
Jump to
Current section
Files
lib/editor_config/parser.ex
defmodule EditorConfig.Parser do
@moduledoc "Parser for EditorConfig files."
alias EditorConfig.File
alias EditorConfig.File.Section
@bom <<0xEF, 0xBB, 0xBF>>
@doc """
Parses EditorConfig contents.
## Examples
iex> {:ok, file} = EditorConfig.Parser.parse("[*.ex]\\nindent_size = 2\\n")
iex> hd(file.sections).name
"*.ex"
"""
@spec parse(binary(), keyword()) :: {:ok, File.t()} | {:error, term()}
def parse(binary, opts \\ []) when is_binary(binary) do
case parse_with_warnings(binary, opts) do
{:ok, file, _warnings} -> {:ok, file}
{:error, reason} -> {:error, reason}
end
end
@doc """
Parses EditorConfig contents and returns warnings for ignored invalid lines.
## Examples
iex> {:ok, file, warnings} =
...> EditorConfig.Parser.parse_with_warnings("[*]\\ninvalid line\\n")
iex> {length(file.sections), warnings}
{1, [%{line: 2, reason: :invalid_line, text: "invalid line"}]}
"""
@spec parse_with_warnings(binary(), keyword()) ::
{:ok, File.t(), [map()]} | {:error, term()}
def parse_with_warnings(binary, opts \\ []) when is_binary(binary) do
strict? = Keyword.get(opts, :strict, false)
binary
|> strip_bom()
|> String.split(~r/\r\n|\n/, trim: false)
|> Enum.with_index(1)
|> Enum.reduce_while({%File{}, nil, [], strict?}, &parse_line/2)
|> finish()
end
defp strip_bom(<<@bom, rest::binary>>), do: rest
defp strip_bom(binary), do: binary
defp parse_line({raw, line}, {file, current, warnings, strict?}) do
text = String.trim(raw)
cond do
text == "" ->
{:cont, {file, current, warnings, strict?}}
String.starts_with?(text, [";", "#"]) ->
{:cont, {file, current, warnings, strict?}}
String.starts_with?(text, "[") and String.ends_with?(text, "]") ->
section = %Section{name: String.slice(text, 1..-2//1), line: line}
{:cont, {append_section(file, current), section, warnings, strict?}}
String.contains?(text, "=") ->
{key, value} = split_pair(text)
{:cont,
{put_pair(file, current, key, value), update_current(current, key, value), warnings,
strict?}}
strict? ->
{:halt, {:error, {:invalid_line, line, text}}}
true ->
warning = %{line: line, reason: :invalid_line, text: text}
{:cont, {file, current, [warning | warnings], strict?}}
end
end
defp split_pair(text) do
[key, value] = String.split(text, "=", parts: 2)
{String.downcase(String.trim(key)), String.trim(value)}
end
defp put_pair(%File{} = file, nil, "root" = key, value) do
%{
file
| preamble: Map.put(file.preamble, key, value),
root?: root_value?(key, value)
}
end
defp put_pair(%File{} = file, nil, key, value) do
%{file | preamble: Map.put(file.preamble, key, value)}
end
defp put_pair(%File{} = file, %Section{} = current, key, value) do
file = append_section(file, current)
section = update_current(current, key, value)
%{file | sections: List.replace_at(file.sections, -1, section)}
end
defp update_current(nil, _key, _value), do: nil
defp update_current(%Section{} = section, key, value) do
%{section | properties: Map.put(section.properties, key, value)}
end
defp append_section(file, nil), do: file
defp append_section(%File{} = file, %Section{} = section) do
if Enum.any?(file.sections, &(&1.line == section.line)) do
file
else
%{file | sections: file.sections ++ [section]}
end
end
defp root_value?("root", value), do: String.downcase(value) == "true"
defp root_value?(_key, _value), do: false
defp finish({:error, reason}), do: {:error, reason}
defp finish({file, nil, warnings, _strict?}), do: {:ok, file, Enum.reverse(warnings)}
defp finish({file, %Section{} = section, warnings, _strict?}) do
{:ok, append_section(file, section), Enum.reverse(warnings)}
end
end