feliperenan / heex_formatter Goto Github PK
View Code? Open in Web Editor NEWFormatter for Phoenix Live View templates.
Formatter for Phoenix Live View templates.
This code grows in new lines above and below the comment when it contains an eex block
Before:
<div></div>
<!-- <%= "comment" %> -->
<div></div>
After running formatter twice:
<div></div>
<!-- <%= "comment" %> -->
<div></div>
Unicode text in .heex
files seems to crash Heex Formatter 😢
testing.html.heex
, add 😔
or 我愛Phoenix!
** (ArgumentError) errors were found at the given arguments:
* 1st argument: not an iodata term
:erlang.iolist_to_binary([128532, 10])
lib/heex_formatter/phases/tokenizer.ex:40: HeexFormatter.Phases.Tokenizer.tokenize/2
(elixir 1.13.2) lib/enum.ex:1250: anonymous fn/3 in Enum.flat_map_reduce/3
(elixir 1.13.2) lib/enum.ex:4475: Enumerable.List.reduce/3
(elixir 1.13.2) lib/enum.ex:1249: Enum.flat_map_reduce/3
lib/heex_formatter/phases/tokenizer.ex:35: HeexFormatter.Phases.Tokenizer.run/2
lib/heex_formatter.ex:17: HeexFormatter.format/2
(mix 1.13.2) lib/mix/tasks/format.ex:561: Mix.Tasks.Format.format_file/2
Please let me know if you need any additional information! As far as I'm aware, I'm using the latest version of Heex Formatter.
I have an anonymous function call where the parentheses are stripped. See the line immediately following ~H"""
I had this
def link(%{to: _, inner_block: _} = assigns) do
assigns =
assigns
|> assign_new(:action, fn -> &live_patch/2 end)
~H"""
<%= @action.(to: @to) do %>
<span class="text-blue-600">
<%= render_slot(@inner_block) %>
</span>
<% end %>
"""
end
become this
def link(%{to: _, inner_block: _} = assigns) do
assigns =
assigns
|> assign_new(:action, fn -> &live_patch/2 end)
~H"""
<%= @action. to: @to do %>
<span class="text-blue-600">
<%= render_slot(@inner_block) %>
</span>
<% end %>
"""
end
The compiler was not happy about that!
Running mix format
removes some spaces, that should stay in place in heex templates:
- Last <%= length(@backlog_feeds) %> of <%= @feedcount %> backlog feeds
+ Last<%= length(@backlog_feeds) %>of<%= @feedcount %>backlog feeds
In case you want to see more context, the file, I ran mix format on is over here.
Happy to provide more information, basically all versions used are current.
Cheers, and happy to see this project making progress! The last changes fixed lots of issues I had with the formatter! 😍
In v0.1 end
lines up with case
when it follows directly from an empty clause
<div>
<%= case :foo do %>
<% :foo -> %>
something
<% _ -> %>
<% end %>
</div>
master is lining end
with the last clause
<div>
<%= case :foo do %>
<% :foo -> %>
something
<% _ -> %>
<% end %>
</div>
Great job with this!
Get this error though:
mix format failed for file: lib/classrooms_web/components/ui/submission.ex
** (ArgumentError) errors were found at the given arguments:
2nd argument: out of range
(stdlib 3.17) :binary.copy(" ", -1)
lib/heex_formatter/phases/format.ex:138: HeexFormatter.Phases.Format.indent_expression/1
lib/heex_formatter/phases/format.ex:131: HeexFormatter.Phases.Format.token_to_string/2
lib/heex_formatter/phases/format.ex:37: anonymous fn/2 in HeexFormatter.Phases.Format.run/2
(elixir 1.13.1) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
lib/heex_formatter/phases/format.ex:36: HeexFormatter.Phases.Format.run/2
(elixir 1.13.1) lib/code/formatter.ex:1323: Code.Formatter.maybe_sigil_to_algebra/4
(elixir 1.13.1) lib/code/formatter.ex:456: Code.Formatter.quoted_to_algebra/3
defmodule Ui.Submission do
use Phoenix.Component
import Phoenix.HTML.Link
def state(assigns) do
css = "#{show?(assigns.chosen_course)}"
~H"""
<span x-data="{ tooltip: false }"
x-on:mouseover="tooltip = true"
x-on:mouseleave="tooltip = false"
class="ml-2 h-5 w-5 cursor-pointer">
<div class="text-sm text-white absolute bg-gray-700 rounded-lg p-2
transform -translate-y-8 translate-x-8" x-show="tooltip">
Klicka för att se elevens arbete
</div>
<%= case @state do %>
<% "NEW" -> %>
<%=link target: "_blank", to: @url do %>
<button class={css}>
<Box.icons
type="regular"
name="file-find"
size="20"
class="stroke-red-900 fill-red-900"
/>
</button>
<% end %>
<% "CREATED" -> %>
<%=link target: "_blank", to: @url do %>
<button class={css} >
<Box.icons
type="regular"
name="edit"
size="20"
class="stroke-yellow-600 fill-yellow-600"
/>
</button>
<% end %>
<% "TURNED_IN" -> %>
<%=link target: "_blank", to: @url do %>
<button class={css}>
<Box.icons
type="regular"
name="task"
size="20"
class="stroke-green-600 fill-green-600"
/>
</button>
<% end %>
<% "RECLAIMED_BY_STUDENT" -> %>
<%=link target: "_blank", to: @url do %>
<button class={css}>
<Box.icons
type="regular"
name="arrow-from-right"
size="20"
class="stroke-gray-600 fill-gray-600"
/>
</button>
<% end %>
<% "RETURNED" -> %>
<%=link target: "_blank", to: @url do %>
<button class={css}>
<Box.icons
type="regular"
name="recycle"
size="20"
class="stroke-gray-600 fill-gray-600"
/>
</button>
<% end %>
<% end %>
</span>
"""
end
defp show?(true) do
"drop-shadow-md hover:drop-shadow-xl "
end
defp show?(nil) do
"drop-shadow-md hover:drop-shadow-xl"
end
defp show?(_) do
"drop-shadow-md hover:drop-shadow-xl opacity-25"
end
end
Causes error:
def table_row(assigns) do
~H"""
<tr>
<%= for value <- @values do %>
<td class="border-2">
<%= case value.type do %>
<% :text -> %>
Do something
<% _ -> %>
Do something else
<% end %>
</td>
<% end %>
</tr>
"""
end
If I remove the Do something (Which makes the code pointless btw) it "works":
def table_row(assigns) do
~H"""
<tr>
<%= for value <- @values do %>
<td class="border-2">
<%= case value.type do %>
<% :text -> %>
<% _ -> %>
Do something else
<% end %>
</td>
<% end %>
</tr>
"""
end
(ArgumentError) errors were found at the given arguments:
2nd argument: out of range
(stdlib 3.17) :binary.copy(" ", -1)
lib/heex_formatter/phases/format.ex:199: HeexFormatter.Phases.Format.indent_expression/2
lib/heex_formatter/phases/format.ex:140: HeexFormatter.Phases.Format.token_to_string/2
lib/heex_formatter/phases/format.ex:44: anonymous fn/2 in HeexFormatter.Phases.Format.run/2
(elixir 1.13.1) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
lib/heex_formatter/phases/format.ex:43: HeexFormatter.Phases.Format.run/2
(elixir 1.13.1) lib/code/formatter.ex:1323: Code.Formatter.maybe_sigil_to_algebra/4
(elixir 1.13.1) lib/code/formatter.ex:456: Code.Formatter.quoted_to_algebra/3
Formatter commit used: c1ffe57
I'm working with a couple of files which contain interpolated styles using HEEx and are failing to format. I was able to trim it down to this minimal reproduction:
<style>
:root {
<%= 2 + 2 %>
<%= 2 + 2 %>
}
</style>
❯ mix format
[
{:tag_block, "style", [],
[
{:text, "\n :root {\n ", %{newlines: 1}},
{:eex, "2 + 2", %{column: 5, line: 3, opt: '='}},
{:text, "\n ", %{newlines: 1}},
{:eex, "2 + 2", %{column: 5, line: 4, opt: '='}},
{:text, "\n }\n", %{newlines: 1}}
]}
]
mix format failed for file:
** (ArgumentError) errors were found at the given arguments:
* 1st argument: not a nonempty list
:erlang.tl([])
lib/heex_formatter/algebra.ex:258: HeexFormatter.Algebra.text_to_algebra/3
lib/heex_formatter/algebra.ex:209: HeexFormatter.Algebra.to_algebra/2
lib/heex_formatter/algebra.ex:42: anonymous fn/3 in HeexFormatter.Algebra.block_to_algebra/2
(elixir 1.13.2) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
lib/heex_formatter/algebra.ex:39: HeexFormatter.Algebra.block_to_algebra/2
lib/heex_formatter/algebra.ex:119: HeexFormatter.Algebra.to_algebra/2
lib/heex_formatter/algebra.ex:36: HeexFormatter.Algebra.block_to_algebra/2
It seems to get as far as the third node: {:text, "\n ", %{newlines: 1}}
before failing.
After running the formatter converts inputs
<%= email_input f, :email_address size: 40 %>
becomes
<%= email_input(f, :email_address,
size: 40
) %>
but these labels are untouched
<%= label f, :email_address, class: "text-gray font-medium" do %>
Email Address
<% end %>
The valid HTML of <br>
which is handled fine by heex appears to break this formatter. I have not tried other valid void elements but I expect they might behave the same way. For reference: https://dev.w3.org/html5/html-author/#void
EEx.Tokenizer.tokenize
returns a single token for multiline comments. For instance, given the following input:
<section>
<!-- commenting out this div
<div>
<p class="my-class">text</p>
</div>
-->
</section>
The EEx.Tokenizer
will return the following structure:
[
{:text, 0, 0,
' <!-- Inline comment -->\n <section>\n <!-- commenting out this div\n <div>\n <p class="my-class">text</p
>\n </div>\n -->\n </section>\n'},
{:eof, 8, 1}
]
Which is send to Phoenix.LiveView.Tokenizer
and transformed to:
[
{:tag_open, "section", [], %{column: 3, line: 1}},
{:text,
"\n <!-- commenting out this div\n <div>\n <p class=\"my-class\">text</p>\n </div>\n -->\n ",
%{column_end: 3, line_end: 7}},
{:tag_close, "section", %{column: 3, line: 7}}
]
We get a different structure when there are eex tags included:
Given the HTML with eex tags:
<section>
<!-- commenting out this div
<div>
<p class="my-class"><%= @user.name %></p>
</div>
-->
</section>
That's the EEx.Tokenizer
output:
[
{:text, 0, 0,
' <section>\n <!-- commenting out this div\n <div>\n <p class="my-class">'},
{:expr, 3, 27, '=', ' user.name '},
{:text, 3, 43, '</p>\n </div>\n -->\n </section>\n'},
{:eof, 7, 1}
]
Which makes the Phoenix.LiveView.Tokenizer
parse only the closing tags after the eex
expression.
[
{:tag_open, "section", [], %{column: 3, line: 1}},
{:text, "\n <!-- commenting out this div\n <div>\n <p class=\"my-class\">",
%{column_end: 27, line_end: 4}},
{:eex_tag_render, "<%= user.name %>", %{block?: false, column: 27, line: 3}},
{:tag_close, "p", %{column: 1, line: 1}},
{:tag_close, "div", %{column: 5, line: 2}},
{:text, "\n -->\n ", %{column_end: 3, line_end: 4}},
{:tag_close, "section", %{column: 3, line: 4}}
]
PS: We have the same behaviour on script tags.
Given the context above, I'm not sure how to handle this. I guess we shouldn't format stuff within HTML comment and script tags at all. But since EEx.Tokeziner
extracts eex expressions from these tags, we need to find way to put them together as they are so we can just ignore it.
Before going down this road, I'd like to confirm if there is something we could do in the Tokenizers
or if I'm missing something.
Currently using 4f53f70
Formatting this
<%= live_redirect(
to: "/my/path",
class: "my class"
) do %>
My Link
<% end %>
<%= label class: "control-label" do %>
<%= radio_button(:user, :choice,
"Choice") %>
<% end %>
<%= for foo <- @foos do %>
<%= foo %>
<% end %>
Produces this
<%= live_redirect(
to: "/my/path",
class: "my class"
) do %>
My Link
<% end %>
<%= label class: "control-label" do %>
<%= radio_button(:user, :choice, "Choice") %>
<% end %>
<%= for foo <- @foos do %>
<%= foo %>
<% end %>
The elixir code inside of <%= ... do %>
lines is not getting formatted
LOVE this project! Thank you to you and the rest of the contribs!
Here's a quick sample of running heex_formatter
on an almost brand-new Phoenix LiveView project..
Code diff of live.html.heex:
https://github.com/megalithic/megalithic.io/pull/2/files#diff-c5cc08848777a65f1977289d91aca51faef0b711e174e6429f083988535a1592
Screenshot of resulting formatting change:
You can see that the live flash alerts render a space so they show up with their styling (padding/etc)..
Thoughts on how to avoid this? Is there a CSS-specific fix, perhaps?
This is similar to #59, but still seems to be an issue in the latest main (c1ffe57).
When text is separated from an element by a newline, and the formatter closes them up, it doesn’t insert whitespace, which will change the appearance of the HTML.
For example, formatting this:
<button>
<i class="fa-solid fa-xmark"></i>
Close
</button>
gives this:
<button>
<i class="fa-solid fa-xmark"></i>Close
</button>
when it should probably give this:
<button>
<i class="fa-solid fa-xmark"></i> Close
</button>
This only happens when there was a newline – if the text and element are on the same line, separated by a space, then the space is correctly preserved.
Awesome work, by the way – great to see the benefits of the formatter extended to heex!
Currently, we're only formatting HTML tags, we need to also format eex
expression.
Given:
<%= live_component(MyAppWeb.MyComponent, id: "component-id", assign_1: a, assign_2: b, assign_3: c, assign_n: n) %>
We want to format to:
<%= live_component(MyAppWeb.MyComponent,
id: "component-id",
assign_1: a,
assign_2: b,
assign_3: c,
assign_n: n
) %>
Do not know how relevant this is but I stumbled on this when I reused some HTML -code with br tags. I found more similar tags and cut and pasted them in a HEEX-file with the result below. It might be a minor issue in some context?
<area>: Used for the area inside of an image map.
<base>
: The base URL for all relative URLs in a document. There can be no more than one of these per document and it must be in the head of the page.
<br>
: A line break, often used in text content to create a single line break instead of a paragraph. It should not be used to create visual separation on a page by stacking up many
<br>tags, because that function is a visual need and therefore the domain of CSS instead of HTML.
<col>: Specifies column properties for each column within a
<colgroup>element.
<command>: Specifies a command that a visitor can invoke.
<embed>: Used with external applications and interactive content for integration.
<hr>
: A horizontal rule, which is a straight line on a page. In many cases, CSS borders create separator lines instead of this HTML element.
<img>
: One of the workhorse elements of HTML, this is the image tag. It is used to add graphic images to a webpage.
<input>
: A form element that is used to capture information from visitors. There are a number of valid input types, from the common "text" input that has been used in forms for years, to some new input types that are part of HTML5.
<keygen>: This tag creates a key-pair generator field that is used for forms.
<link>: Not to be confused with the "hyperlink" or anchor (
<a>
) tag, this link is to set linkage between a document and an external resource. Use it to link to an external CSS file, for example.
<meta>
: Meta tags are "information about content." They are found in the head of a document and used to convey page information to the browser. There are many different meta tags that you can use on a webpage.
<param>: Used to define parameters for plugins.
<source>
: This tag allows you to specify alternative file paths for media on your page, including videos or images or audio files.
After running the formatter on this code
<div>
<%# some %>
<%# comment %>
<%# lines %>
</div>
The result has all comment tags stripped out
<div></div>
Putting a comment inside of an inline expression tag also doesn't quite work, the content and all following tags get un-indented one level
<div>
<% # some %>
<% # comment %>
<% # lines %>
</div>
<.next_thing>
A workaround for now is to use a replace expression tag. This stays the same after running the formatter
<div>
<%= # some %>
<%= # comment %>
<%= # lines %>
</div>
plugins: [HeexFormatter]
to .formatter.exs
Skipping formatter plugin HeexFormatter because module cannot be found
elixir 1.13.2
and phoenix 1.6.6
The formatter is working will for the sigil inside *.ex
files. However, it is not working for *.html.heex
files. Should we be adding something to the formatter.exs
file?
This code was formatted in v0.1
<.live_component
module={WorkGroupTypeComponent}
id={WorkGroupTypeComponent}
work_group_types={@work_group_types}
/>
and master is indenting the self-closing line with an extra space
<.live_component
module={WorkGroupTypeComponent}
id={WorkGroupTypeComponent}
work_group_types={@work_group_types}
/>
Is that expected?
Hello @feliperenan and @paulo-silva. Thanks for actively improving the module to fix all the issues encountered. However, the package in its current form, can not be added to a new project. Mainly, because you have changed the live_view dependency to your fork. May be you could have made a release without that fork ( even if it meant some feature is not working) - that would have allowed a smooth usage of the package.
It is not a complaint - just a suggestion. Thanks again.
Given this input
<!-- this is a HTML comment -->
<div>
<p>Hello</p>
</div>
the formatter outputs
<!-- this is a HTML comment --><div>
<p>Hello</p>
</div>
I wouldn't have expected it to change
I had this Bulma #modal-close button, fairly deeply-indented with no text content
<button class="modal-close is-large" aria-label="close"
phx-click="cancel-admin"></button>
On v0.1 it formatted to
<button class="modal-close is-large" aria-label="close" phx-click="cancel-admin"></button>
And now on master it's adding an extra line break.
<button class="modal-close is-large" aria-label="close" phx-click="cancel-admin">
</button>
Technically all buttons should have inner text for accessibility and Bulma does the right thing by hiding the text through CSS, so maybe this is working as intended, but it's maybe unexpected for people unfamiliar with the spec.
I have found an error in the formatter when creating new variables inside an HTML tag. It indents the variable creation with 0 tabs, when it should be 1. And when it gets to the next HEEX tag tries to indent it with -1 and it breaks.
This is a an example that breaks:
<p>
<% a = 1 %> <%# indents it to 0, should be 1 tab %>
</p>
<%= "hello" %> <%# indents it to -1, ERROR %>
This is the error:
** (ArgumentError) errors were found at the given arguments:
* 2nd argument: out of range
(stdlib 3.15.2) :binary.copy(" ", -1)
lib/heex_formatter/formatter.ex:504: HeexFormatter.Formatter.format_eex/2
lib/heex_formatter/formatter.ex:199: HeexFormatter.Formatter.token_to_string/2
lib/heex_formatter/formatter.ex:63: anonymous fn/2 in HeexFormatter.Formatter.format/2
(elixir 1.13.0) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
lib/heex_formatter/formatter.ex:61: HeexFormatter.Formatter.format/2
(mix 1.13.0) lib/mix/tasks/format.ex:560: Mix.Tasks.Format.format_file/2
(elixir 1.13.0) lib/task/supervised.ex:89: Task.Supervised.invoke_mfa/2
I have not been able to find the error in the formatter.ex
code. But I can create some tests for this.
Thanks so much for building this formatter, it is great. I spotted a couple of things in my codebase after running the formatter (using latest a208ca8)
Given the following input:
<!DOCTYPE html>
the output contains a newline above the doctype declaration
<!DOCTYPE html>
Thanks!
Another whitespace edge case I’m afraid (tested in 5fdff54) …
Starting with the the standard live layout created by mix phx.new
:
<p class="alert alert-info" role="alert"
phx-click="lv:clear-flash"
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
Running the formatter inserts newlines:
<p class="alert alert-info" role="alert" phx-click="lv:clear-flash" phx-value-key="info">
<%= live_flash(@flash, :info) %>
</p>
This adds whitespace html nodes either side of the inner element, which means that even if there is no content in the flash, the css rule for .alert:empty
doesn’t match, so empty flash divs appear on the page.
file.html.heex:
<div>
<h2 class="font-bold m-4">...
> mix format
file.html.heex:1:1: syntax error before: '<'
(elixir 1.12.3) lib/code.ex:978: Code.format_string!/2
(mix 1.12.3) lib/mix/tasks/format.ex:418: Mix.Tasks.Format.format_file/2
(elixir 1.12.3) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
(elixir 1.12.3) lib/task/supervised.ex:35: Task.Supervised.reply/5
(stdlib 3.16.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
For example:
<%= form_for @changeset, Routes.sign_in_path(@conn, :create), fn f -> %>
// 30 lines of html
<% end %>
gets turned into
<%= form_for(@changeset, Routes.sign_in_path(@conn, :create), fn f -> %>
// 30 lines of html
<% end %>
e.g.
~H"<template hidden></template>"
becomes
~H"<template hidden></template>
"
Hi,
I followed the instructions on the README.md file in a new phoenix project, however, I receive the following error:
mix deps.get
Dependencies have diverged:
* phoenix_live_view (Hex package)
the dependency phoenix_live_view in mix.exs is overriding a child dependency:
> In mix.exs:
{:phoenix_live_view, "~> 0.17.5", [env: :prod, repo: "hexpm", hex: "phoenix_live_view"]}
> In deps/heex_formatter/mix.exs:
{:phoenix_live_view, [env: :prod, git: "https://github.com/phoenixframework/phoenix_live_view.git"]}
Ensure they match or specify one of the above in your deps and set "override: true"
** (Mix) Can't continue due to errors on dependencies
Adding override: true
to phoenix_live_view
fixes the error, however, I then encounter an issue when I run mix format
:
mix format
Skipping formatter plugin HeexFormatter because module cannot be found
Am I doing something wrong? This is with the latest version of Phoenix.
If you are open to it, I think you should allow sponsors to this Github repo. I for one would gladly sponsor some of the development.
Currently, the formatter is formatting cases statement like this:
<%= case value.type do %>
<% :text -> %>
Do something
<% _ -> %>
Do something else
<% end %>
but it is supposed to format like that:
<%= case value.type do %>
<% :text -> %>
Do something
<% _ -> %>
Do something else
<% end %>
Same happens for cond
:
<%= cond do %>
<% 1 == 1 -> %>
<%= "Hello" %>
<% 2 == 2 -> %>
<%= "World" %>
<% true -> %>
<%= "" %>
<% end %>
The correct format is:
<%= cond do %>
<% 1 == 1 -> %>
<%= "Hello" %>
<% 2 == 2 -> %>
<%= "World" %>
<% true -> %>
<%= "" %>
<% end %>
Just wanted to report a small issue that might be related to #65
I have this formatted code (removed a few things for clarity):
<p>
You can also<a href>sign up</a>or<a href>sign in</a>
with your email address.
</p>
Which appears with words together as You can alsosign uporsign in with your email address.
Any attempt to separate the parts will format to the code above
A 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.