Packages

An Elixir client for the Nu HTML Checker (validator.w3.org/nu). Offers validating HTML, CSS, and SVG documents.

Current section

Files

Jump to
vnu lib vnu formatter.ex
Raw

lib/vnu/formatter.ex

defmodule Vnu.Formatter do
@moduledoc false
alias Vnu.Message
import IO.ANSI, only: [red: 0, yellow: 0, blue: 0, cyan: 0, green: 0, reset: 0, reverse: 0]
@doc false
@spec format_messages(list(Message.t())) :: list(String.t())
def format_messages(messages, file_path \\ nil) do
messages
|> sort()
|> Enum.map(fn message ->
type = type(message)
color = color(message)
location = format_location(message)
extract = format_extract(message)
header = "#{with_color("#{reverse()} #{type} ", color)} #{message.message}"
left_padding = if file_path, do: with_color("┃ ", color), else: ""
extract_with_location = extract_with_location(extract, location, left_padding, file_path)
[header, extract_with_location]
|> Enum.filter(& &1)
|> Enum.join("\n")
end)
end
@doc false
def sort(messages) do
Enum.sort(messages, fn a, b ->
al = a.first_line || 0
bl = b.first_line || 0
ac = a.first_column || 0
bc = b.first_column || 0
if al == bl do
ac <= bc
else
al <= bl
end
end)
end
@doc false
def format_counts(
%{error_count: error_count, warning_count: warning_count, info_count: info_count},
opts \\ []
) do
exclude_zeros? = Keyword.get(opts, :exclude_zeros, false)
exclude_warnings? = Keyword.get(opts, :exclude_warnings, false)
exclude_infos? = Keyword.get(opts, :exclude_infos, false)
with_colors? = Keyword.get(opts, :with_colors, true)
error_count_phrase =
phrase_maybe_with_color(
"error",
"errors",
error_count,
exclude_zeros?,
error_color(),
with_colors?
)
warning_count_phrase =
phrase_maybe_with_color(
"warning",
"warnings",
warning_count,
exclude_zeros?,
warning_color(),
with_colors?
)
info_count_phrase =
phrase_maybe_with_color(
"info",
"infos",
info_count,
exclude_zeros?,
info_color(),
with_colors?
)
phrases = []
phrases = if exclude_infos?, do: phrases, else: [info_count_phrase | phrases]
phrases = if exclude_warnings?, do: phrases, else: [warning_count_phrase | phrases]
phrases = [error_count_phrase | phrases]
phrases = Enum.filter(phrases, & &1)
case phrases do
[a, b, c] -> "#{a}, #{b}, and #{c}"
[a, b] -> "#{a} and #{b}"
[a] -> "#{a}"
[] -> ""
end
end
defp phrase_maybe_with_color(singular, plural, count, exclude_zeros?, color, with_color?) do
phrase =
case count do
0 -> if exclude_zeros?, do: nil, else: "#{count} #{plural}"
1 -> "#{count} #{singular}"
_ -> "#{count} #{plural}"
end
if phrase && with_color? do
with_color(phrase, color)
else
phrase
end
end
@doc false
def with_color(string, color) do
"#{color}#{string}#{reset()}"
end
@doc false
def info_color() do
blue()
end
@doc false
def warning_color() do
yellow()
end
@doc false
def error_color() do
red()
end
@doc false
def success_color() do
green()
end
@doc false
def label_color() do
cyan()
end
defp type(%Message{} = message) do
case message do
%Message{type: :error} -> "Error"
%Message{type: :info, sub_type: :warning} -> "Warning"
%Message{type: :info, sub_type: nil} -> "Info"
end
end
defp color(%Message{} = message) do
case message do
%Message{type: :error} -> error_color()
%Message{type: :info, sub_type: :warning} -> warning_color()
%Message{type: :info, sub_type: nil} -> info_color()
end
end
defp format_location(%Message{} = message) do
case message do
%Message{first_line: nil, last_line: nil, first_column: nil, last_column: nil} ->
nil
%Message{first_line: l, last_line: l, first_column: nil, last_column: nil} ->
"L#{l}"
%Message{first_line: l1, last_line: l2, first_column: nil, last_column: nil} ->
"L#{l1}-#{l2}"
%Message{first_line: l, last_line: l, first_column: col1, last_column: col2} ->
"L#{l}:#{col1}-#{col2}"
%Message{first_line: l1, last_line: l2, first_column: col1, last_column: col2} ->
"L#{l1}:#{col1}-L#{l2}:#{col2}"
end
end
defp format_extract(%Message{} = message) do
extract =
if message.extract do
String.replace(message.extract, "\n", "↩")
end
hilite =
if extract && message.hilite_start && message.hilite_length do
String.duplicate(" ", message.hilite_start) <>
label_color() <>
String.duplicate("^", message.hilite_length) <>
reset()
end
if extract do
if hilite, do: [extract, hilite], else: [extract]
end
end
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
defp extract_with_location(extract, location, left_padding, path) do
if path do
location = location_with_path(location, path)
case extract do
[extract, hilite] ->
"#{left_padding}#{with_color(location, label_color())}\n" <>
"#{left_padding}#{extract}\n" <>
"#{left_padding}#{hilite}"
[extract] ->
"#{left_padding}#{with_color(location, label_color())}\n" <>
"#{left_padding}#{extract}"
nil ->
"#{left_padding}#{with_color(location, label_color())}"
end
else
location = location_without_path(location)
case extract do
[extract, hilite] ->
"#{left_padding}#{if location, do: with_color(location, label_color())}#{extract}\n" <>
"#{left_padding}#{if location, do: String.duplicate(" ", String.length(location))}#{hilite}"
[extract] ->
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
"#{left_padding}#{if location, do: with_color(location, label_color())}#{extract}"
nil ->
nil
end
end
end
defp location_with_path(location, path) do
location =
if location do
location
|> String.replace(~r/L/, "")
|> String.replace(~r/-(.)*/, "")
end
if location do
path <> ":" <> location
else
path
end
end
defp location_without_path(location) do
if location, do: "[#{location}] "
end
end