Surface.Formatter (SurfaceFormatter v0.4.1) View Source
Functions for formatting Surface code snippets.
Link to this section Summary
Types
A parsed HTML/Component attribute name and value.
The value of a parsed HTML/Component attribute.
A node that will ultimately be sent to Surface.Formatter.Render.node/2
for rendering.
Options that can be passed to Surface.Formatter.format_string!/2
.
A node output by Surface.Compiler.Parser.parse
.
The name of an HTML/Surface tag, such as div
, ListItem
, or #Markdown
.
Whitespace nodes that can be rendered by Surface.Formatter.Render.node/2
.
Functions
Formats the given Surface code string. (Typically the contents of an ~H
sigil or .sface
file.)
Returns true if the argument is an element (HTML element or surface component), false otherwise.
Given a tag, return whether to render the contens verbatim instead of formatting them. Specifically, don't modify contents of macro components or <pre> and <code> tags.
Link to this section Types
Specs
attribute() :: {name :: String.t(), attribute_value(), term()}
A parsed HTML/Component attribute name and value.
Specs
attribute_value() :: integer() | boolean() | String.t() | {:attribute_expr, interpolated_expression :: String.t(), term()} | [String.t()]
The value of a parsed HTML/Component attribute.
Specs
formatter_node() :: surface_node() | whitespace()
A node that will ultimately be sent to Surface.Formatter.Render.node/2
for rendering.
The output of Surface.Compiler.Parser.parse
is ran through the various formatting
phases, which ultimately output a tree of this type.
Specs
Options that can be passed to Surface.Formatter.format_string!/2
.
:line_length
- Maximum line length before wrapping opening tags:indent
- Starting indentation depth depending on the context of the ~H sigil
Specs
surface_node() :: String.t() | {:interpolation, String.t(), map()} | {tag(), [attribute()], [surface_node()], map()}
A node output by Surface.Compiler.Parser.parse
.
Specs
tag() :: String.t()
The name of an HTML/Surface tag, such as div
, ListItem
, or #Markdown
.
Specs
whitespace() :: :newline | :space | :indent | :indent_one_less
Whitespace nodes that can be rendered by Surface.Formatter.Render.node/2
.
The Surface parser does not return these, but formatter phases introduce these nodes in preparation for rendering.
:newline
adds a newline (\n
) character:space
adds a space () character
:indent
adds spaces at the appropriate indentation amount:indent_one_less
adds spaces at 1 indentation level removed (used for closing tags)
Link to this section Functions
Specs
Formats the given Surface code string. (Typically the contents of an ~H
sigil or .sface
file.)
In short:
- HTML/Surface elements are indented to the right of their parents.
- Attributes are split on multiple lines if the line is too long; otherwise on the same line.
- Elixir code snippets (inside
{{ }}
) are ran through the Elixir code formatter. - Lack of whitespace is preserved, so that intended behaviors are not removed.
(For example,
<span>Foo bar baz</span>
will not have newlines or spaces added.)
Below the Options section is a non-exhaustive list of behaviors of the formatter.
Options
:line_length
- the line length to aim for when formatting the document. Defaults to 98. As with the Elixir formatter, this value is used as reference but is not always enforced depending on the context.
Indentation
The formatter ensures that children are indented one tab (two spaces) in from their parent.
Whitespace
Whitespace that exists
As in regular HTML, any string of continuous whitespace is considered equivalent to any other string of continuous whitespace. There are three exceptions:
- Macro components (with names starting with
#
, such as<#Markdown>
) <pre>
tags<code>
tags
The contents of those tags are considered whitespace-sensitive, and developers should sanity check after running the formatter.
Whitespace that doesn't exist (Lack of whitespace)
As is sometimes the case in HTML, lack of whitespace is considered significant. Instead of attempting to determine which contexts matter, the formatter consistently retains lack of whitespace. This means that the following
<div><p>Hello</p></div>
will not be changed. However, the following
<div> <p> Hello </p> </div>
will be formatted as
<div>
<p>
Hello
</p>
</div>
because of the whitespace on either side of each tag.
To be clear, this example
<div> <p>Hello</p> </div>
will be formatted as
<div>
<p>Hello</p>
</div>
because of the lack of whitespace in between the opening and closing <p>
tags
and their child content.
Splitting children onto separate lines
In certain scenarios, the formatter will move nodes to their own line:
(Below, "element" means an HTML element or a Surface component.)
- If an element contains other elements as children, it will be surrounded by newlines.
- If there is a space after an opening tag or before a closing tag, it is converted to a newline.
- If a closing tag is put on its own line, the formatter ensures there's a newline before the next sibling node.
Since SurfaceFormatter doesn't know if a component represents an inline or block element, it does not currently make distinctions between elements that should or should not be moved onto their own lines, other than the above rules.
This allows inline elements to be placed among text without splitting them onto their own lines:
The <b>Dialog</b> is a stateless component. All event handlers
had to be defined in the parent <b>LiveView</b>.
Newline characters
The formatter will not add extra newlines unprompted beyond moving nodes onto their own line. However, if the input code has extra newlines, the formatter will retain them but will collapse more than one extra newline into a single one.
This means that
<p>Hello</p>
<p>Goodbye</p>
will be formatted as
<p>Hello</p>
<p>Goodbye</p>
Attributes
HTML attributes such as class
in <p class="container">
are formatted to
make use of Surface features.
Inline literals
String, integer, and boolean literals are placed after the =
without any
interpolation brackets ({{ }}
). This means that
<Component foo={{ "hello" }} bar={{123}} secure={{ false }} />
will be formatted as
<Component foo="hello" bar=123 secure=false />
One exception is that true
boolean literals are formatted using the Surface
shorthand whereby you can simply write the name of the attribute and it is
passed in as true
. For example,
<Component secure={{ true }} />
and
<Component secure=true />
will both be formatted as
<Component secure />
Interpolation ({{ }}
brackets)
Attributes that interpolate Elixir code with {{ }}
brackets are ran through
the Elixir code formatter.
This means that:
<Foo num=123456 />
becomes<Foo num=123_456 />
list={{[1,2,3]}}
becomeslist={{ [1, 2, 3] }}
things={{ %{one: "1", two: "2"}}}
becomesthings={{ %{ one: "1", two: "2" } }}
Sometimes the Elixir code formatter will add line breaks in the formatted expression. In that case, SurfaceFormatter will ensure indentation lines up. If there is a single attribute, it will keep the attribute on the same line as the tag name, for example:
<Component list={{[
{"foo", foo},
{"bar", bar}
]}} />
However, if there are multiple attributes it will put them on separate lines:
<Child
list={{[
{"foo", foo},
{"bar", bar}
]}}
int=123
/>
Note in the above example that if the Elixir code formatter introduces
newlines, whitespace between the expression and the interpolation brackets is
collapsed. That is to say the formatter will emit list={{[
instead of
list={{ [
.
Wrapping attributes on separate lines
In the Interpolation ({{ }}
brackets) section we noted that attributes
will each be put on their own line if there is more than one attribute and at
least one contains a newline after being formatted by the Elixir code
formatter.
There is another scenario where attributes will each be given their own line:
any time the opening tag would exceed line_length
if put on one line.
This value is provided in .formatter.exs
and defaults to 98.
The formatter indents attributes one tab in from the start of the opening tag for readability:
<div
class="very long class value that causes this to exceed the established line length"
aria-role="button"
>
If you desire to have a separate line length for mix format
and mix surface.format
,
provide surface_line_length
in .formatter.exs
and it will be given precedence
when running mix surface.format
. For example:
# .formatter.exs
[
surface_line_length: 120,
import_deps: [...],
# ...
]
As with all changes (for both mix format
and mix surface.format
) it's
recommended that developers don't blindly run the formatter on an entire
codebase and commit, but instead sanity check each file to ensure the results
are desired.
Specs
is_element?(surface_node()) :: boolean()
Returns true if the argument is an element (HTML element or surface component), false otherwise.
Specs
Given a tag, return whether to render the contens verbatim instead of formatting them. Specifically, don't modify contents of macro components or <pre> and <code> tags.