Repository: 100phlecs/tailwind_formatter
Pull request #30 (2023-08-12 12:52:01)

handle multiple concats
Fixes Issue #16 

Was tough but it seems to work.
@100phlecs Can you imagine other combinations that will fail with this?

100phlecs: Yeah, as we venture further into elixir expressions it seems the best thing to do is to tap into the HEEx tokenizer.
I've not looked into concatenation in-depth (and I rarely use it). Your test cases look fine. Thanks for the PR

If it's just concatenation then I think it's fine.

Future failure will be other elixir expressions, like the stuff in the CoreComponents of phoenix for example:
https://github.com/phoenixframework/phoenix/blob/e095f23ff62efb2f8c1205b0cc67bfacd80b11ed/installer/templates/phx_web/components/core_components.ex#L115-L119

But that's a separate issue. This solves #16 in the short term. 
In the future we will have do a whole different approach, as I can't imagine doing any more Regex to support all of elixir expressions




Patch:
diff --git a/lib/tailwind_formatter.ex b/lib/tailwind_formatter.ex
index 10f279d..ea3c341 100644
--- a/lib/tailwind_formatter.ex
+++ b/lib/tailwind_formatter.ex
@@ -24,7 +24,7 @@ defmodule TailwindFormatter do
       Regex.replace(Defaults.class_regex(), placeholder_contents, fn class_html_attr ->
         [class_attr, class_val] = String.split(class_html_attr, ~r/[=:]/, parts: 2)
         needs_curlies = String.match?(class_val, ~r/{/)
-        maybe_concatenated = String.match?(class_val, ~r/<>/)
+        is_concatenated = String.match?(class_val, ~r/<>/)
         variable_only = not String.match?(class_val, ~r/"/)
 
         trimmed_classes =
@@ -35,40 +35,82 @@ defmodule TailwindFormatter do
           |> String.trim("\"")
           |> String.trim()
 
-        [trimmed_classes, concatenation] =
-          if maybe_concatenated do
+        [trimmed_classes | concatenations] =
+          if is_concatenated do
             String.split(trimmed_classes, ~r/" <>/)
           else
             [trimmed_classes, ""]
           end
 
-        if variable_only or trimmed_classes == "" or
-             Regex.match?(Defaults.invalid_input_regex(), trimmed_classes) do
+        if variable_only or trimmed_classes == "" or invalid_input(trimmed_classes) do
           class_html_attr
         else
-          sorted_list =
-            trimmed_classes
-            |> String.split()
-            |> sort_variant_chains()
-            |> sort()
-            |> Enum.join(" ")
-            |> then(fn sorted ->
-              if maybe_concatenated do
-                sorted <> " \" <>" <> concatenation
-              else
-                sorted
-              end
-            end)
-
           delimiter = if String.contains?(class_html_attr, "class:"), do: ": ", else: "="
 
-          class_attr <> delimiter <> wrap_classes(sorted_list, needs_curlies)
+          sorting(
+            trimmed_classes,
+            concatenations,
+            class_attr,
+            delimiter,
+            is_concatenated,
+            needs_curlies
+          )
         end
       end)
 
     undo_placeholders(sorted_html, dynamic_classes)
   end
 
+  defp invalid_input(trimmed_classes),
+    do: Regex.match?(Defaults.invalid_input_regex(), trimmed_classes)
+
+  defp sorting(
+         trimmed_classes,
+         [concatenation] = _concatenations,
+         class_attr,
+         delimiter,
+         is_concatenated,
+         needs_curlies
+       ) do
+    sorted_list =
+      trimmed_classes
+      |> sort_classes_string()
+      |> then(fn sorted ->
+        if is_concatenated do
+          sorted <> " \" <>" <> concatenation
+        else
+          sorted
+        end
+      end)
+
+    class_attr <> delimiter <> wrap_classes(sorted_list, needs_curlies)
+  end
+
+  defp sorting(
+         trimmed_classes,
+         concatenations,
+         class_attr,
+         delimiter,
+         true,
+         needs_curlies
+       ) do
+    concatenations
+    |> Enum.map(&(" \" <> " <> sort_classes_string(&1)))
+    |> List.insert_at(0, sort_classes_string(trimmed_classes))
+    |> Enum.join("")
+    # trimmed_classes comes once without and once with space somehow
+    |> String.replace("  ", " ")
+    |> then(fn sorted ->
+      class_attr <> delimiter <> wrap_classes(sorted, needs_curlies)
+    end)
+  end
+
+  # when we want to sort but on a concatenations is a slash appended
+  defp sort_classes_string(" \"" <> str = _reg), do: " \"" <> sort_classes_string(str)
+
+  defp sort_classes_string(str),
+    do: str |> String.split() |> sort_variant_chains() |> sort() |> Enum.join(" ")
+
   defp validate_inline_fns(contents) do
     Regex.scan(Defaults.func_regex(), contents, capture: :all_but_first)
     |> Enum.map(&List.first/1)


Test patch:
diff --git a/test/tailwind_formatter_test.exs b/test/tailwind_formatter_test.exs
index f60fadf..4e03537 100644
--- a/test/tailwind_formatter_test.exs
+++ b/test/tailwind_formatter_test.exs
@@ -514,5 +514,29 @@ defmodule TailwindFormatterTest do
 
       assert_formatter_output(input, input)
     end
+
+    test "works with multiple concatenations in one line" do
+      input = """
+      <div class={"odd:decoration-slate-50 uppercase tomato disabled:sm:text-lg text-sm dark:disabled:sm:lg:group-hover:text-blue-500 sm:lowercase sm:hover:bg-unknown-500 sm:hover:bg-gray-500 sm:disabled:text-2xl" <> "px-3  py-3 rounded-lg   " <> "px-3     py-5 rounded-lg " <> "text-sm" <> "tomato"}></div>
+      """
+
+      expected = """
+      <div class={"tomato text-sm uppercase odd:decoration-slate-50 sm:lowercase sm:hover:bg-unknown-500 sm:hover:bg-gray-500 sm:disabled:text-lg sm:disabled:text-2xl lg:sm:dark:group-hover:disabled:text-blue-500 " <> "rounded-lg px-3 py-3 " <> "rounded-lg px-3 py-5 " <> "text-sm " <> "tomato"}></div>
+      """
+
+      assert_formatter_output(input, expected)
+    end
+
+    test "issue#16" do
+      input = """
+      <div class={"h-6 " <> if @active, do: "bg-white", else: "bg-red"}></div>
+      """
+
+      expected = """
+      <div class={"h-6 " <> if @active, do: "bg-white", else: "bg-red"}></div>
+      """
+
+      assert_formatter_output(input, expected)
+    end
   end
 end