Skip to content

Commit a15fb27

Browse files
committed
Begin prototype for Surface formatter
0 parents  commit a15fb27

File tree

9 files changed

+257
-0
lines changed

9 files changed

+257
-0
lines changed

.formatter.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

.gitignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
surface_formatter-*.tar
24+
25+
26+
# Temporary files for e.g. tests
27+
/tmp

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# SurfaceFormatter
2+
3+
**TODO: Add description**
4+
5+
## Installation
6+
7+
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
8+
by adding `surface_formatter` to your list of dependencies in `mix.exs`:
9+
10+
```elixir
11+
def deps do
12+
[
13+
{:surface_formatter, "~> 0.1.0"}
14+
]
15+
end
16+
```
17+
18+
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
19+
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
20+
be found at [https://hexdocs.pm/surface_formatter](https://hexdocs.pm/surface_formatter).
21+

lib/mix/tasks/surface_format.ex

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
defmodule Mix.Tasks.SurfaceFormat do
2+
use Mix.Task
3+
4+
@typedoc "A node output by &Surface.Compiler.Parser.parse/1"
5+
@type parsed_surface_node :: term
6+
7+
@typedoc "An HTML/Surface tag"
8+
@type tag :: String.t
9+
10+
@typedoc """
11+
An HTML/Surface attribute string, such as `class="container"`,
12+
`width=6`, or `items={{ @cart_items }}`
13+
"""
14+
@type attribute :: String.t
15+
16+
@typedoc "Children of an HTML element"
17+
@type children :: list(code_segment)
18+
19+
@typedoc "A segment of HTML that can be rendered given a tab level"
20+
@type code_segment :: String.t | {tag, list(attribute), children}
21+
22+
# Use 2 spaces for a tab
23+
@tab " "
24+
25+
# Line length of opening tags before splitting attributes onto their own line
26+
@max_line_length 80
27+
28+
def run(_) do
29+
~S"""
30+
<div class="container text-xl " data-id="6" UPCASE-ATTR="upcased">
31+
before the span
32+
33+
34+
35+
36+
<span class="age">6 years</span>
37+
after the span
38+
<SomeComponent int_prop=6 expr_int_prop={{ 6 }} bool_prop=false string_prop="some-string" map_prop={{ %{ima: "map", avery: "long_map", thattakesup: "alottaspace"} }} interpolated_prop={{ "#{some_var} asdf" }}>
39+
<span>Dedented</span>
40+
Inside SomeComponent
41+
{{1 + 1}} {{Foo.bar("baz ", qux )}}
42+
</SomeComponent>
43+
</div>
44+
"""
45+
|> Surface.Compiler.Parser.parse()
46+
|> elem(1)
47+
|> Enum.map(&code_segment/1)
48+
|> Enum.map(&render/1)
49+
|> List.flatten()
50+
|> Enum.join("\n")
51+
|> IO.puts()
52+
end
53+
54+
@spec code_segment(parsed_surface_node) :: code_segment
55+
defp code_segment({:interpolation, expression, _meta}) do
56+
"{{ #{Code.format_string!(expression)} }}"
57+
end
58+
59+
defp code_segment(html) when is_binary(html) do
60+
String.trim(html)
61+
end
62+
63+
defp code_segment({tag, attributes, children, _meta}) do
64+
rendered_attributes = Enum.map(attributes, fn
65+
{name, value, _meta} when is_binary(value) ->
66+
"#{name}=\"#{String.trim(value)}\""
67+
68+
{name, value, _meta} when is_boolean(value) ->
69+
"#{name}=#{value}"
70+
71+
{name, value, _meta} when is_number(value) ->
72+
"#{name}=#{value}"
73+
74+
{name, {:attribute_expr, expression, _expr_meta}, _meta} when is_binary(expression) ->
75+
"#{name}={{ #{Code.format_string!(expression)} }}"
76+
end)
77+
78+
{
79+
tag,
80+
rendered_attributes,
81+
Enum.map(children, &code_segment/1)
82+
}
83+
end
84+
85+
@spec render(code_segment) :: String.t | nil
86+
defp render(segment, depth \\ 0)
87+
defp render(segment, _depth) when segment in ["", "\n"] do
88+
nil
89+
end
90+
91+
defp render(segment, depth) when is_binary(segment) do
92+
String.duplicate(@tab, depth) <> segment
93+
end
94+
95+
defp render({tag, attributes, children}, depth) do
96+
indentation = String.duplicate(@tab, depth)
97+
98+
joined_attributes = case attributes do
99+
[] -> ""
100+
_ -> " " <> Enum.join(attributes, " ")
101+
end
102+
103+
opening = "<" <> tag <> joined_attributes <> ">"
104+
105+
opening =
106+
if String.length(opening) > @max_line_length do
107+
indented_attributes =
108+
attributes
109+
|> Enum.map(&indent(&1, depth + 1))
110+
111+
[
112+
"<#{tag}",
113+
indented_attributes,
114+
"#{indentation}>"
115+
]
116+
|> List.flatten()
117+
|> Enum.join("\n")
118+
else
119+
opening
120+
end
121+
122+
rendered_children =
123+
children
124+
|> Enum.map(& render(&1, depth + 1))
125+
|> List.flatten()
126+
# Remove nils
127+
|> Enum.filter(&Function.identity/1)
128+
|> Enum.join("\n\n")
129+
130+
closing = "</#{tag}>"
131+
132+
"#{indentation}#{opening}\n#{rendered_children}\n#{indentation}#{closing}"
133+
end
134+
135+
defp indent(string, depth), do: "#{String.duplicate(@tab, depth)}#{string}"
136+
end

lib/surface_formatter.ex

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
defmodule SurfaceFormatter do
2+
@moduledoc """
3+
Documentation for `SurfaceFormatter`.
4+
"""
5+
6+
@doc """
7+
Hello world.
8+
9+
## Examples
10+
11+
iex> SurfaceFormatter.hello()
12+
:world
13+
14+
"""
15+
def hello do
16+
:world
17+
end
18+
end

mix.exs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
defmodule SurfaceFormatter.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :surface_formatter,
7+
version: "0.1.0",
8+
elixir: "~> 1.11",
9+
start_permanent: Mix.env() == :prod,
10+
deps: deps()
11+
]
12+
end
13+
14+
# Run "mix help compile.app" to learn about applications.
15+
def application do
16+
[
17+
extra_applications: [:logger]
18+
]
19+
end
20+
21+
# Run "mix help deps" to learn about dependencies.
22+
defp deps do
23+
[
24+
{:surface, "~> 0.1.1"}
25+
]
26+
end
27+
end

mix.lock

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
%{
2+
"earmark": {:hex, :earmark, "1.4.13", "2c6ce9768fc9fdbf4046f457e207df6360ee6c91ee1ecb8e9a139f96a4289d91", [:mix], [{:earmark_parser, ">= 1.4.12", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "a0cf3ed88ef2b1964df408889b5ecb886d1a048edde53497fc935ccd15af3403"},
3+
"earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"},
4+
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
5+
"mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"},
6+
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
7+
"phoenix": {:hex, :phoenix, "1.5.7", "2923bb3af924f184459fe4fa4b100bd25fa6468e69b2803dfae82698269aa5e0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "774cd64417c5a3788414fdbb2be2eb9bcd0c048d9e6ad11a0c1fd67b7c0d0978"},
8+
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
9+
"phoenix_live_view": {:hex, :phoenix_live_view, "0.15.1", "052ecf12c7bc19d5d4942d3a9503f23c5c6da879d71c76080f118a7935622471", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "040c285d3e22757157a2ade6995ad96816214a907d170f067686aeb1fc018439"},
10+
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
11+
"plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"},
12+
"plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"},
13+
"surface": {:hex, :surface, "0.1.1", "812249c5832d1b36051101a3fe68eac374758cc4437652bf9bf2cb14ab052fb7", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "08b2c00ad1a3c6d0f60fa05bda43aa90b112f71e7989870a6e756670fdc1698c"},
14+
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
15+
}

test/surface_formatter_test.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule SurfaceFormatterTest do
2+
use ExUnit.Case
3+
doctest SurfaceFormatter
4+
5+
test "greets the world" do
6+
assert SurfaceFormatter.hello() == :world
7+
end
8+
end

test/test_helper.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ExUnit.start()

0 commit comments

Comments
 (0)