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 = "#{tag}>"
+
+ "#{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()