Current section
11 Versions
Jump to
Current section
11 Versions
Compare versions
4
files changed
+63
additions
-22
deletions
| @@ -1,5 +1,10 @@ | |
| 1 1 | # Changelog |
| 2 2 | |
| 3 | + ## v0.3.2 (2026-07-18) |
| 4 | + |
| 5 | + ### Bug fixes |
| 6 | + - [CanonicalTailwind] Keep `~w`/`~W` sigils in `class` attributes as word lists: each class is normalized in place, never sorted or collapsed against the others. Previously `~w(w-100 w-140 w-60)` was collapsed to a single class, silently dropping runtime elements on format. ([#8](https://github.com/aptinio/canonical_tailwind/issues/8)) |
| 7 | + |
| 3 8 | ## v0.3.1 (2026-07-04) |
| 4 9 | |
| 5 10 | ### Bug fixes |
| @@ -2,7 +2,7 @@ | |
| 2 2 | [{<<"Changelog">>,<<"https://hexdocs.pm/canonical_tailwind/changelog.html">>}, |
| 3 3 | {<<"GitHub">>,<<"https://github.com/aptinio/canonical_tailwind">>}]}. |
| 4 4 | {<<"name">>,<<"canonical_tailwind">>}. |
| 5 | - {<<"version">>,<<"0.3.1">>}. |
| 5 | + {<<"version">>,<<"0.3.2">>}. |
| 6 6 | {<<"description">>, |
| 7 7 | <<"Canonicalizes Tailwind CSS utility classes in HEEx templates and Elixir code">>}. |
| 8 8 | {<<"elixir">>,<<"~> 1.18">>}. |
| @@ -70,27 +70,21 @@ defmodule CanonicalTailwind do | |
| 70 70 | {sigil, meta, [{:<<>>, bin_meta, [canonicalized]}, mods]} |
| 71 71 | end |
| 72 72 | |
| 73 | - defp canonicalize_ast({:<<>>, meta, segments}, opts) do |
| 73 | + defp canonicalize_ast({sigil, meta, [{:<<>>, bin_meta, segments}, mods]}, opts) when sigil in [:sigil_w, :sigil_W] do |
| 74 | + # A word-list sigil desugars to a list of independent classes: normalize each |
| 75 | + # word in place, but never sort or collapse them against each other. Modifiers |
| 76 | + # (~w()a, ~w()c) are canonicalized the same way; in a class attribute the words |
| 77 | + # are class data regardless of the produced element type. |
| 74 78 | segments = |
| 75 | - Enum.map(segments, fn |
| 76 | - seg when is_binary(seg) -> seg |
| 77 | - interp -> canonicalize_ast(interp, opts) |
| 78 | - end) |
| 79 | - |
| 80 | - canonicalized = |
| 81 79 | segments |
| 82 | - |> Enum.with_index() |
| 83 | - |> Enum.map(fn |
| 84 | - {binary, i} when is_binary(binary) -> |
| 85 | - prev_interp? = i > 0 and not is_binary(Enum.at(segments, i - 1)) |
| 86 | - next_interp? = i < length(segments) - 1 and not is_binary(Enum.at(segments, i + 1)) |
| 87 | - canonicalize_segment(binary, prev_interp?, next_interp?, opts) |
| 80 | + |> canonicalize_segments(opts, :word_list) |
| 81 | + |> preserve_heredoc_newline_segments(meta[:delimiter]) |
| 88 82 | |
| 89 | - {interp, _i} -> |
| 90 | - interp |
| 91 | - end) |
| 83 | + {sigil, meta, [{:<<>>, bin_meta, segments}, mods]} |
| 84 | + end |
| 92 85 | |
| 93 | - {:<<>>, meta, canonicalized} |
| 86 | + defp canonicalize_ast({:<<>>, meta, segments}, opts) do |
| 87 | + {:<<>>, meta, canonicalize_segments(segments, opts, :class_string)} |
| 94 88 | end |
| 95 89 | |
| 96 90 | defp canonicalize_ast({left, right}, opts) do |
| @@ -113,14 +107,52 @@ defmodule CanonicalTailwind do | |
| 113 107 | |
| 114 108 | defp preserve_heredoc_newline(value, _delim), do: value |
| 115 109 | |
| 116 | - defp canonicalize_segment(binary, prev_interp?, next_interp?, opts) do |
| 110 | + defp preserve_heredoc_newline_segments(segments, delim) when delim in ["\"\"\"", "'''"] do |
| 111 | + # A heredoc body always ends in a newline before the closing delimiter, so its |
| 112 | + # last segment is a binary; restore the newline the flattening replaced. |
| 113 | + case List.last(segments) do |
| 114 | + last when is_binary(last) -> |
| 115 | + List.replace_at(segments, -1, String.trim_trailing(last) <> "\n") |
| 116 | + |
| 117 | + _ -> |
| 118 | + segments |
| 119 | + end |
| 120 | + end |
| 121 | + |
| 122 | + defp preserve_heredoc_newline_segments(segments, _delim), do: segments |
| 123 | + |
| 124 | + defp canonicalize_segments(segments, opts, mode) do |
| 125 | + # In :word_list mode interpolations are opaque: a runtime value spliced into |
| 126 | + # the list could be whitespace-split into independent classes, so rewriting a |
| 127 | + # nested class string as a group would risk the same collapse as a bare list. |
| 128 | + segments = |
| 129 | + Enum.map(segments, fn |
| 130 | + seg when is_binary(seg) -> seg |
| 131 | + interp when mode == :word_list -> interp |
| 132 | + interp -> canonicalize_ast(interp, opts) |
| 133 | + end) |
| 134 | + |
| 135 | + segments |
| 136 | + |> Enum.with_index() |
| 137 | + |> Enum.map(fn |
| 138 | + {binary, i} when is_binary(binary) -> |
| 139 | + prev_interp? = i > 0 and not is_binary(Enum.at(segments, i - 1)) |
| 140 | + next_interp? = i < length(segments) - 1 and not is_binary(Enum.at(segments, i + 1)) |
| 141 | + canonicalize_segment(binary, prev_interp?, next_interp?, opts, mode) |
| 142 | + |
| 143 | + {interp, _i} -> |
| 144 | + interp |
| 145 | + end) |
| 146 | + end |
| 147 | + |
| 148 | + defp canonicalize_segment(binary, prev_interp?, next_interp?, opts, mode) do |
| 117 149 | words = String.split(binary) |
| 118 150 | |
| 119 151 | if words == [] do |
| 120 152 | binary |
| 121 153 | else |
| 122 154 | {prefix, words, suffix} = split_boundary_words(binary, words, prev_interp?, next_interp?) |
| 123 | - canonicalized = canonicalize(Enum.join(words, " "), opts) |
| 155 | + canonicalized = canonicalize_words(words, mode, opts) |
| 124 156 | leading = if String.match?(binary, ~r/^\s/), do: " ", else: "" |
| 125 157 | trailing = if String.match?(binary, ~r/\s$/), do: " ", else: "" |
| 126 158 | |
| @@ -149,6 +181,10 @@ defmodule CanonicalTailwind do | |
| 149 181 | {prefix, words, suffix} |
| 150 182 | end |
| 151 183 | |
| 184 | + defp canonicalize_words(words, :class_string, opts), do: canonicalize(Enum.join(words, " "), opts) |
| 185 | + |
| 186 | + defp canonicalize_words(words, :word_list, opts), do: Enum.map_join(words, " ", &canonicalize(&1, opts)) |
| 187 | + |
| 152 188 | defp canonicalize(class_string, opts) do |
| 153 189 | if String.trim(class_string) == "" do |
| 154 190 | class_string |
| @@ -1,7 +1,7 @@ | |
| 1 1 | defmodule CanonicalTailwind.MixProject do |
| 2 2 | use Mix.Project |
| 3 3 | |
| 4 | - @version "0.3.1" |
| 4 | + @version "0.3.2" |
| 5 5 | @source_url "https://github.com/aptinio/canonical_tailwind" |
| 6 6 | |
| 7 7 | def application do |
| @@ -37,7 +37,7 @@ defmodule CanonicalTailwind.MixProject do | |
| 37 37 | {:tailwind, "~> 0.3", optional: true}, |
| 38 38 | {:ex_doc, "~> 0.40.3", only: :dev, runtime: false}, |
| 39 39 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, |
| 40 | - {:def_layout, "~> 0.1.0", only: [:dev, :test], runtime: false}, |
| 40 | + {:def_layout, "~> 0.1.1", only: [:dev, :test], runtime: false}, |
| 41 41 | {:quokka, "~> 2.13", only: [:dev, :test], runtime: false} |
| 42 42 | ] |
| 43 43 | end |