Skip to content

Commit

Permalink
Begin prototype for Surface formatter
Browse files Browse the repository at this point in the history
  • Loading branch information
paulstatezny committed Dec 23, 2020
0 parents commit a15fb27
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
27 changes: 27 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
surface_formatter-*.tar


# Temporary files for e.g. tests
/tmp
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SurfaceFormatter

**TODO: Add description**

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `surface_formatter` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:surface_formatter, "~> 0.1.0"}
]
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/surface_formatter](https://hexdocs.pm/surface_formatter).

136 changes: 136 additions & 0 deletions lib/mix/tasks/surface_format.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
defmodule Mix.Tasks.SurfaceFormat do
use Mix.Task

@typedoc "A node output by &Surface.Compiler.Parser.parse/1"
@type parsed_surface_node :: term

@typedoc "An HTML/Surface tag"
@type tag :: String.t

@typedoc """
An HTML/Surface attribute string, such as `class="container"`,
`width=6`, or `items={{ @cart_items }}`
"""
@type attribute :: String.t

@typedoc "Children of an HTML element"
@type children :: list(code_segment)

@typedoc "A segment of HTML that can be rendered given a tab level"
@type code_segment :: String.t | {tag, list(attribute), children}

# Use 2 spaces for a tab
@tab " "

# Line length of opening tags before splitting attributes onto their own line
@max_line_length 80

def run(_) do
~S"""
<div class="container text-xl " data-id="6" UPCASE-ATTR="upcased">
before the span
<span class="age">6 years</span>
after the span
<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" }}>
<span>Dedented</span>
Inside SomeComponent
{{1 + 1}} {{Foo.bar("baz ", qux )}}
</SomeComponent>
</div>
"""
|> Surface.Compiler.Parser.parse()
|> elem(1)
|> Enum.map(&code_segment/1)
|> Enum.map(&render/1)
|> List.flatten()
|> Enum.join("\n")
|> IO.puts()
end

@spec code_segment(parsed_surface_node) :: code_segment
defp code_segment({:interpolation, expression, _meta}) do
"{{ #{Code.format_string!(expression)} }}"
end

defp code_segment(html) when is_binary(html) do
String.trim(html)
end

defp code_segment({tag, attributes, children, _meta}) do
rendered_attributes = Enum.map(attributes, fn
{name, value, _meta} when is_binary(value) ->
"#{name}=\"#{String.trim(value)}\""

{name, value, _meta} when is_boolean(value) ->
"#{name}=#{value}"

{name, value, _meta} when is_number(value) ->
"#{name}=#{value}"

{name, {:attribute_expr, expression, _expr_meta}, _meta} when is_binary(expression) ->
"#{name}={{ #{Code.format_string!(expression)} }}"
end)

{
tag,
rendered_attributes,
Enum.map(children, &code_segment/1)
}
end

@spec render(code_segment) :: String.t | nil
defp render(segment, depth \\ 0)
defp render(segment, _depth) when segment in ["", "\n"] do
nil
end

defp render(segment, depth) when is_binary(segment) do
String.duplicate(@tab, depth) <> segment
end

defp render({tag, attributes, children}, depth) do
indentation = String.duplicate(@tab, depth)

joined_attributes = case attributes do
[] -> ""
_ -> " " <> Enum.join(attributes, " ")
end

opening = "<" <> tag <> joined_attributes <> ">"

opening =
if String.length(opening) > @max_line_length do
indented_attributes =
attributes
|> Enum.map(&indent(&1, depth + 1))

[
"<#{tag}",
indented_attributes,
"#{indentation}>"
]
|> List.flatten()
|> Enum.join("\n")
else
opening
end

rendered_children =
children
|> Enum.map(& render(&1, depth + 1))
|> List.flatten()
# Remove nils
|> Enum.filter(&Function.identity/1)
|> Enum.join("\n\n")

closing = "</#{tag}>"

"#{indentation}#{opening}\n#{rendered_children}\n#{indentation}#{closing}"
end

defp indent(string, depth), do: "#{String.duplicate(@tab, depth)}#{string}"
end
18 changes: 18 additions & 0 deletions lib/surface_formatter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule SurfaceFormatter do
@moduledoc """
Documentation for `SurfaceFormatter`.
"""

@doc """
Hello world.
## Examples
iex> SurfaceFormatter.hello()
:world
"""
def hello do
:world
end
end
27 changes: 27 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule SurfaceFormatter.MixProject do
use Mix.Project

def project do
[
app: :surface_formatter,
version: "0.1.0",
elixir: "~> 1.11",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:surface, "~> 0.1.1"}
]
end
end
15 changes: 15 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
%{
"earmark": {:hex, :earmark, "1.4.13", "2c6ce9768fc9fdbf4046f457e207df6360ee6c91ee1ecb8e9a139f96a4289d91", [:mix], [{:earmark_parser, ">= 1.4.12", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "a0cf3ed88ef2b1964df408889b5ecb886d1a048edde53497fc935ccd15af3403"},
"earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"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"},
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
"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"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"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"},
"plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"},
"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"},
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
}
8 changes: 8 additions & 0 deletions test/surface_formatter_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule SurfaceFormatterTest do
use ExUnit.Case
doctest SurfaceFormatter

test "greets the world" do
assert SurfaceFormatter.hello() == :world
end
end
1 change: 1 addition & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()

0 comments on commit a15fb27

Please sign in to comment.