From 2c5d9c7329a1b5604fa5f1ade859257d05b713ca Mon Sep 17 00:00:00 2001 From: Aaron Tinio Date: Fri, 8 Dec 2023 08:58:07 +0800 Subject: [PATCH] Make Order a project module instead of a dependency module so that its external resources can be tracked. --- lib/tailwind_formatter.ex | 266 ++++++++++++++++++-------------- lib/tailwind_formatter/order.ex | 29 ---- 2 files changed, 150 insertions(+), 145 deletions(-) delete mode 100644 lib/tailwind_formatter/order.ex diff --git a/lib/tailwind_formatter.ex b/lib/tailwind_formatter.ex index 7c9b2ed..8433da9 100644 --- a/lib/tailwind_formatter.ex +++ b/lib/tailwind_formatter.ex @@ -5,146 +5,180 @@ defmodule TailwindFormatter do |> String.split("") |> Enum.fetch!(1) - alias TailwindFormatter.{Order, HEExTokenizer} + defmacro __using__(_opts) do + quote do + defmodule Order do + custom_classes = Path.join(Path.dirname(Mix.Project.build_path()), "classes.txt") + custom_variants = Path.join(Path.dirname(Mix.Project.build_path()), "variants.txt") + + @external_resource custom_classes + @external_resource custom_variants + + @moduledoc false + order_map = fn src -> + Mix.shell().info([:green, "Loading in #{src}."]) + + src + |> File.read!() + |> String.split(~r/\R/) + |> Enum.with_index() + |> Map.new() + end + + @classes if File.exists?(custom_classes), + do: order_map.(custom_classes), + else: order_map.("priv/classes.txt") + + @variants if File.exists?(custom_variants), + do: order_map.(custom_variants), + else: order_map.("priv/variants.txt") + + def variants(), do: @variants + def classes(), do: @classes + end - @behaviour Mix.Tasks.Format + alias TailwindFormatter.HEExTokenizer - @placeholder "💧" + @behaviour Mix.Tasks.Format - def features(_opts) do - [sigils: [:H], extensions: [".heex"]] - end + @placeholder "💧" - def format(contents, _opts) do - contents - |> HEExTokenizer.tokenize() - |> Enum.reduce([contents], fn - {:tag, _name, attrs, _meta}, contents -> - 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] + def features(_opts) do + [sigils: [:H], extensions: [".heex"]] + end + + def format(contents, _opts) do + contents + |> HEExTokenizer.tokenize() + |> Enum.reduce([contents], fn + {:tag, _name, attrs, _meta}, contents -> + 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 - _, 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 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() - end + 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() + end - defp sort_expr({:<<>>, meta, children}), do: {:<<>>, meta, handle_interpolation(children)} - defp sort_expr({a, b, c}), do: {sort_expr(a), sort_expr(b), sort_expr(c)} - defp sort_expr({a, b}), do: {sort_expr(a), sort_expr(b)} - defp sort_expr(list) when is_list(list), do: Enum.map(list, &sort_expr/1) - defp sort_expr(text) when is_binary(text), do: sort(text) - defp sort_expr(node), do: node - - defp handle_interpolation(children) do - {classes_with_placeholders, {placeholder_map, _index}} = - Enum.map_reduce(children, {%{}, 0}, fn - str, acc when is_binary(str) -> - {str, acc} - - node, {placeholder_map, index} -> - {"#{@placeholder}#{index}#{@placeholder}", - {Map.put(placeholder_map, "#{index}", sort_expr(node)), index + 1}} - end) - - classes_with_placeholders - |> Enum.reduce("", fn class, acc -> - if placeholder?(class) or String.starts_with?(class, "-"), - do: acc <> class, - else: "#{acc} #{class}" - end) - |> sort() - |> String.split() - |> weave_in_code(placeholder_map) - end + defp sort_expr({:<<>>, meta, children}), do: {:<<>>, meta, handle_interpolation(children)} + defp sort_expr({a, b, c}), do: {sort_expr(a), sort_expr(b), sort_expr(c)} + defp sort_expr({a, b}), do: {sort_expr(a), sort_expr(b)} + defp sort_expr(list) when is_list(list), do: Enum.map(list, &sort_expr/1) + defp sort_expr(text) when is_binary(text), do: sort(text) + defp sort_expr(node), do: node + + defp handle_interpolation(children) do + {classes_with_placeholders, {placeholder_map, _index}} = + Enum.map_reduce(children, {%{}, 0}, fn + str, acc when is_binary(str) -> + {str, acc} + + node, {placeholder_map, index} -> + {"#{@placeholder}#{index}#{@placeholder}", + {Map.put(placeholder_map, "#{index}", sort_expr(node)), index + 1}} + end) + + classes_with_placeholders + |> Enum.reduce("", fn class, acc -> + if placeholder?(class) or String.starts_with?(class, "-"), + do: acc <> class, + else: "#{acc} #{class}" + end) + |> sort() + |> String.split() + |> weave_in_code(placeholder_map) + end - defp weave_in_code(classes, placeholder_map) do - classes - |> Enum.map(fn class -> - if placeholder?(class) do - [prefix, index, suffix] = String.split(class, @placeholder) - [prefix, Map.fetch!(placeholder_map, index), suffix] - else - class + defp weave_in_code(classes, placeholder_map) do + classes + |> Enum.map(fn class -> + if placeholder?(class) do + [prefix, index, suffix] = String.split(class, @placeholder) + [prefix, Map.fetch!(placeholder_map, index), suffix] + else + class + end + end) + |> Enum.intersperse(" ") + |> List.flatten() end - end) - |> Enum.intersperse(" ") - |> List.flatten() - end - defp sort_variant_chains(classes) do - classes - |> String.split() - |> Enum.map(fn class -> - class - |> String.split(":") - |> Enum.sort_by(&variant_position/1, :desc) - |> Enum.join(":") - end) - end + defp sort_variant_chains(classes) do + classes + |> String.split() + |> Enum.map(fn class -> + class + |> String.split(":") + |> Enum.sort_by(&variant_position/1, :desc) + |> Enum.join(":") + end) + end - defp sort(classes) when is_binary(classes) do - leading_space = if classes =~ ~r/\A\s/, do: " " - trailing_space = if classes =~ ~r/\s\z/, do: " " + defp sort(classes) when is_binary(classes) do + leading_space = if classes =~ ~r/\A\s/, do: " " + trailing_space = if classes =~ ~r/\s\z/, do: " " - classes = - classes - |> sort_variant_chains() - |> sort() - |> Enum.join(" ") + classes = + classes + |> sort_variant_chains() + |> sort() + |> Enum.join(" ") - Enum.join([leading_space, classes, trailing_space]) - end + Enum.join([leading_space, classes, trailing_space]) + end - defp sort([]), do: [] + defp sort([]), do: [] - defp sort(class_list) when is_list(class_list) do - {variants, base_classes} = Enum.split_with(class_list, &variant?/1) + defp sort(class_list) when is_list(class_list) do + {variants, base_classes} = Enum.split_with(class_list, &variant?/1) - Enum.sort_by(base_classes, &class_position/1) ++ sort_variant_classes(variants) - end + Enum.sort_by(base_classes, &class_position/1) ++ sort_variant_classes(variants) + end - defp placeholder?(class), do: String.contains?(class, @placeholder) - defp variant?(class), do: String.contains?(class, ":") - defp prose?(class), do: String.contains?(class, "prose") + defp placeholder?(class), do: String.contains?(class, @placeholder) + defp variant?(class), do: String.contains?(class, ":") + defp prose?(class), do: String.contains?(class, "prose") - defp class_position(class), - do: if(placeholder?(class), do: -1_000_000, else: Map.get(Order.classes(), class, -1)) + defp class_position(class), + do: if(placeholder?(class), do: -1_000_000, else: Map.get(Order.classes(), class, -1)) - # prose variant order matters, thus push to front - defp variant_position(variant), - do: if(prose?(variant), do: 0, else: Map.get(Order.variants(), variant, -1)) + # prose variant order matters, thus push to front + defp variant_position(variant), + do: if(prose?(variant), do: 0, else: Map.get(Order.variants(), variant, -1)) - defp sort_variant_classes(variants) do - variants - |> group_by_first_variant() - |> Enum.sort_by(fn {variant, _rest} -> variant_position(variant) end) - |> Enum.map(fn {variant, rest} -> {variant, sort(rest)} end) - |> Enum.flat_map(fn {variant, rest} -> Enum.map(rest, &"#{variant}:#{&1}") end) - end + defp sort_variant_classes(variants) do + variants + |> group_by_first_variant() + |> Enum.sort_by(fn {variant, _rest} -> variant_position(variant) end) + |> Enum.map(fn {variant, rest} -> {variant, sort(rest)} end) + |> Enum.flat_map(fn {variant, rest} -> Enum.map(rest, &"#{variant}:#{&1}") end) + end - defp group_by_first_variant(variants) do - variants - |> Enum.map(&String.split(&1, ":", parts: 2)) - |> Enum.group_by(&List.first/1, &List.last/1) + defp group_by_first_variant(variants) do + variants + |> Enum.map(&String.split(&1, ":", parts: 2)) + |> Enum.group_by(&List.first/1, &List.last/1) + end + end end end diff --git a/lib/tailwind_formatter/order.ex b/lib/tailwind_formatter/order.ex deleted file mode 100644 index 23e7c9f..0000000 --- a/lib/tailwind_formatter/order.ex +++ /dev/null @@ -1,29 +0,0 @@ -defmodule TailwindFormatter.Order do - custom_classes = Path.join(Path.dirname(Mix.Project.build_path()), "classes.txt") - custom_variants = Path.join(Path.dirname(Mix.Project.build_path()), "variants.txt") - - @external_resource custom_classes - @external_resource custom_variants - - @moduledoc false - order_map = fn src -> - Mix.shell().info([:green, "Loading in #{src}."]) - - src - |> File.read!() - |> String.split(~r/\R/) - |> Enum.with_index() - |> Map.new() - end - - @classes if File.exists?(custom_classes), - do: order_map.(custom_classes), - else: order_map.("priv/classes.txt") - - @variants if File.exists?(custom_variants), - do: order_map.(custom_variants), - else: order_map.("priv/variants.txt") - - def variants(), do: @variants - def classes(), do: @classes -end