diff --git a/README.md b/README.md index 543f1df..80962d5 100644 --- a/README.md +++ b/README.md @@ -27,15 +27,12 @@ end ## Setup -`TailwindFormatter` is most likely to be used alongside `Phoenix.LiveView.HTMLFormatter`, -so it should be installed in a way that allows the HTML formatter to -run after the Tailwind formatter. - -Update your `.formatter.exs` to include `TailwindFormatter`. +Add `TailwindFormatter` as a class attribute formatter of `Phoenix.LiveView.HTMLFormatter` in `.formatter.exs`. ```elixir [ - plugins: [TailwindFormatter, Phoenix.LiveView.HTMLFormatter], + plugins: [Phoenix.LiveView.HTMLFormatter], + attribute_formatters: %{class: TailwindFormatter}, inputs: [ "*.{heex,ex,exs}", "priv/*/seeds.exs", @@ -54,8 +51,8 @@ let { extract } = require("../deps/tailwind_formatter/assets/js") extract(module.exports, "../_build") ``` -This will write a `classes.txt` and `variants.txt` to your `_build/` directory. -The second argument within `extract` may vary depending on the value of `cd:` in your `:tailwind` config. +This will write a `classes.txt` and `variants.txt` to your `_build/` directory. +The second argument within `extract` may vary depending on the value of `cd:` in your `:tailwind` config. The above will work with this configuration, for example: diff --git a/lib/tailwind_formatter.ex b/lib/tailwind_formatter.ex index 9ac3829..e86306b 100644 --- a/lib/tailwind_formatter.ex +++ b/lib/tailwind_formatter.ex @@ -5,48 +5,51 @@ defmodule TailwindFormatter do |> String.split("") |> Enum.fetch!(1) - alias TailwindFormatter.{Order, HEExTokenizer} - - @behaviour Mix.Tasks.Format + alias TailwindFormatter.Order @placeholder "💧" def features(_opts) do - [sigils: [:H], extensions: [".heex"]] + raise """ + TailwindFormatter is now an attribute formatter for Phoenix.LiveView.HTMLFormatter. + + In your .formatter.exs file, make sure to: + 1. remove TailwindFormatter from the :plugins list + 2. add `attribute_formatters: %{class: TailwindFormatter}` + + # .formatter.exs + [ + ..., + plugins: [Phoenix.LiveView.HTMLFormatter], + attribute_formatters: %{class: TailwindFormatter}, + ] + """ end - def format(contents, _opts) do - contents - |> HEExTokenizer.tokenize() - |> Enum.reduce([contents], fn - {elt, _name, attrs, _meta}, contents - when elt in [:tag, :local_component, :remote_component] -> - Enum.reduce(attrs, contents, fn - {"class", class_attr, _meta}, [remainder | acc] -> - [attr, remainder] = String.split(remainder, old_classes(class_attr), parts: 2) - [remainder, sort_classes(class_attr), attr | acc] - - _, contents -> - contents - end) - - _, contents -> - contents - end) - |> Enum.reverse() - |> Enum.join() - end - - defp old_classes({_type, classes, _meta}), do: classes - defp sort_classes({:string, classes, _meta}), do: sort(classes) - - defp sort_classes({:expr, expr_class, _meta}) do - expr_class - |> Code.string_to_quoted!(literal_encoder: &{:ok, {:__block__, &2, [&1]}}) - |> sort_expr() - |> Code.quoted_to_algebra() - |> Inspect.Algebra.format(:infinity) - |> IO.iodata_to_binary() + def render_attribute({"class", class, meta}, _opts) do + { + "class", + case class do + {:string, string, meta} -> + {:string, sort(string), meta} + + {:expr, expr, meta} -> + { + :expr, + expr + |> Code.string_to_quoted!(literal_encoder: &{:ok, {:__block__, &2, [&1]}}) + |> sort_expr() + |> Code.quoted_to_algebra() + |> Inspect.Algebra.format(:infinity) + |> IO.iodata_to_binary(), + meta + } + + nil -> + nil + end, + meta + } end defp sort_expr({:<<>>, meta, children}), do: {:<<>>, meta, handle_interpolation(children)} diff --git a/lib/tailwind_formatter/heex_tokenizer.ex b/lib/tailwind_formatter/heex_tokenizer.ex deleted file mode 100644 index 23a0b34..0000000 --- a/lib/tailwind_formatter/heex_tokenizer.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule TailwindFormatter.HEExTokenizer do - @moduledoc false - alias TailwindFormatter.PhoenixLiveViewTokenizer, as: Tokenizer - - # Taken directly from Phoenix.LiveView.HTMLFormatter - # https://github.com/phoenixframework/phoenix_live_view/blob/v0.20.1/lib/phoenix_live_view/html_formatter.ex#L288-L335 - @eex_expr [:start_expr, :expr, :end_expr, :middle_expr] - def tokenize(source) do - {:ok, eex_nodes} = EEx.tokenize(source) - {tokens, cont} = Enum.reduce(eex_nodes, {[], :text}, &do_tokenize(&1, &2, source)) - Tokenizer.finalize(tokens, "nofile", cont, source) - end - - defp do_tokenize({:text, text, meta}, {tokens, cont}, source) do - text = List.to_string(text) - meta = [line: meta.line, column: meta.column] - state = Tokenizer.init(0, "nofile", source, TailwindFormatter.PhoenixLiveViewHTMLEngine) - Tokenizer.tokenize(text, meta, tokens, cont, state) - end - - defp do_tokenize({:comment, text, meta}, {tokens, cont}, _contents) do - {[{:eex_comment, List.to_string(text), meta} | tokens], cont} - end - - defp do_tokenize({type, opt, expr, %{column: column, line: line}}, {tokens, cont}, _contents) - when type in @eex_expr do - meta = %{opt: opt, line: line, column: column} - {[{:eex, type, expr |> List.to_string() |> String.trim(), meta} | tokens], cont} - end - - defp do_tokenize(_node, acc, _contents) do - acc - end -end diff --git a/lib/tailwind_formatter/phoenix_live_view_html_engine.ex b/lib/tailwind_formatter/phoenix_live_view_html_engine.ex deleted file mode 100644 index bd3f5cf..0000000 --- a/lib/tailwind_formatter/phoenix_live_view_html_engine.ex +++ /dev/null @@ -1,21 +0,0 @@ -# Taken directly from Phoenix.LiveView.HTMLEngine -# https://github.com/phoenixframework/phoenix_live_view/blob/v0.20.1/lib/phoenix_live_view/html_engine.ex#L41-L57 -defmodule TailwindFormatter.PhoenixLiveViewHTMLEngine do - @moduledoc false - def classify_type(":" <> name), do: {:slot, name} - def classify_type(":inner_block"), do: {:error, "the slot name :inner_block is reserved"} - - def classify_type(<> = name) when first in ?A..?Z, - do: {:remote_component, name} - - def classify_type("." <> name), - do: {:local_component, name} - - def classify_type(name), do: {:tag, name} - - for void <- ~w(area base br col hr img input link meta param command keygen source) do - def void?(unquote(void)), do: true - end - - def void?(_), do: false -end diff --git a/lib/tailwind_formatter/phoenix_live_view_tokenizer.ex b/lib/tailwind_formatter/phoenix_live_view_tokenizer.ex deleted file mode 100644 index 6e8b4f7..0000000 --- a/lib/tailwind_formatter/phoenix_live_view_tokenizer.ex +++ /dev/null @@ -1,719 +0,0 @@ -# Taken directly from Phoenix.LiveView.Tokenizer -# https://github.com/phoenixframework/phoenix_live_view/blob/v0.20.1/lib/phoenix_live_view/tokenizer.ex -defmodule TailwindFormatter.PhoenixLiveViewTokenizer do - @moduledoc false - @space_chars ~c"\s\t\f" - @quote_chars ~c"\"'" - @stop_chars ~c">/=\r\n" ++ @quote_chars ++ @space_chars - - defmodule ParseError do - @moduledoc false - defexception [:file, :line, :column, :description] - - @impl true - def message(exception) do - location = - exception.file - |> Path.relative_to_cwd() - |> Exception.format_file_line_column(exception.line, exception.column) - - "#{location} #{exception.description}" - end - - def code_snippet(source, meta, indentation \\ 0) do - line_start = max(meta.line - 3, 1) - line_end = meta.line - digits = line_end |> Integer.to_string() |> byte_size() - number_padding = String.duplicate(" ", digits) - indentation = String.duplicate(" ", indentation) - - source - |> String.split(["\r\n", "\n"]) - |> Enum.slice((line_start - 1)..(line_end - 1)) - |> Enum.map_reduce(line_start, fn - expr, line_number when line_number == line_end -> - arrow = String.duplicate(" ", meta.column - 1) <> "^" - acc = "#{line_number} | #{indentation}#{expr}\n #{number_padding}| #{arrow}" - {acc, line_number + 1} - - expr, line_number -> - line_number_padding = String.pad_leading("#{line_number}", digits) - {"#{line_number_padding} | #{indentation}#{expr}", line_number + 1} - end) - |> case do - {[], _} -> - "" - - {snippet, _} -> - Enum.join(["\n #{number_padding}|" | snippet], "\n") - end - end - end - - def finalize(_tokens, file, {:comment, line, column}, source) do - message = "expected closing `-->` for comment" - meta = %{line: line, column: column} - raise_syntax_error!(message, meta, %{source: source, file: file, indentation: 0}) - end - - def finalize(tokens, _file, _cont, _source) do - tokens - |> strip_text_token_fully() - |> Enum.reverse() - |> strip_text_token_fully() - end - - @doc """ - Initiate the Tokenizer state. - - ### Params - - * `indentation` - An integer that indicates the current indentation. - * `file` - Can be either a file or a string "nofile". - * `source` - The contents of the file as binary used to be tokenized. - * `tag_handler` - Tag handler to classify the tags. See `Phoenix.LiveView.TagEngine` - behaviour. - """ - def init(indentation, file, source, tag_handler) do - %{ - file: file, - column_offset: indentation + 1, - braces: [], - context: [], - source: source, - indentation: indentation, - tag_handler: tag_handler - } - end - - @doc """ - Tokenize the given text according to the given params. - - ### Params - - * `text` - The content to be tokenized. - * `meta` - A keyword list with `:line` and `:column`. Both must be integers. - * `tokens` - A list of tokens. - * `cont` - An atom that is `:text`, `:style`, or `:script`, or a tuple - {:comment, line, column}. - * `state` - The tokenizer state that must be initiated by `Tokenizer.init/4` - - ### Examples - - iex> alias Phoenix.LiveView.Tokenizer - - iex> state = - Tokenizer.init(indent, file, [text: "
"], HTMLEngine) - - iex> Tokenizer.tokenize(state) - {[ - {:close, :tag, "section", %{column: 16, line: 1}}, - {:tag, "div", [], %{column: 10, line: 1, closing: :self}}, - {:tag, "section", [], %{column: 1, line: 1}} - ], :text} - """ - def tokenize(text, meta, tokens, cont, state) do - line = Keyword.get(meta, :line, 1) - column = Keyword.get(meta, :column, 1) - - case cont do - :text -> handle_text(text, line, column, [], tokens, state) - :style -> handle_style(text, line, column, [], tokens, state) - :script -> handle_script(text, line, column, [], tokens, state) - {:comment, _, _} -> handle_comment(text, line, column, [], tokens, state) - end - end - - ## handle_text - - defp handle_text("\r\n" <> rest, line, _column, buffer, acc, state) do - handle_text(rest, line + 1, state.column_offset, ["\r\n" | buffer], acc, state) - end - - defp handle_text("\n" <> rest, line, _column, buffer, acc, state) do - handle_text(rest, line + 1, state.column_offset, ["\n" | buffer], acc, state) - end - - defp handle_text(" rest, line, column, buffer, acc, state) do - handle_doctype(rest, line, column + 9, [" rest, line, column, buffer, acc, state) do - handle_doctype(rest, line, column + 9, [" rest, line, column, buffer, acc, state) do - state = update_in(state.context, &[:comment_start | &1]) - handle_comment(rest, line, column + 4, ["" <> rest, line, column, buffer, _state) do - {:text, rest, line, column + 3, ["-->" | buffer]} - end - - defp handle_comment(<>, line, column, buffer, state) do - handle_comment(rest, line, column + 1, [char_or_bin(c) | buffer], state) - end - - defp handle_comment(<<>>, line, column, buffer, _state) do - {:ok, line, column, buffer} - end - - ## handle_tag_open - - defp handle_tag_open(text, line, column, acc, state) do - case handle_tag_name(text, column, []) do - {:ok, name, new_column, rest} -> - meta = %{line: line, column: column - 1, inner_location: nil, tag_name: name} - - case state.tag_handler.classify_type(name) do - {:error, message} -> - raise_syntax_error!(message, meta, state) - - {type, name} -> - acc = [{type, name, [], meta} | acc] - handle_maybe_tag_open_end(rest, line, new_column, acc, state) - end - - :error -> - message = - "expected tag name after <. If you meant to use < as part of a text, use < instead" - - meta = %{line: line, column: column} - - raise_syntax_error!(message, meta, state) - end - end - - ## handle_tag_close - - defp handle_tag_close(text, line, column, acc, state) do - case handle_tag_name(text, column, []) do - {:ok, name, new_column, ">" <> rest} -> - meta = %{ - line: line, - column: column - 2, - inner_location: {line, column - 2}, - tag_name: name - } - - case state.tag_handler.classify_type(name) do - {:error, message} -> - raise_syntax_error!(message, meta, state) - - {type, name} -> - acc = [{:close, type, name, meta} | acc] - handle_text(rest, line, new_column + 1, [], acc, state) - end - - {:ok, _, new_column, _} -> - message = "expected closing `>`" - meta = %{line: line, column: new_column} - raise_syntax_error!(message, meta, state) - - :error -> - message = "expected tag name after > = text, column, buffer) - when c in @stop_chars do - done_tag_name(text, column, buffer) - end - - defp handle_tag_name(<>, column, buffer) do - handle_tag_name(rest, column + 1, [char_or_bin(c) | buffer]) - end - - defp handle_tag_name(<<>>, column, buffer) do - done_tag_name(<<>>, column, buffer) - end - - defp done_tag_name(_text, _column, []) do - :error - end - - defp done_tag_name(text, column, buffer) do - {:ok, buffer_to_string(buffer), column, text} - end - - ## handle_maybe_tag_open_end - - defp handle_maybe_tag_open_end("\r\n" <> rest, line, _column, acc, state) do - handle_maybe_tag_open_end(rest, line + 1, state.column_offset, acc, state) - end - - defp handle_maybe_tag_open_end("\n" <> rest, line, _column, acc, state) do - handle_maybe_tag_open_end(rest, line + 1, state.column_offset, acc, state) - end - - defp handle_maybe_tag_open_end(<>, line, column, acc, state) - when c in @space_chars do - handle_maybe_tag_open_end(rest, line, column + 1, acc, state) - end - - defp handle_maybe_tag_open_end("/>" <> rest, line, column, acc, state) do - acc = normalize_tag(acc, line, column + 2, true, state) - handle_text(rest, line, column + 2, [], acc, state) - end - - defp handle_maybe_tag_open_end(">" <> rest, line, column, acc, state) do - case normalize_tag(acc, line, column + 1, false, state) do - [{:tag, "script", _, _} | _] = acc -> - handle_script(rest, line, column + 1, [], acc, state) - - [{:tag, "style", _, _} | _] = acc -> - handle_style(rest, line, column + 1, [], acc, state) - - acc -> - handle_text(rest, line, column + 1, [], acc, state) - end - end - - defp handle_maybe_tag_open_end("{" <> rest, line, column, acc, state) do - handle_root_attribute(rest, line, column + 1, acc, state) - end - - defp handle_maybe_tag_open_end(<<>>, line, column, _acc, state) do - message = ~S""" - expected closing `>` or `/>` - - Make sure the tag is properly closed. This may happen if there - is an EEx interpolation inside a tag, which is not supported. - For instance, instead of - -
Content
- - do - -
Content
- - If @id is nil or false, then no attribute is sent at all. - - Inside {...} you can place any Elixir expression. If you want - to interpolate in the middle of an attribute value, instead of - - Text - - you can pass an Elixir string with interpolation: - - Text - """ - - raise ParseError, file: state.file, line: line, column: column, description: message - end - - defp handle_maybe_tag_open_end(text, line, column, acc, state) do - handle_attribute(text, line, column, acc, state) - end - - ## handle_attribute - - defp handle_attribute(text, line, column, acc, state) do - case handle_attr_name(text, column, []) do - {:ok, name, new_column, rest} -> - acc = put_attr(acc, name, %{line: line, column: column}) - handle_maybe_attr_value(rest, line, new_column, acc, state) - - {:error, message, column} -> - meta = %{line: line, column: column} - raise_syntax_error!(message, meta, state) - end - end - - ## handle_root_attribute - - defp handle_root_attribute(text, line, column, acc, state) do - case handle_interpolation(text, line, column, [], state) do - {:ok, value, new_line, new_column, rest, state} -> - meta = %{line: line, column: column} - acc = put_attr(acc, :root, meta, {:expr, value, meta}) - handle_maybe_tag_open_end(rest, new_line, new_column, acc, state) - - {:error, message} -> - # We do column - 1 to point to the opening { - meta = %{line: line, column: column - 1} - raise_syntax_error!(message, meta, state) - end - end - - ## handle_attr_name - - defp handle_attr_name(<>, column, _buffer) - when c in @quote_chars do - {:error, "invalid character in attribute name: #{<>}", column} - end - - defp handle_attr_name(<>, column, []) - when c in @stop_chars do - {:error, "expected attribute name", column} - end - - defp handle_attr_name(<> = text, column, buffer) - when c in @stop_chars do - {:ok, buffer_to_string(buffer), column, text} - end - - defp handle_attr_name(<>, column, buffer) do - handle_attr_name(rest, column + 1, [char_or_bin(c) | buffer]) - end - - ## handle_maybe_attr_value - - defp handle_maybe_attr_value("\r\n" <> rest, line, _column, acc, state) do - handle_maybe_attr_value(rest, line + 1, state.column_offset, acc, state) - end - - defp handle_maybe_attr_value("\n" <> rest, line, _column, acc, state) do - handle_maybe_attr_value(rest, line + 1, state.column_offset, acc, state) - end - - defp handle_maybe_attr_value(<>, line, column, acc, state) - when c in @space_chars do - handle_maybe_attr_value(rest, line, column + 1, acc, state) - end - - defp handle_maybe_attr_value("=" <> rest, line, column, acc, state) do - handle_attr_value_begin(rest, line, column + 1, acc, state) - end - - defp handle_maybe_attr_value(text, line, column, acc, state) do - handle_maybe_tag_open_end(text, line, column, acc, state) - end - - ## handle_attr_value_begin - - defp handle_attr_value_begin("\r\n" <> rest, line, _column, acc, state) do - handle_attr_value_begin(rest, line + 1, state.column_offset, acc, state) - end - - defp handle_attr_value_begin("\n" <> rest, line, _column, acc, state) do - handle_attr_value_begin(rest, line + 1, state.column_offset, acc, state) - end - - defp handle_attr_value_begin(<>, line, column, acc, state) - when c in @space_chars do - handle_attr_value_begin(rest, line, column + 1, acc, state) - end - - defp handle_attr_value_begin("\"" <> rest, line, column, acc, state) do - handle_attr_value_quote(rest, ?", line, column + 1, [], acc, state) - end - - defp handle_attr_value_begin("'" <> rest, line, column, acc, state) do - handle_attr_value_quote(rest, ?', line, column + 1, [], acc, state) - end - - defp handle_attr_value_begin("{" <> rest, line, column, acc, state) do - handle_attr_value_as_expr(rest, line, column + 1, acc, state) - end - - defp handle_attr_value_begin(_text, line, column, _acc, state) do - message = - "invalid attribute value after `=`. Expected either a value between quotes " <> - "(such as \"value\" or \'value\') or an Elixir expression between curly brackets (such as `{expr}`)" - - meta = %{line: line, column: column} - raise_syntax_error!(message, meta, state) - end - - ## handle_attr_value_quote - - defp handle_attr_value_quote("\r\n" <> rest, delim, line, _column, buffer, acc, state) do - column = state.column_offset - handle_attr_value_quote(rest, delim, line + 1, column, ["\r\n" | buffer], acc, state) - end - - defp handle_attr_value_quote("\n" <> rest, delim, line, _column, buffer, acc, state) do - column = state.column_offset - handle_attr_value_quote(rest, delim, line + 1, column, ["\n" | buffer], acc, state) - end - - defp handle_attr_value_quote(<>, delim, line, column, buffer, acc, state) do - value = buffer_to_string(buffer) - acc = put_attr_value(acc, {:string, value, %{delimiter: delim}}) - handle_maybe_tag_open_end(rest, line, column + 1, acc, state) - end - - defp handle_attr_value_quote(<>, delim, line, column, buffer, acc, state) do - handle_attr_value_quote(rest, delim, line, column + 1, [char_or_bin(c) | buffer], acc, state) - end - - defp handle_attr_value_quote(<<>>, delim, line, column, _buffer, _acc, state) do - message = """ - expected closing `#{<>}` for attribute value - - Make sure the attribute is properly closed. This may also happen if - there is an EEx interpolation inside a tag, which is not supported. - Instead of - -
> -
- - do - -
-
- - Where @some_attributes must be a keyword list or a map. - """ - - meta = %{line: line, column: column} - raise_syntax_error!(message, meta, state) - end - - ## handle_attr_value_as_expr - - defp handle_attr_value_as_expr(text, line, column, acc, %{braces: []} = state) do - case handle_interpolation(text, line, column, [], state) do - {:ok, value, new_line, new_column, rest, state} -> - acc = put_attr_value(acc, {:expr, value, %{line: line, column: column}}) - handle_maybe_tag_open_end(rest, new_line, new_column, acc, state) - - {:error, message} -> - # We do column - 1 to point to the opening { - meta = %{line: line, column: column - 1} - raise_syntax_error!(message, meta, state) - end - end - - ## handle_interpolation - - defp handle_interpolation("\r\n" <> rest, line, _column, buffer, state) do - handle_interpolation(rest, line + 1, state.column_offset, ["\r\n" | buffer], state) - end - - defp handle_interpolation("\n" <> rest, line, _column, buffer, state) do - handle_interpolation(rest, line + 1, state.column_offset, ["\n" | buffer], state) - end - - defp handle_interpolation("}" <> rest, line, column, buffer, %{braces: []} = state) do - value = buffer_to_string(buffer) - {:ok, value, line, column + 1, rest, state} - end - - defp handle_interpolation(~S(\}) <> rest, line, column, buffer, state) do - handle_interpolation(rest, line, column + 2, [~S(\}) | buffer], state) - end - - defp handle_interpolation(~S(\{) <> rest, line, column, buffer, state) do - handle_interpolation(rest, line, column + 2, [~S(\{) | buffer], state) - end - - defp handle_interpolation("}" <> rest, line, column, buffer, state) do - {_pos, state} = pop_brace(state) - handle_interpolation(rest, line, column + 1, ["}" | buffer], state) - end - - defp handle_interpolation("{" <> rest, line, column, buffer, state) do - state = push_brace(state, {line, column}) - handle_interpolation(rest, line, column + 1, ["{" | buffer], state) - end - - defp handle_interpolation(<>, line, column, buffer, state) do - handle_interpolation(rest, line, column + 1, [char_or_bin(c) | buffer], state) - end - - defp handle_interpolation(<<>>, _line, _column, _buffer, _state) do - {:error, "expected closing `}` for expression"} - end - - ## helpers - - @compile {:inline, ok: 2, char_or_bin: 1} - defp ok(acc, cont), do: {acc, cont} - - defp char_or_bin(c) when c <= 127, do: c - defp char_or_bin(c), do: <> - - defp buffer_to_string(buffer) do - IO.iodata_to_binary(Enum.reverse(buffer)) - end - - defp text_to_acc(buffer, acc, line, column, context) - - defp text_to_acc([], acc, _line, _column, _context), - do: acc - - defp text_to_acc(buffer, acc, line, column, context) do - meta = %{line_end: line, column_end: column} - - meta = - if context == [] do - meta - else - Map.put(meta, :context, trim_context(context)) - end - - [{:text, buffer_to_string(buffer), meta} | acc] - end - - defp trim_context([:comment_end, :comment_start | [_ | _] = rest]), do: trim_context(rest) - defp trim_context(rest), do: Enum.reverse(rest) - - defp put_attr([{type, name, attrs, meta} | acc], attr, attr_meta, value \\ nil) do - attrs = [{attr, value, attr_meta} | attrs] - [{type, name, attrs, meta} | acc] - end - - defp put_attr_value([{type, name, [{attr, _value, attr_meta} | attrs], meta} | acc], value) do - attrs = [{attr, value, attr_meta} | attrs] - [{type, name, attrs, meta} | acc] - end - - defp normalize_tag([{type, name, attrs, meta} | acc], line, column, self_close?, state) do - attrs = Enum.reverse(attrs) - meta = %{meta | inner_location: {line, column}} - - meta = - cond do - type == :tag and state.tag_handler.void?(name) -> Map.put(meta, :closing, :void) - self_close? -> Map.put(meta, :closing, :self) - true -> meta - end - - [{type, name, attrs, meta} | acc] - end - - defp push_brace(state, pos) do - %{state | braces: [pos | state.braces]} - end - - defp pop_brace(%{braces: [pos | braces]} = state) do - {pos, %{state | braces: braces}} - end - - defp strip_text_token_fully(tokens) do - with [{:text, text, _} | rest] <- tokens, - "" <- String.trim_leading(text) do - strip_text_token_fully(rest) - else - _ -> tokens - end - end - - defp raise_syntax_error!(message, meta, state) do - raise ParseError, - file: state.file, - line: meta.line, - column: meta.column, - description: message <> ParseError.code_snippet(state.source, meta, state.indentation) - end -end diff --git a/mix.exs b/mix.exs index d341c7b..3867d8c 100644 --- a/mix.exs +++ b/mix.exs @@ -50,6 +50,7 @@ defmodule TailwindFormatter.MixProject do [ {:tailwind, "~> 0.2", only: :dev}, {:benchee, "~> 1.0", only: [:dev], optional: true}, + {:phoenix_live_view, "~> 1.1.0", only: :test}, # docs {:ex_doc, "~> 0.28", only: :dev, runtime: false} ] diff --git a/mix.lock b/mix.lock index 8f29137..41a0af8 100644 --- a/mix.lock +++ b/mix.lock @@ -7,7 +7,18 @@ "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, + "phoenix": {:hex, :phoenix, "1.8.3", "49ac5e485083cb1495a905e47eb554277bdd9c65ccb4fc5100306b350151aa95", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "36169f95cc2e155b78be93d9590acc3f462f1e5438db06e6248613f27c80caec"}, + "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.20", "4f20850ee700b309b21906a0e510af1b916b454b4f810fb8581ada016eb42dfc", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c16abd605a21f778165cb0079946351ef20ef84eb1ef467a862fb9a173b1d27d"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, + "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "tailwind": {:hex, :tailwind, "0.2.1", "83d8eadbe71a8e8f67861fe7f8d51658ecfb258387123afe4d9dc194eddc36b0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "e8a13f6107c95f73e58ed1b4221744e1eb5a093cd1da244432067e19c8c9a277"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, } diff --git a/test/tailwind_formatter_test.exs b/test/tailwind_formatter_test.exs index 4c4c1e1..81cc06d 100644 --- a/test/tailwind_formatter_test.exs +++ b/test/tailwind_formatter_test.exs @@ -2,14 +2,13 @@ defmodule TailwindFormatterTest do use ExUnit.Case doctest TailwindFormatter - alias TailwindFormatter + defp assert_formatter_output(input, expected) do + output = + Phoenix.LiveView.HTMLFormatter.format(input, + attribute_formatters: %{class: TailwindFormatter} + ) - defp assert_formatter_output(input, expected, dot_formatter_opts \\ []) do - first_pass = TailwindFormatter.format(input, dot_formatter_opts) - assert first_pass == expected - - second_pass = TailwindFormatter.format(first_pass, dot_formatter_opts) - assert second_pass == expected + assert output == expected end test "works" do @@ -26,11 +25,13 @@ defmodule TailwindFormatterTest do test "handles and sorts chained variants" do input = """ -
+
+
""" expected = """ -
+
+
""" assert_formatter_output(input, expected) @@ -40,11 +41,13 @@ defmodule TailwindFormatterTest do input = """
+ "> + """ expected = """ -
+
+
""" assert_formatter_output(input, expected) @@ -76,11 +79,13 @@ defmodule TailwindFormatterTest do test "supports multiple inline elixir codes" do input = ~S""" -
+
+
""" expected = ~S""" -
+
+
""" assert_formatter_output(input, expected) @@ -88,13 +93,21 @@ defmodule TailwindFormatterTest do test "regex allows multiple attributes" do input = ~S""" - + + """ expected = ~S""" - + + """ assert_formatter_output(input, expected) @@ -102,13 +115,11 @@ defmodule TailwindFormatterTest do test "dynamic classes" do input = ~S""" - + """ expected = ~S""" - + """ assert_formatter_output(input, expected) @@ -116,13 +127,13 @@ defmodule TailwindFormatterTest do test "dynamic variant classes" do input = ~S""" - + + """ expected = ~S""" - + + """ assert_formatter_output(input, expected) @@ -130,13 +141,21 @@ defmodule TailwindFormatterTest do test "allows comparisons inline elixir" do input = ~S""" - + + """ expected = ~S""" - + + """ assert_formatter_output(input, expected) @@ -144,11 +163,11 @@ defmodule TailwindFormatterTest do test "allows string concatenation" do input = ~S""" -
if @active, do: "bg-white", else: "bg-red"}>
+
if @active, do: "bg-white", else: "bg-red"}>
""" expected = ~S""" -
if @active, do: "bg-white", else: "bg-red"}>
+
if @active, do: "bg-white", else: "bg-red"}>
""" assert_formatter_output(input, expected) @@ -156,13 +175,13 @@ defmodule TailwindFormatterTest do test "handles empty classes" do input = ~S""" - - + + """ expected = ~S""" - - + + """ assert_formatter_output(input, expected) @@ -230,15 +249,21 @@ defmodule TailwindFormatterTest do test "larger css with unknown classes" do input = ~S""" - + href="#" + > + """ expected = ~S""" - + href="#" + > + """ assert_formatter_output(input, expected) @@ -246,15 +271,21 @@ defmodule TailwindFormatterTest do test "larger css with known classes" do input = ~S""" - + href="#" + > + """ expected = ~S""" - + href="#" + > + """ assert_formatter_output(input, expected) @@ -262,15 +293,21 @@ defmodule TailwindFormatterTest do test "placeholding does not change anything outside of class attr" do input = ~S""" - + href="#" + > + """ expected = ~S""" - + href="#" + > + """ assert_formatter_output(input, expected) @@ -291,73 +328,67 @@ defmodule TailwindFormatterTest do test "handles classes with placeholders" do input = ~S""" - ETA/ETD - - <%= for {vessel, eta, etd} <- list do %> -

- <%= vessel.name %> - (<%= convert_date?( - eta, - "Not Specified" - ) %> – <%= convert_date?( - etd, - "Not Specified" - ) %>) -

- <% end %> - + ETA/ETD + + <%= for {vessel, eta, etd} <- list do %> +

+ <%= vessel.name %> + ( + <%= convert_date?(eta, "Not Specified") %> + – + <%= convert_date?(etd, "Not Specified") %> + ) +

+ <% end %> + - Deliverables - - <%= for {fuel, vessel_fuels} <- list, - vessel_fuel <- vessel_fuels do %> -

- <%= vessel_fuel.quantity %> - MT of <%= fuel.name %> - to - - <%= vessel_fuel.vessel.name %> (<%= vessel_fuel.vessel.imo %>) - -

- <% end %> - + Deliverables + + <%= for {fuel, vessel_fuels} <- list, vessel_fuel <- vessel_fuels do %> +

+ <%= vessel_fuel.quantity %> + MT of {fuel.name} + to + + {vessel_fuel.vessel.name} ({vessel_fuel.vessel.imo}) + +

+ <% end %> + """ expected = ~S""" - ETA/ETD - - <%= for {vessel, eta, etd} <- list do %> -

- <%= vessel.name %> - (<%= convert_date?( - eta, - "Not Specified" - ) %> – <%= convert_date?( - etd, - "Not Specified" - ) %>) -

- <% end %> - + ETA/ETD + + <%= for {vessel, eta, etd} <- list do %> +

+ {vessel.name} + ( + <%= convert_date?(eta, "Not Specified") %> + – + <%= convert_date?(etd, "Not Specified") %> + ) +

+ <% end %> + - Deliverables - - <%= for {fuel, vessel_fuels} <- list, - vessel_fuel <- vessel_fuels do %> -

- <%= vessel_fuel.quantity %> - MT of <%= fuel.name %> - to - - <%= vessel_fuel.vessel.name %> (<%= vessel_fuel.vessel.imo %>) - -

- <% end %> - + Deliverables + + <%= for {fuel, vessel_fuels} <- list, vessel_fuel <- vessel_fuels do %> +

+ {vessel_fuel.quantity} + MT of {fuel.name} + to + + {vessel_fuel.vessel.name} ({vessel_fuel.vessel.imo}) + +

+ <% end %> + """ @@ -372,7 +403,7 @@ defmodule TailwindFormatterTest do <:hover_content> - <%= text %> + {text} @@ -385,7 +416,7 @@ defmodule TailwindFormatterTest do <:hover_content> - <%= text %> + {text} @@ -418,11 +449,13 @@ defmodule TailwindFormatterTest do test "works with multiple concatenations in one line" do input = ~S""" -
"px-3 py-3 rounded-lg " <> "px-3 py-5 rounded-lg " <> "text-sm" <> "tomato"}>
+
"px-3 py-3 rounded-lg " <> "px-3 py-5 rounded-lg " <> "text-sm" <> "tomato"}> +
""" expected = ~S""" -
"rounded-lg px-3 py-3 " <> "rounded-lg px-3 py-5 " <> "text-sm" <> "tomato"}>
+
"rounded-lg px-3 py-3 " <> "rounded-lg px-3 py-5 " <> "text-sm" <> "tomato"}> +
""" assert_formatter_output(input, expected) @@ -442,15 +475,21 @@ defmodule TailwindFormatterTest do test "custom variant does not flip" do input = ~S""" - + + """ expected = ~S""" - + + """ assert_formatter_output(input, expected) @@ -478,11 +517,19 @@ defmodule TailwindFormatterTest do assert_formatter_output(input, expected) input = """ -
+
+
""" expected = """ -
+
+
""" assert_formatter_output(input, expected) @@ -490,11 +537,19 @@ defmodule TailwindFormatterTest do test "handles multiple class attributes" do input = """ -
+
+
""" expected = """ -
+
+
""" assert_formatter_output(input, expected) @@ -527,11 +582,11 @@ defmodule TailwindFormatterTest do # https://tailwindcss.com/docs/adding-custom-styles#arbitrary-properties test "handles arbitrary properties" do input = """ - + """ expected = """ - + """ assert_formatter_output(input, expected) @@ -539,11 +594,11 @@ defmodule TailwindFormatterTest do test "issue #63" do input = """ - + """ expected = """ - + """ assert_formatter_output(input, expected)