100phlecs / tailwind_formatter Goto Github PK
View Code? Open in Web Editor NEWSorts tailwind classes within elixir projects
Home Page: https://hexdocs.pm/tailwind_formatter
License: MIT License
Sorts tailwind classes within elixir projects
Home Page: https://hexdocs.pm/tailwind_formatter
License: MIT License
Ran into an error where the formatter has some ordering issues with classes not defined in the priv/classes.txt
- to where it would switch classes around.
Effectively this bug makes mix format --check-formatted
fail every time even if you run mix format
This class list is sorting correctly:
<body class="bg-zinc-200 text-zinc-900">
Tailwind 3.3 introduced an extended palette that includes a 950 shade for every color.
This one is not sorting correctly:
<body class="text-zinc-950 bg-zinc-200">
text-zinc-950
gets moved to the front as it's not recognized by the formatter.
This is so there's no structural changes that may conflict with HEEx Formatter.
The reason it's currently altering the structure is because it's hard to select the two quotes with inline elixir functions.
So the current formatter reconstructs the class:
or class=
even if it removes \n
characters.
A possible solution would be to take the original class:
string and do a String.split
on the first "
: this'll capture the possible \n
characters preceding the string.
Still, the tricky part is capturing the formatting afterwards too. i.e.
<a
class={
"content"
}
>
The following code:
<tr>
<th>ETA/ETD</th>
<td>
<%= for {vessel, eta, etd} <- list do %>
<p class={"qa-thingy-etas-#{vessel.id} has-margin-bottom-sm"}>
<span class={"qa-thingy-etas-#{vessel.id}-vessel"}><%= vessel.name %></span>
(<span class={"qa-thingy-etas-#{vessel.id}-eta"}><%= convert_date?(
eta,
"Not Specified"
) %></span> – <span class={"qa-thingy-etas-#{vessel.id}-etd"}><%= convert_date?(
etd,
"Not Specified"
) %></span>)
</p>
<% end %>
</td>
</tr>
<tr>
<th>Deliverables</th>
<td>
<%= for {fuel, vessel_fuels} <- list,
vessel_fuel <- vessel_fuels do %>
<p class={"qa-thingy-deliverable-#{vessel_fuel.id} has-margin-bottom-sm"}>
<span class="qa-thingy-deliverable-quantity"><%= vessel_fuel.quantity %></span>
MT of <span class="qa-thingy-deliverable-fuel"><%= fuel.name %></span>
to
<span class="qa-thingy-deliverable-vessel">
<%= vessel_fuel.vessel.name %> (<%= vessel_fuel.vessel.imo %>)
</span>
</p>
<% end %>
</td>
</tr>
gives me this error:
** (ArgumentError) errors were found at the given arguments:
* 1st argument: not a textual representation of an integer
:erlang.binary_to_integer("0$-vessel")
lib/tailwind_formatter.ex:146: TailwindFormatter.get_sort_position/1
lib/tailwind_formatter.ex:157: anonymous fn/1 in TailwindFormatter.sort_base_classes/1
(elixir 1.14.2) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
lib/tailwind_formatter.ex:156: TailwindFormatter.sort_base_classes/1
lib/tailwind_formatter.ex:124: TailwindFormatter.sort/1
lib/tailwind_formatter.ex:51: anonymous fn/1 in TailwindFormatter.format/2
(elixir 1.14.2) lib/regex.ex:756: Regex.apply_list/5
https://tailwindcss.com/docs/hover-focus-and-other-states#ordering-stacked-modifiers
even though prose-*
variants have a default sort value that pushes them toward back, they actually need to be the last variant on a variant stack to apply correctly.
The quickest solution is to move all the prose-*
modifiers above common variants like hover
- but maybe there are more not accounted for.
Hello and thank you for this great library. I'm interested in using it but wondering if function components are supported?
For example, if I have a class
on a <.link>
tag autoformatting doesn't work, but if I change it to a div it works:
While rendering, some optional classes may be encoded with #{ if x, do: y}
The regex probably needs to be changed
On a billing page I have the following code:
<.img src={~p"/images/#{"card-#{@card.brand}.svg"}"} />
The formatter errors with the following:
mix format failed for file: lib/heads_down_web/live/subscription_live/edit.html.heex
** (ArgumentError) Invalid inlined elixir function:
"card-#{@card.brand -- missing interpolation terminator: "}" (for string starting at line 1)
lib/tailwind_formatter.ex:61: anonymous fn/1 in TailwindFormatter.validate_inline_fns/1
(elixir 1.14.3) lib/enum.ex:975: Enum."-each/2-lists^foreach/1-0-"/2
lib/tailwind_formatter.ex:58: TailwindFormatter.validate_inline_fns/1
lib/tailwind_formatter.ex:19: TailwindFormatter.format/2
(mix 1.14.3) lib/mix/tasks/format.ex:576: Mix.Tasks.Format.format_file/2
(elixir 1.14.3) lib/task/supervised.ex:89: Task.Supervised.invoke_mfa/2
(elixir 1.14.3) lib/task/supervised.ex:34: Task.Supervised.reply/4
(stdlib 4.2) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Thank you so much for this library!
The first time I tried the formatter in one of our projects it worked. The second time I got an FunctionClauseError no function clause matching in Phoenix.LiveView.HTMLFormatter.to_tree/4
.
It turns out that the first run of the tailwind formatter formats the following
<div class={"h-6 " <> if @active, do: "bg-white", else: "bg-red"}></div>
to
<div class={"h-6"} <> if @active, do: "bg-white", else: "bg-red"}></div>
And this is invalid code because of the inserted }
:-)
Hi,
thanks a lot for this, awesome work!
I found an issue with class sorting when interpolating a variable:
<div class={"lg:grid-cols-#{@cols}"}>
is formatted to
<div class={"#{@cols} lg:grid-cols-"}>
I haven't looked into the code yet to see where it breaks but I will hopefully be able to do that in the next days.
Tailwind uses dot characters for some classes (like py-3.5
), but as soon as one is added to the class string, the formatter won't sort it.
[&::-webkit-details-marker]:hidden
is formatted to :[&:-webkit-details-marker]:hidden
In a project using tailwind_formatter
in both :dev
and :test
.
MIX_ENV=dev mix format --check-formatted
shows tailwind_formatter
things to be fixed.
However,
MIX_ENV=test mix format --check-formatted
runs clean, finding no issues.
config/config.exs
only configures :tailwind
in :dev
.
While the client may be able to configure around this, it seems the default behavior should be consistent between :dev
and :test
.
Also, I suspect the current behavior contributed to this previously reported issue as well.
I've forked, fixed, and tested this already. Happy to offer it as a PR.
My main test was performed manually, seeing that the Symptoms described above were resolved. And that the existing tailwind_formatter
test suite passed 100%. I did not add any new tests specifically for this situation.
When one uses the standalone cli with custom classes–depending on your release strategy, if you only pull in :prod
dependencies before generating your tailwind classes, the standalone cli will throw an error as tailwind_formatter
is nowhere to be found
This is encountered in unreleased commit, 0dcc490:
<div class={["text-sm potato sm:lowercase uppercase"]}></div>
is turned into:
<div class={"["text-sm potato sm:lowercase uppercase"]"}></div>
The formatter currently seems to reduce multiple spaces to single spaces but keeps the first and last space, these should be removed.
" grid hidden h-96 grid-cols-4 " currently becomes
" grid hidden h-96 grid-cols-4 " instead of
"grid hidden h-96 grid-cols-4"
The Order
class doesn't get recompiled when external resources are created or modified.
Relevant thread:
https://elixirforum.com/t/recompile-dependency-based-on-external-resource/49030
I'm wondering if we should just read the files at runtime and ensure they're only read once for each invocation of TailwindFormatter.format/2
. I'm hoping that won't take too long. I'll be happy to take a stab at this.
Elixir: 1.14.1
Erlang: 25.1.2
tailwind_formatter: 0.2.2
When I run
mix format --dot-formatter .tailwind_formatter.exs
with the following formatter config .tailwind_formatter.exs
[
plugins: [TailwindFormatter],
inputs: [
"*.{heex}",
"{lib}/**/*.{heex}"
]
]
I tested this with many different combinations of
plugins: [TailwindFormatter, Phoenix.LiveView.HTMLFormatter]
plugins: [Phoenix.LiveView.HTMLFormatter]
plugins: [TailwindFormatter]
but nothing seems to work for me.
TailwindFormatter constantly switches between the following orders
class="bg-colorGreen-400 inline-block rounded-lg px-3 py-3 text-center text-sm font-semibold text-white shadow-sm transition duration-200 hover:bg-colorGreen-500 hover:shadow-md focus:bg-colorGreen-600 focus:ring-colorGreen-500 focus:shadow-sm focus:ring-4 focus:ring-opacity-50"
class="bg-colorGreen-400 inline-block rounded-lg px-3 py-3 text-center text-sm font-semibold text-white shadow-sm transition duration-200 hover:bg-colorGreen-500 hover:shadow-md focus:ring-colorGreen-500 focus:bg-colorGreen-600 focus:shadow-sm focus:ring-4 focus:ring-opacity-50"
Reduced to the important stuff:
class="focus:bg-colorGreen-600 focus:ring-colorGreen-500"
class="focus:ring-colorGreen-500 focus:bg-colorGreen-600"
Like this, even if I would not care about this order, I can not use it in CI.
This happens with a few files more. I can also share the differences, if needed.
Maybe it is worth mentioning that I also use recode, prettier, stylelint, Phoenix.LiveView.HTMLFormatter, credo. All with the integrations in VSC. I would not exclude some interfering extensions but for now I hope you can spot and fix this problem.
Keep up the good work!
Thanks for the formatter. Exactly what I was looking for.
I created an alternative workaround for the mix limitations prior to the unreleased 1.15 for my use case looking like this, so I can still use this properly with my vscode plugin formatting files on save:
defmodule MultiFormatter do
if Version.match?(System.version(), ">= 1.13.0") do
@behaviour Mix.Tasks.Format
end
def features(_opts) do
[sigils: [:H], extensions: [".heex"]]
end
def format(contents, opts) do
Phoenix.LiveView.HTMLFormatter.format(contents, opts) |> TailwindFormatter.format(opts)
end
end
Maybe this is useful to you or could provide a basis for a more generic workaround in you project.
Hi,
I've have recently added the formatter and it seems to rename strings and change some of the variables. Seems strange to me.
Started as:
<CP.modal
:if={@live_action in [:confirm_delivery]}
id={"fixture_confirm_delivery_modal-#{@fixture.id}"}
show
on_cancel={JS.navigate(~p"/admin/fixtures")}
>
and ended like this after running mix format
:
<CP.modal
:if={@live_action in [:confirm_delivery]}
id={"fixture_confirm_delivery_container-#{auction_id}1"}
show
on_cancel={JS.navigate(~p"/admin/fixtures")}
>
This is my phoenix versions:
{:phoenix, "~> 1.7.0", override: true},
{:phoenix_view, "~> 2.0.2"},
{:phoenix_live_view, "~> 0.18.16"},
{:phoenix_html, "~> 3.2"}
With the new 0.4.0 version, classlists are formatted to one line like so:
From:
def button(%{variant: :gray} = assigns) do
~H"""
<button
type={@type}
class={[
"phx-submit-loading:opacity-75 rounded-lg disabled:pointer-events-none disabled:opacity-80 flex items-center justify-center border border-gray-300 bg-gray-200 text-gray-700",
"text-sm hover:bg-gray-300 active:bg-gray-300 px-4 py-[10px]",
@class
]}
{@rest}
>
<%= render_slot(@inner_block) %>
</button>
"""
end
To:
def button(%{variant: :gray} = assigns) do
~H"""
<button
type={@type}
class={["flex items-center justify-center rounded-lg border border-gray-300 bg-gray-200 text-gray-700 disabled:pointer-events-none disabled:opacity-80 phx-submit-loading:opacity-75", "py-[10px] px-4 text-sm hover:bg-gray-300 active:bg-gray-300", @class]}
{@rest}
>
<%= render_slot(@inner_block) %>
</button>
"""
end
This makes it harder to read, when there is a lot of classes.
BTW i love the new features!
~H"""
<div class="flex flex-row gap-3 justify-center items-center text-lg">
<CP.tooltip :for={{route, icon, text, _} <- @menu_items}>
<a class="text-gray-500" href={route}>
<i class={icon}></i>
</a>
<:hover_content>
<%= text %>
</:hover_content>
</CP.tooltip>
</div>
"""
is formatted into
~H"""
<div class="flex flex-row items-center justify-center gap-3 text-lg">
<CP.tooltip :for={{route, icon, text, _} <- @menu_items}>
<a class="text-gray-500" href={route}>
<i class={"icon"}></i>
</a>
<:hover_content>
<%= text %>
</:hover_content>
</CP.tooltip>
</div>
"""
You can see the icon variable of the < i > tag is converted into a string.
MultiFormatter
by itself is a bit generic and not self-documenting. I can imaging someone else looking at my .formatter.exs
and having basically no way to figure out what MultiFormatter
is doing or where it comes from without running git blame
.
Sticking it under TailwindFormatter.MultiFormatter
would, in my opinion, be the right move!
With the current regex, one may end up sorting the rest of the document if badly formatted HTML is sent in.
Validating the class_list
string doesn't have strange symbols is a strong guard against this. If any strange symbols are found after extracting inline elixir code, return the original document
Hey, the prettier plugin takes into the consideration the current tailwind config applied and applies the sorting accordingly.
At the moment tailwind formatter only takes into consideration the default config.
this means, that if you have a boxShadow: {
shadow-1: ... }
defined in your config, the formatter puts it in the front of the class list, rather than the location of boxShadows.
Is there possibility to support custom config?
This seems like a super wierd but severe bug. It seems like when having more than nine assigns in a string interpolation, all following string interpolations will be malformatted!
Format the following .heex
code:
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"bar: #{@bar}"} />
<div text={"bar: #{@bar}"} />
<div text={"bar: #{@bar}"} />
The expected outcome is that the code should be the same.
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"foo: #{@foo}"} />
<div text={"bar: #{@foo}0"} />
<div text={"bar: #{@foo}0"} />
<div text={"bar: #{@foo}0"} />
Using tailwind_formatter 0.3.2
, .formatter.exs
:
[
plugins: [TailwindFormatter],
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs,heex}"],
subdirectories: ["priv/*/migrations"]
]
A pattern in the Phoenix default components is to separate classes into a list.
Previously on the forum it was suggested to split classes up, with a declaration order.
This is also found within another guide.
The work foreseen to support lists:
The main thing up in the air is deciding how to divide the CSS classes. Tailwind already comes with three potential separations.
Further details:
Tailwind organizes the styles it generates into three different “layers” — a concept popularized by ITCSS.
- The base layer is for things like reset rules or default styles applied to plain HTML elements.
- The components layer is for class-based styles that you want to be able to override with utilities.
- The utilities layer is for small, single-purpose classes that should always take precedence over any other styles.
Being explicit about this makes it easier to understand how your styles will interact with each other, and using the @layer directive lets you control the final declaration order while still organizing your actual code in whatever way you like.
Suggestions welcomed
Hello 👋,
I just added tailwind_formatter
to my project dependencies but I keep seeing this error in the logs:
Tailwind CSS: Can't resolve 'tailwindcss/resolveConfig' in '/home/development/myapp/deps/tailwind_formatter/assets/js'
Does anyone what could be going on? 🤔
.tailwind_formatter.exs
as solution until multi-plugin format is supportedA declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.