Current section
Files
Jump to
Current section
Files
lib/toon/decoder/strict_mode.ex
defmodule ExToon.Decoder.StrictMode do
@moduledoc false
# Strict-mode validation pass over scanned lines.
#
# Checks performed:
# - Indentation is a multiple of indent_size (no partial-indent lines)
# - No tab characters used as indentation (spaces only)
# - Quoted strings have valid escape sequences
alias ExToon.{DecodeError, Validation}
alias ExToon.Decoder.Scanner
# Validate lines for strict-mode compliance.
# Returns :ok or {:error, DecodeError.t()} on the first violation found.
@spec validate_lines([Scanner.t()], pos_integer()) :: :ok | {:error, DecodeError.t()}
def validate_lines(parsed_lines, indent_size) do
Enum.reduce_while(parsed_lines, :ok, fn line, :ok ->
if String.trim(line.raw) == "" do
{:cont, :ok}
else
with :ok <- check_indentation(line, indent_size),
:ok <- check_escape_sequences(line) do
{:cont, :ok}
else
{:error, e} -> {:halt, {:error, e}}
end
end
end)
end
# ---------------------------------------------------------------------------
# Indentation check
# ---------------------------------------------------------------------------
defp check_indentation(line, indent_size) do
# Reject tab-based indentation
if has_tab_indent?(line.raw) do
{:error,
%DecodeError{
line: line.line_number,
reason: :indentation_error,
message: "Tab indentation is not allowed at line #{line.line_number}"
}}
else
Validation.validate_indent(line.indent, indent_size, line.line_number)
end
end
defp has_tab_indent?(raw) do
# Tabs only matter if they appear before non-whitespace content as indent
case Regex.run(~r/^(\s*)/, raw) do
[_, leading] -> String.contains?(leading, "\t")
_ -> false
end
end
# ---------------------------------------------------------------------------
# Escape sequence check — scan for \X sequences in quoted strings
# ---------------------------------------------------------------------------
defp check_escape_sequences(line) do
check_content_escapes(line.content, line.line_number)
end
defp check_content_escapes(content, line_number) do
# Find all quoted strings in the content and validate each
do_scan_escapes(content, line_number, false)
end
# Walk through content; when inside a quote, validate escape sequences
defp do_scan_escapes("", line_number, true) do
{:error,
%DecodeError{
line: line_number,
reason: :unterminated_string,
message: "unterminated quoted string at line #{line_number}"
}}
end
defp do_scan_escapes("", _line_number, false), do: :ok
defp do_scan_escapes(<<"\"", rest::binary>>, line_number, in_quote) do
do_scan_escapes(rest, line_number, not in_quote)
end
defp do_scan_escapes(<<"\\", rest::binary>>, line_number, true) do
case rest do
"\\" <> tail -> do_scan_escapes(tail, line_number, true)
"\"" <> tail -> do_scan_escapes(tail, line_number, true)
"n" <> tail -> do_scan_escapes(tail, line_number, true)
"r" <> tail -> do_scan_escapes(tail, line_number, true)
"t" <> tail -> do_scan_escapes(tail, line_number, true)
other ->
bad_char = if byte_size(other) > 0, do: String.first(other), else: "EOF"
{:error,
%DecodeError{
line: line_number,
reason: :invalid_escape,
message:
"Invalid escape sequence \\#{bad_char} at line #{line_number}"
}}
end
end
defp do_scan_escapes(<<_::utf8, rest::binary>>, line_number, in_quote) do
do_scan_escapes(rest, line_number, in_quote)
end
end