Skip to content

Commit 9ad548c

Browse files
authored
add option to treat custom components as inline (#3795)
* add option to treat custom components as inline Relates to: #3567 * only add inline_matcher for now
1 parent 5dc26f0 commit 9ad548c

File tree

2 files changed

+84
-41
lines changed

2 files changed

+84
-41
lines changed

lib/phoenix_live_view/html_formatter.ex

Lines changed: 57 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ defmodule Phoenix.LiveView.HTMLFormatter do
6060
]
6161
```
6262
63+
* `:inline_matcher` - a list of regular expressions to determine if a component
64+
should be treated as inline.
65+
Defaults to `[~r/link/, ~r/button/]`, which treats any component with `link
66+
or `button` in its name as inline.
67+
Can be disabled by setting it to an empty list.
68+
6369
## Formatting
6470
6571
This formatter tries to be as consistent as possible with the Elixir formatter.
@@ -214,10 +220,6 @@ defmodule Phoenix.LiveView.HTMLFormatter do
214220
mark meter noscript object output picture progress q ruby s samp select slot
215221
small span strong sub sup svg template textarea time u tt var video wbr)
216222

217-
@inline_components ~w(.link)
218-
219-
@inline_elements @inline_tags ++ @inline_components
220-
221223
# Default line length to be used in case nothing is specified in the `.formatter.exs` options.
222224
@default_line_length 98
223225

@@ -235,6 +237,7 @@ defmodule Phoenix.LiveView.HTMLFormatter do
235237
else
236238
line_length = opts[:heex_line_length] || opts[:line_length] || @default_line_length
237239
newlines = :binary.matches(source, ["\r\n", "\n"])
240+
inline_matcher = opts[:inline_matcher] || [~r/link/, ~r/button/]
238241

239242
opts =
240243
Keyword.update(opts, :attribute_formatters, %{}, fn formatters ->
@@ -251,7 +254,11 @@ defmodule Phoenix.LiveView.HTMLFormatter do
251254
formatted =
252255
source
253256
|> tokenize()
254-
|> to_tree([], [], {source, newlines})
257+
|> to_tree([], [], %{
258+
source: {source, newlines},
259+
inline_elements: @inline_tags,
260+
inline_matcher: inline_matcher
261+
})
255262
|> case do
256263
{:ok, nodes} ->
257264
nodes
@@ -413,88 +420,88 @@ defmodule Phoenix.LiveView.HTMLFormatter do
413420
# ]}
414421
# ]
415422
# ```
416-
defp to_tree([], buffer, [], _source) do
423+
defp to_tree([], buffer, [], _opts) do
417424
{:ok, Enum.reverse(buffer)}
418425
end
419426

420-
defp to_tree([], _buffer, [{name, _, %{line: line, column: column}, _} | _], _source) do
427+
defp to_tree([], _buffer, [{name, _, %{line: line, column: column}, _} | _], _opts) do
421428
message = "end of template reached without closing tag for <#{name}>"
422429
{:error, line, column, message}
423430
end
424431

425-
defp to_tree([{:text, text, %{context: [:comment_start]}} | tokens], buffer, stack, source) do
426-
to_tree(tokens, [], [{:comment, text, buffer} | stack], source)
432+
defp to_tree([{:text, text, %{context: [:comment_start]}} | tokens], buffer, stack, opts) do
433+
to_tree(tokens, [], [{:comment, text, buffer} | stack], opts)
427434
end
428435

429436
defp to_tree(
430437
[{:text, text, %{context: [:comment_end | _rest]}} | tokens],
431438
buffer,
432439
[{:comment, start_text, upper_buffer} | stack],
433-
source
440+
opts
434441
) do
435442
buffer = Enum.reverse([{:text, String.trim_trailing(text), %{}} | buffer])
436443
text = {:text, String.trim_leading(start_text), %{}}
437-
to_tree(tokens, [{:html_comment, [text | buffer]} | upper_buffer], stack, source)
444+
to_tree(tokens, [{:html_comment, [text | buffer]} | upper_buffer], stack, opts)
438445
end
439446

440447
defp to_tree(
441448
[{:text, text, %{context: [:comment_start, :comment_end]}} | tokens],
442449
buffer,
443450
stack,
444-
source
451+
opts
445452
) do
446453
meta = %{
447454
newlines_before_text: count_newlines_before_text(text),
448455
newlines_after_text: count_newlines_after_text(text)
449456
}
450457

451-
to_tree(tokens, [{:html_comment, [{:text, String.trim(text), meta}]} | buffer], stack, source)
458+
to_tree(tokens, [{:html_comment, [{:text, String.trim(text), meta}]} | buffer], stack, opts)
452459
end
453460

454-
defp to_tree([{:text, text, _meta} | tokens], buffer, stack, source) do
461+
defp to_tree([{:text, text, _meta} | tokens], buffer, stack, opts) do
455462
buffer = may_set_preserve_on_block(buffer, text)
456463

457464
if line_html_comment?(text) do
458-
to_tree(tokens, [{:comment, text} | buffer], stack, source)
465+
to_tree(tokens, [{:comment, text} | buffer], stack, opts)
459466
else
460467
meta = %{newlines: count_newlines_before_text(text)}
461-
to_tree(tokens, [{:text, text, meta} | buffer], stack, source)
468+
to_tree(tokens, [{:text, text, meta} | buffer], stack, opts)
462469
end
463470
end
464471

465-
defp to_tree([{:body_expr, value, meta} | tokens], buffer, stack, source) do
472+
defp to_tree([{:body_expr, value, meta} | tokens], buffer, stack, opts) do
466473
buffer = set_preserve_on_block(buffer)
467-
to_tree(tokens, [{:body_expr, value, meta} | buffer], stack, source)
474+
to_tree(tokens, [{:body_expr, value, meta} | buffer], stack, opts)
468475
end
469476

470-
defp to_tree([{type, _name, attrs, %{closing: _} = meta} | tokens], buffer, stack, source)
477+
defp to_tree([{type, _name, attrs, %{closing: _} = meta} | tokens], buffer, stack, opts)
471478
when is_tag_open(type) do
472-
to_tree(tokens, [{:tag_self_close, meta.tag_name, attrs} | buffer], stack, source)
479+
to_tree(tokens, [{:tag_self_close, meta.tag_name, attrs} | buffer], stack, opts)
473480
end
474481

475-
defp to_tree([{type, _name, attrs, meta} | tokens], buffer, stack, source)
482+
defp to_tree([{type, _name, attrs, meta} | tokens], buffer, stack, opts)
476483
when is_tag_open(type) do
477-
to_tree(tokens, [], [{meta.tag_name, attrs, meta, buffer} | stack], source)
484+
to_tree(tokens, [], [{meta.tag_name, attrs, meta, buffer} | stack], opts)
478485
end
479486

480487
defp to_tree(
481488
[{:close, _type, _name, close_meta} | tokens],
482489
reversed_buffer,
483490
[{tag_name, attrs, open_meta, upper_buffer} | stack],
484-
source
491+
opts
485492
) do
486493
{mode, block} =
487494
cond do
488495
tag_name in ["pre", "textarea"] or contains_special_attrs?(attrs) ->
489496
content =
490-
content_from_source(source, open_meta.inner_location, close_meta.inner_location)
497+
content_from_source(opts.source, open_meta.inner_location, close_meta.inner_location)
491498

492499
{:preserve, [{:text, content, %{newlines: 0}}]}
493500

494501
preceeded_by_non_white_space?(upper_buffer) ->
495502
{:preserve, Enum.reverse(reversed_buffer)}
496503

497-
tag_name in @inline_elements ->
504+
inline?(tag_name, opts.inline_elements, opts.inline_matcher) ->
498505
{:inline,
499506
reversed_buffer
500507
|> may_set_preserve_on_text(:last)
@@ -506,66 +513,71 @@ defmodule Phoenix.LiveView.HTMLFormatter do
506513
end
507514

508515
tag_block = {:tag_block, tag_name, attrs, block, %{mode: mode}}
509-
to_tree(tokens, [tag_block | upper_buffer], stack, source)
516+
to_tree(tokens, [tag_block | upper_buffer], stack, opts)
510517
end
511518

512519
# handle eex
513520

514-
defp to_tree([{:eex_comment, text, _meta} | tokens], buffer, stack, source) do
515-
to_tree(tokens, [{:eex_comment, text} | buffer], stack, source)
521+
defp to_tree([{:eex_comment, text, _meta} | tokens], buffer, stack, opts) do
522+
to_tree(tokens, [{:eex_comment, text} | buffer], stack, opts)
516523
end
517524

518-
defp to_tree([{:eex, :start_expr, expr, meta} | tokens], buffer, stack, source) do
519-
to_tree(tokens, [], [{:eex_block, expr, meta, buffer} | stack], source)
525+
defp to_tree([{:eex, :start_expr, expr, meta} | tokens], buffer, stack, opts) do
526+
to_tree(tokens, [], [{:eex_block, expr, meta, buffer} | stack], opts)
520527
end
521528

522529
defp to_tree(
523530
[{:eex, :middle_expr, middle_expr, _meta} | tokens],
524531
buffer,
525532
[{:eex_block, expr, meta, upper_buffer, middle_buffer} | stack],
526-
source
533+
opts
527534
) do
528535
middle_buffer = [{Enum.reverse(buffer), middle_expr} | middle_buffer]
529-
to_tree(tokens, [], [{:eex_block, expr, meta, upper_buffer, middle_buffer} | stack], source)
536+
to_tree(tokens, [], [{:eex_block, expr, meta, upper_buffer, middle_buffer} | stack], opts)
530537
end
531538

532539
defp to_tree(
533540
[{:eex, :middle_expr, middle_expr, _meta} | tokens],
534541
buffer,
535542
[{:eex_block, expr, meta, upper_buffer} | stack],
536-
source
543+
opts
537544
) do
538545
middle_buffer = [{Enum.reverse(buffer), middle_expr}]
539-
to_tree(tokens, [], [{:eex_block, expr, meta, upper_buffer, middle_buffer} | stack], source)
546+
to_tree(tokens, [], [{:eex_block, expr, meta, upper_buffer, middle_buffer} | stack], opts)
540547
end
541548

542549
defp to_tree(
543550
[{:eex, :end_expr, end_expr, _meta} | tokens],
544551
buffer,
545552
[{:eex_block, expr, meta, upper_buffer, middle_buffer} | stack],
546-
source
553+
opts
547554
) do
548555
block = Enum.reverse([{Enum.reverse(buffer), end_expr} | middle_buffer])
549-
to_tree(tokens, [{:eex_block, expr, block, meta} | upper_buffer], stack, source)
556+
to_tree(tokens, [{:eex_block, expr, block, meta} | upper_buffer], stack, opts)
550557
end
551558

552559
defp to_tree(
553560
[{:eex, :end_expr, end_expr, _meta} | tokens],
554561
buffer,
555562
[{:eex_block, expr, meta, upper_buffer} | stack],
556-
source
563+
opts
557564
) do
558565
block = [{Enum.reverse(buffer), end_expr}]
559-
to_tree(tokens, [{:eex_block, expr, block, meta} | upper_buffer], stack, source)
566+
to_tree(tokens, [{:eex_block, expr, block, meta} | upper_buffer], stack, opts)
560567
end
561568

562-
defp to_tree([{:eex, _type, expr, meta} | tokens], buffer, stack, source) do
569+
defp to_tree([{:eex, _type, expr, meta} | tokens], buffer, stack, opts) do
563570
buffer = set_preserve_on_block(buffer)
564-
to_tree(tokens, [{:eex, expr, meta} | buffer], stack, source)
571+
to_tree(tokens, [{:eex, expr, meta} | buffer], stack, opts)
565572
end
566573

567574
# -- HELPERS
568575

576+
defp inline?(tag_name, inline_elements, inline_matcher) do
577+
tag_name in inline_elements or
578+
Enum.any?(inline_matcher, &Regex.match?(&1, tag_name))
579+
end
580+
569581
defp count_newlines_before_text(binary),
570582
do: count_newlines_until_text(binary, 0, 0, 1)
571583

@@ -665,7 +677,11 @@ defmodule Phoenix.LiveView.HTMLFormatter do
665677
end)
666678
end
667679

668-
defp content_from_source({source, newlines}, {line_start, column_start}, {line_end, column_end}) do
680+
defp content_from_source(
681+
{source, newlines},
682+
{line_start, column_start},
683+
{line_end, column_end}
684+
) do
669685
lines = Enum.slice([{0, 0} | newlines], (line_start - 1)..(line_end - 1))
670686
[first_line | _] = lines
671687
[last_line | _] = Enum.reverse(lines)

test/phoenix_live_view/html_formatter_test.exs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,6 +1515,33 @@ defmodule Phoenix.LiveView.HTMLFormatterTest do
15151515
)
15161516
end
15171517

1518+
test "treats components with link or button in their name as inline" do
1519+
assert_formatter_doesnt_change("""
1520+
<.styled_link> Foo: </.styled_link>
1521+
""")
1522+
1523+
assert_formatter_output(
1524+
"""
1525+
<.styled_link> Foo: </.styled_link>
1526+
""",
1527+
"""
1528+
<.styled_link>Foo:</.styled_link>
1529+
""",
1530+
inline_matcher: []
1531+
)
1532+
1533+
assert_formatter_doesnt_change("""
1534+
<.styled_button_custom> Foo: </.styled_button_custom>
1535+
""")
1536+
1537+
assert_formatter_doesnt_change(
1538+
"""
1539+
<.my_custom_inline_element> Foo: </.my_custom_inline_element>
1540+
""",
1541+
inline_matcher: [~r/inline_element$/]
1542+
)
1543+
end
1544+
15181545
test "does not keep empty lines on script and styles tags" do
15191546
input = """
15201547
<script>

0 commit comments

Comments
 (0)