surface-ui / surface_formatter Goto Github PK
View Code? Open in Web Editor NEWLicense: MIT License
License: MIT License
Hi @paulstatezny!
Since all syntax changes have been already implemented and the new converter is already available in https://github.com/surface-ui/surface/tree/surface-next, I think we can start working on other important projects that should be released alongside Surface to make sure everything goes as smooth as possible. My plan is to have v0.5
released by the end on the week unless something unexpected comes up.
Detailed information about the migration process can be found in the Migration Guide and the CHANGELOG has been updated with all changes.
Regarding the formatter, the most important changes to be aware of are probably:
~H
with ~F
to avoid conflict with HEEx
{{
and }}
with {
and }
{!-- --}
{ }
, {... }
and {= }
{#if}..{#elseif}..{#else}..{/if}
, {#for}..{#else}..{/for}
, etc.The last three changes above introduces new tokens or metadata at the tokenizer level so we'll have to handle them. For a quick reference, they are:
{:comment, comment, %{visibility: visibility}}
- where visibility
can be :private
or :public
, representing {!-- --}
and <!-- -->
respectively.{:tagged_expr, marker, expr, meta}
- where marker
can be ...
or =
{:block, name, expr, body, meta}
- where name
can be the name of the block (e.g. if
, case
) or sub-block (e.g. else
, match
)NodeTranslator
behaviourWe changed the design of the parser allowing external tools to use a different node translator that can generate a tree of nodes according to their needs. All you have to do is implementing the Surface.Compiler.NodeTranslator behaviour and define the nodes in the shape you want. Here's the implementation of the translator used by the default Surface parser: https://github.com/surface-ui/surface/blob/surface-next/lib/surface/compiler/parse_tree_translator.ex.
For the formatter, we can have a very similar one but much simpler as it requires less information and no validation.
Please let me know if you need any further clarification.
Cheers.
Is there any way to configure this to run as part of the mix format
command?
<div>
{gettext(~M"It's my cat's {year, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} birthday!")}
</div>
The Formatter Plugin for the M
sigil is executed. (In this example: https://hexdocs.pm/ex_cldr_messages/Cldr.Formatter.Plugin.html#content)
.formatter.exs
[
inputs: ["..."],
plugins: [Surface.Formatter.Plugin, Cldr.Formatter.Plugin]
]
The formatter doesn't generate code that causes directives to be sent to the browser.
<div :if={true} />
is formatted into <div :if>
, which makes Surface send <div :if=""></div>
to the browser instead of <div></div>
.
Thanks @bretfunk for reporting.
Example .sface
:
<div>
{gettext("foo
bar
baz
")}
</div>
No change
On every run of mix format
, the lines are indented further:
<div>
{gettext("foo
bar
baz
")}
</div>
.formatter.exs
https://github.com/jshmrtn/hygeia/blob/b438cab763ce3f0eb6ff244c09c0e59e084dd7de/.formatter.exs#L1
[
import_deps: [:phoenix, :phoenix_live_view, :surface, :ecto, :ecto_enum],
inputs: [
".dialyzer_ignore.exs",
".formatter.exs",
"*.{ex,exs}",
"priv/*/seeds.exs",
"priv/repo/seeds/*.exs",
"{config,lib,test}/**/*.{ex,exs,sface}"
],
subdirectories: ["priv/*/migrations"],
plugins: [Surface.Formatter.Plugin]
]
Bug can be reproduced on this sample repo https://github.com/anildigital/surface_formatter_bug
$ mix surface.format
<TextInput
class={
"w-full h-12 max-w-full px-4 bg-xxx-100 hover:bg-xxx-120 text-base leading-normal
text-color-yyy-100 box-border border border-solid border-kkk-100 rounded transition
ease-in placeholder-hhh-100 placeholder-opacity-100 disabled:opacity-50
disabled:cursor-not-allowed focus:border-mmmm-100 focus:outline-none
no-scrollbar invalid:shadow-none invalid:border-ttt-100 #{@class}",
Then run again
$ mix surface.format
<TextInput
class={
"w-full h-12 max-w-full px-4 bg-xxx-100 hover:bg-xxx-120 text-base leading-normal
text-color-yyy-100 box-border border border-solid border-kkk-100 rounded transition
ease-in placeholder-hhh-100 placeholder-opacity-100 disabled:opacity-50
disabled:cursor-not-allowed focus:border-mmmm-100 focus:outline-none
no-scrollbar invalid:shadow-none invalid:border-ttt-100 #{@class}",
then run again
$ mix surface.format
<TextInput
class={
"w-full h-12 max-w-full px-4 bg-xxx-100 hover:bg-xxx-120 text-base leading-normal
text-color-yyy-100 box-border border border-solid border-kkk-100 rounded transition
ease-in placeholder-trunks-100 placeholder-opacity-100 disabled:opacity-50
disabled:cursor-not-allowed focus:border-mmmm-100 focus:outline-none
no-scrollbar invalid:shadow-none invalid:border-ttt-100 #{@class}",
before
@impl true
def render(assigns) do
~F"""
<div class="flex flex-col w-screen h-screen">
<div class="flex-grow w-full bg-white overflow-auto">
<div class="h-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="h-full py-6 flex flex-col justify-center items-center">
<div class="relative py-6 sm:max-w-7xl sm:mx-auto">
<div class="absolute inset-0 bg-gradient-to-r from-blue-700 to-blue-800 shadow-lg transform -skew-y-6 sm:skew-y-0 sm:rotate-2 sm:rounded-none" />
<div class="relative bg-white border border-blue-800 shadow-lg rounded-none">
{!--{#unless @step == 0}
<div class="absolute left-1 top-2 h-12 w-12">
<img src="/images/logo.svg" class="h-12 w-12">
</div>
{/unless}--}
{#case @step}
{#match 0}
{render_welcome()}
{#match 1}
<div class="max-w-none mx-auto">
<div class="bg-white overflow-hidden">
{render_card_header("Display & Reporting")}
<TimeAndDateSettings id="time-and-date-settings" {=@start_of_day} {=@timezone} />
</div>
</div>
{#match 2}
<div class="max-w-none mx-auto">
<div class="bg-white overflow-hidden">
{render_card_header(
"Default Alarm Columns",
"Choose the columns displayed on the alarm log and alarm search results screen."
)}
<div class="px-6 py-2">
<InitDefaultAlarmColumns id="default-alarm-columns" drop_zone={Settings.get(:default_alarm_columns)} />
</div>
</div>
</div>
{#match 3}
<div class="max-w-none mx-auto">
<div class="bg-white overflow-hidden">
{render_card_header(
"Alarm Categories",
"Label and order alarm categories. Categories can be defined as a combination of single numbers, lists of numbers or ranges. There can be no overlap between categories. For example, '1', '100-199', '1, 2, 5' and '10, 100-199, 1001' are all valid. To avoid importing entire categories of alarms, uncheck LOG."
)}
<div class="px-6 py-2">
<AlarmCategoriesSettings id="alarm-categories-settings" show_sidebar={@live_action == :edit_alarm_category} />
</div>
</div>
</div>
{#match _}
{render_done()}
{/case}
</div>
</div>
</div>
</div>
</div>
{#unless @step == 0}
<div class="flex items-center justify-end p-4 w-full bg-gray-50">
<Button click="change_step" values={step: @step - 1}>
Back
</Button>
<Button color="green" click="change_step" values={step: @step + 1} left_margin>
{#if @step < @steps}
Next
{#else}
Done
{/if}
</Button>
</div>
{/unless}
</div>
"""
end
run mix surface.format
and it removes the {/unless}
from the second last line. The line stays there, but is empty :(
after:
@impl true
def render(assigns) do
~F"""
<div class="flex flex-col w-screen h-screen">
<div class="flex-grow w-full bg-white overflow-auto">
<div class="h-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="h-full py-6 flex flex-col justify-center items-center">
<div class="relative py-6 sm:max-w-7xl sm:mx-auto">
<div class="absolute inset-0 bg-gradient-to-r from-blue-700 to-blue-800 shadow-lg transform -skew-y-6 sm:skew-y-0 sm:rotate-2 sm:rounded-none" />
<div class="relative bg-white border border-blue-800 shadow-lg rounded-none">
{!--{#unless @step == 0}
<div class="absolute left-1 top-2 h-12 w-12">
<img src="/images/logo.svg" class="h-12 w-12">
</div>
{/unless}--}
{#case @step}
{#match 0}
{render_welcome()}
{#match 1}
<div class="max-w-none mx-auto">
<div class="bg-white overflow-hidden">
{render_card_header("Display & Reporting")}
<TimeAndDateSettings id="time-and-date-settings" {=@start_of_day} {=@timezone} />
</div>
</div>
{#match 2}
<div class="max-w-none mx-auto">
<div class="bg-white overflow-hidden">
{render_card_header(
"Default Alarm Columns",
"Choose the columns displayed on the alarm log and alarm search results screen."
)}
<div class="px-6 py-2">
<InitDefaultAlarmColumns id="default-alarm-columns" drop_zone={Settings.get(:default_alarm_columns)} />
</div>
</div>
</div>
{#match 3}
<div class="max-w-none mx-auto">
<div class="bg-white overflow-hidden">
{render_card_header(
"Alarm Categories",
"Label and order alarm categories. Categories can be defined as a combination of single numbers, lists of numbers or ranges. There can be no overlap between categories. For example, '1', '100-199', '1, 2, 5' and '10, 100-199, 1001' are all valid. To avoid importing entire categories of alarms, uncheck LOG."
)}
<div class="px-6 py-2">
<AlarmCategoriesSettings id="alarm-categories-settings" show_sidebar={@live_action == :edit_alarm_category} />
</div>
</div>
</div>
{#match _}
{render_done()}
{/case}
</div>
</div>
</div>
</div>
</div>
{#unless @step == 0}
<div class="flex items-center justify-end p-4 w-full bg-gray-50">
<Button click="change_step" values={step: @step - 1}>
Back
</Button>
<Button color="green" click="change_step" values={step: @step + 1} left_margin>
{#if @step < @steps}
Next
{#else}
Done
{/if}
</Button>
</div>
</div>
"""
end
workaround: replace with {#if ...}
The extension works great.
I was just wondering if there's an option to automate format on save on VSCode (like the elixir format that runs with elixir-ls).
Would it be possible to have it as an option in the surface VSCode extension?
Running mix surface.format will change <template>
to <#template>
. For me, that change results in a compilation error because it's changing a template tag that I'm using for AlpineJS.
1.13.0
24.1.6
0.6.1
0.7.1
mix format failed for file: lib/hygeia_web/live/statistics_live/export.sface
** (FunctionClauseError) no function clause matching in Inspect.Algebra.format/2
The following arguments were given to Inspect.Algebra.format/2:
# 1
{:doc_group, {:doc_cons, {:doc_string, "gettext", 7}, {:doc_group, {:doc_cons, {:doc_cons, {:doc_cons, {:doc_nest, {:doc_cons, "(", {:doc_break, "", :strict}}, 2, :break}, {:doc_cons, {:doc_nest, {:doc_cons, {:doc_cons, "\"", {:doc_string, "From", 4}}, "\""}, 2, :break}, {:doc_break, "", :strict}}}, ")"}, :doc_nil}, :self}}, :self}
# 2
nil
Attempted function clauses (showing 1 out of 1):
def format(doc, width) when is_binary(doc) or doc === :doc_nil or doc === :doc_line or is_tuple(doc) and elem(doc, 0) === :doc_break or elem(doc, 0) === :doc_collapse or elem(doc, 0) === :doc_color or elem(doc, 0) === :doc_cons or elem(doc, 0) === :doc_fits or elem(doc, 0) === :doc_force or elem(doc, 0) === :doc_group or elem(doc, 0) === :doc_nest or elem(doc, 0) === :doc_string and width == :infinity or is_integer(width) and width >= 0
(elixir 1.13.0) lib/inspect/algebra.ex:958: Inspect.Algebra.format/2
lib/surface/formatter/phases/render.ex:38: Surface.Formatter.Phases.Render.render_node/2
(elixir 1.13.0) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
lib/surface/formatter/phases/render.ex:183: Surface.Formatter.Phases.Render.render_node/2
(elixir 1.13.0) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
(elixir 1.13.0) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
lib/surface/formatter/phases/render.ex:183: Surface.Formatter.Phases.Render.render_node/2
(elixir 1.13.0) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
See in CI: https://github.com/jshmrtn/hygeia/runs/4412768200?check_suite_focus=true
[
import_deps: [:phoenix, :phoenix_live_view, :surface, :ecto, :ecto_enum],
inputs: [
".dialyzer_ignore.exs",
".formatter.exs",
"*.{ex,exs}",
"priv/*/seeds.exs",
"priv/repo/seeds/*.exs",
"{config,lib,test}/**/*.{ex,exs,sface}"
],
subdirectories: ["priv/*/migrations"],
plugins: [Surface.Formatter.Plugin]
]
{/unless}
is being removed by mix surface.format
render
function with an {#unless}...{/unless}
block in the ~F
sigil.mix surface.format
{/unless}
is removedI expect the formatter to not create compile errors.
Surface: v0.5.0
LiveView: v0.15.7
Elixir: v1.12.1
it turned my <script> tag into this:
<script>
document.addEventListener('alpine:init', () => {
console.log('apline inited');
Alpine.data('columnFilterOptions', () => ({
all: true,
open: false,
options: [],
initialSelection: [],
selected: [],
available: [],
selectAll() {
this.selected = this.options;
},
selectNone() {
this.selected = [];
},
clear() {
this.open = false;
this.selected = this.initialSelection = this.options;
},
cancel() {
this.open = false;
this.selected = this.initialSelection;
},
apply() {
this.open = false;
this.initialSelection = this.selected;
},
init() {
console.log('col filt init')
this.$nextTick(() => {
this.$watch('selected', (value) => {
if (this.options.every((x) => { return this.selected.includes(x) })) {
this.all = true;
} else {
this.all = false;
}
})
})
}
}))
})
</script>
Should it ignore contents of script tags or use a JS formatter?
In the following code-snippet, the formatter converts {=class}
into {=@class}
even when {=class}
was intended. This is the case for all props
that are bound in the render
scope.
defmodule Lotus.Span do
use Surface.Component
prop class, :css_class, default: []
prop extra_class, :css_class, default: []
slot default
def render(assigns) do
class = assigns.class ++ assigns.extra_class
~F"""
<span {=class}>
<#slot />
</span>
"""
end
end
Hi,
I thought I'd try with v0.6.0 but it is still moving stuff up onto the previous line. I feel like it worked when I first tried it but lately it is shifting comments and other tags up onto the end of the line above it. As you can see in the diff below, the commented out div
, GotoTime
and the start of the surface comment {!--
all disappeared at the end of the AlarmDivisionFilterDropdown
. And for some reason it put the <button ...
lines onto the one line.
I'm on Elixir 1.12.3, OTP 24, surface & formatter 0.6 and Windows 10, though I use LF
line endings.
The problem is new in 0.7.3
, it worked in 0.7.2
.
Update PR in our project: jshmrtn/hygeia#1036
Failed CI (detailed error): https://github.com/jshmrtn/hygeia/runs/4584434083?check_suite_focus=true
File with Problems: https://github.com/jshmrtn/hygeia/blob/master/lib/hygeia_web/live/organisation_live/merge.sface
Detailed Error:
mix format --check-formatted
mix format failed for file: lib/hygeia_web/live/organisation_live/merge.sface
** (FunctionClauseError) no function clause matching in Surface.Formatter.Phases.Render.quoted_strings_with_newlines/1
The following arguments were given to Surface.Formatter.Phases.Render.quoted_strings_with_newlines/1:
# 1
{:unless, [line: 1], [{:is_nil, [line: 1], [{:@, [line: 1], [{:delete, [line: 1], nil}]}]}, [do: {{:., [line: 1], [{:@, [line: 1], [{:delete, [line: 1], nil}]}, :uuid]}, [no_parens: true, line: 1], []}]]}
Attempted function clauses (showing 2 out of 2):
defp quoted_strings_with_newlines(+attribute_expression+) when -is_binary(attribute_expression)-
defp quoted_strings_with_newlines(+nodes+) when -is_list(nodes)-
lib/surface/formatter/phases/render.ex:377: Surface.Formatter.Phases.Render.quoted_strings_with_newlines/1
Warning: lib/surface/formatter/phases/render.ex:445: Surface.Formatter.Phases.Render.format_attribute_expression/2
lib/surface/formatter/phases/render.ex:371: Surface.Formatter.Phases.Render.render_attribute/2
Warning: (elixir 1.13.0) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
(elixir 1.13.0) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
Warning: lib/surface/formatter/phases/render.ex:203: Surface.Formatter.Phases.Render.render_opening_tag/3
(elixir 1.13.0) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
Warning: (elixir 1.13.0) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
Error: Process completed with exit code 1.
The issue would be in the future but I've a library that integrate surface_formatter to mix format plugin at this repository. It would be great if it can merge into surface_formatter repo in the future. ^^
Example:
# this div exists for creating space inside display: flex
<div class="flex-1"></div>
is formatted as
<div class="flex-1" />
According to the W3C validator, self-closing div
s are not valid. (https://validator.w3.org/nu/#textarea)
Maybe self-closing should be done only for whitelisted tags?
I formatted surface_catalogue using the main surface branch and found this change weird.
Before
<div class="code" style={"width: #{example.code_perc}%"}>
<pre class="language-surface">
<code class="content language-surface" phx-hook="Highlight" id={"example-code-#{index}-#{example.func}"}>
{example.code}</code>
</pre>
</div>
After
<div class="code" style={"width: #{example.code_perc}%"}>
<pre class="language-surface">
<code
class="content language-surface"
phx-hook="Highlight"
id={"example-code-#{index}-#{example.func}"}
>
{example.code}</code>
</pre>
</div>
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.