From a15fb27258cefadca521fc883d70ab29b4df7a5e Mon Sep 17 00:00:00 2001 From: Paul Statezny Date: Wed, 23 Dec 2020 10:27:07 -0700 Subject: [PATCH] Begin prototype for Surface formatter --- .formatter.exs | 4 + .gitignore | 27 +++++++ README.md | 21 +++++ lib/mix/tasks/surface_format.ex | 136 ++++++++++++++++++++++++++++++++ lib/surface_formatter.ex | 18 +++++ mix.exs | 27 +++++++ mix.lock | 15 ++++ test/surface_formatter_test.exs | 8 ++ test/test_helper.exs | 1 + 9 files changed, 257 insertions(+) create mode 100644 .formatter.exs create mode 100644 .gitignore create mode 100644 README.md create mode 100644 lib/mix/tasks/surface_format.ex create mode 100644 lib/surface_formatter.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 test/surface_formatter_test.exs create mode 100644 test/test_helper.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ba1a27 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..68fbbbe --- /dev/null +++ b/README.md @@ -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). + diff --git a/lib/mix/tasks/surface_format.ex b/lib/mix/tasks/surface_format.ex new file mode 100644 index 0000000..14ee14c --- /dev/null +++ b/lib/mix/tasks/surface_format.ex @@ -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""" +
+ before the span + + + + + 6 years + after the span + + Dedented + Inside SomeComponent + {{1 + 1}} {{Foo.bar("baz ", qux )}} + +
+ """ + |> 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 = "" + + "#{indentation}#{opening}\n#{rendered_children}\n#{indentation}#{closing}" + end + + defp indent(string, depth), do: "#{String.duplicate(@tab, depth)}#{string}" +end diff --git a/lib/surface_formatter.ex b/lib/surface_formatter.ex new file mode 100644 index 0000000..e720c1f --- /dev/null +++ b/lib/surface_formatter.ex @@ -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 diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..d4477d6 --- /dev/null +++ b/mix.exs @@ -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 diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..0133860 --- /dev/null +++ b/mix.lock @@ -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"}, +} diff --git a/test/surface_formatter_test.exs b/test/surface_formatter_test.exs new file mode 100644 index 0000000..93cc3bd --- /dev/null +++ b/test/surface_formatter_test.exs @@ -0,0 +1,8 @@ +defmodule SurfaceFormatterTest do + use ExUnit.Case + doctest SurfaceFormatter + + test "greets the world" do + assert SurfaceFormatter.hello() == :world + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()