├── .babelrc ├── .eslintrc.json ├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── js │ ├── index.js │ └── sample_code.js └── scss │ └── app.scss ├── build.sh ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── lib ├── elixir_formatter.ex ├── elixir_formatter │ └── application.ex ├── elixir_formatter_web.ex └── elixir_formatter_web │ ├── channels │ ├── formatter_channel.ex │ └── user_socket.ex │ ├── controllers │ └── page_controller.ex │ ├── endpoint.ex │ ├── gettext.ex │ ├── router.ex │ ├── templates │ ├── layout │ │ └── app.html.eex │ └── page │ │ └── index.html.eex │ └── views │ ├── error_helpers.ex │ ├── error_view.ex │ ├── layout_view.ex │ └── page_view.ex ├── mix.exs ├── mix.lock ├── package-lock.json ├── package.json ├── priv ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot └── static │ ├── cache_manifest.json │ ├── favicon-b0c6f6a1a140812fcd05ddc4e3e563a7.ico │ ├── favicon.ico │ ├── images │ ├── favicons │ │ ├── android-chrome-192x192-2c81ae1e94e111a346b29a15d291aebe.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512-c13725c41f167aaa7e87e51b0589cc31.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-fc27aa2197e2bb7aaf445a22a1e636c1.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16-6b31e7e74ef0ed0ee4da2d7b738d4b21.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32-8666bcf48e0145a94d27e4b38a3652d1.png │ │ ├── favicon-32x32.png │ │ ├── safari-pinned-tab-57cac335744da672578e9d9da25eb737.svg │ │ └── safari-pinned-tab.svg │ ├── logo-256-eb49ec9f2c066c55de82052da611a146.png │ ├── logo-256.png │ ├── logo-512-d3a93f49ca3b706106a746f40c7ed507.png │ └── logo-512.png │ ├── manifest-8d31319e01e16501d2244acb0788d08c.json │ ├── manifest.json │ ├── robots-067185ba27a5d9139b10a759679045bf.txt │ └── robots.txt ├── test ├── elixir_formatter_web │ ├── channels │ │ └── formatter_channel_test.exs │ ├── controllers │ │ └── page_controller_test.exs │ └── views │ │ ├── error_view_test.exs │ │ ├── layout_view_test.exs │ │ └── page_view_test.exs ├── support │ ├── channel_case.ex │ └── conn_case.ex └── test_helper.exs └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "browser": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 6, 8 | "sourceType": "module", 9 | "ecmaFeatures": { 10 | "experimentalObjectRestSpread": true, 11 | "jsx": false 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "lib/**/*.{ex,exs}" 4 | ], 5 | 6 | locals_without_parens: [ 7 | # Formatter tests 8 | assert_format: 2, 9 | assert_format: 3, 10 | assert_same: 1, 11 | assert_same: 2, 12 | 13 | # Errors tests 14 | assert_eval_raise: 3, 15 | 16 | # Mix tests 17 | in_fixture: 2, 18 | in_tmp: 2, 19 | 20 | render: 2, 21 | plug: 1, 22 | plug: 2, 23 | 24 | # socket 25 | channel: 2, 26 | socket: 2, 27 | transport: 2, 28 | ] 29 | ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generated on crash by the VM 8 | erl_crash.dump 9 | 10 | # Files matching config/*.secret.exs pattern contain sensitive 11 | # data and you should not commit them into version control. 12 | # 13 | # Alternatively, you may comment the line below and commit the 14 | # secrets files as long as you replace their contents by environment 15 | # variables. 16 | /config/*.secret.exs 17 | 18 | # Build artifacts 19 | /priv/static/js 20 | /priv/static/css 21 | 22 | # JavaScript 23 | node_modules 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Slab Inc 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 20 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 21 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 22 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Elixir Formatter](https://elixirformatter.com) 2 | 3 | `mix format` in your browser. 4 | 5 | ## Development 6 | 7 | Install Dependencies: 8 | 9 | * `npm install` 10 | * `mix deps.get` 11 | 12 | Start server: 13 | 14 | * `mix phx.server` 15 | 16 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 17 | -------------------------------------------------------------------------------- /assets/js/index.js: -------------------------------------------------------------------------------- 1 | import ace from "brace"; 2 | import "brace/mode/elixir"; 3 | import "brace/theme/tomorrow"; 4 | 5 | import Clipboard from "clipboard"; 6 | import { Socket } from "phoenix"; 7 | import sample from "./sample_code"; 8 | 9 | function getChannel() { 10 | const socket = new Socket("/socket"); 11 | socket.connect(); 12 | 13 | const channel = socket.channel("formatter"); 14 | channel.join(); 15 | 16 | return channel; 17 | } 18 | 19 | function getEditor(id) { 20 | const editor = ace.edit(id); 21 | editor.getSession().setMode("ace/mode/elixir"); 22 | editor.setTheme("ace/theme/tomorrow"); 23 | editor.setOptions({ 24 | fontFamily: "Inconsolata, 'SF Code', Menlo, monospace", 25 | fontSize: "14px" 26 | }); 27 | editor.$blockScrolling = Infinity; 28 | 29 | return editor; 30 | } 31 | 32 | function configCopyButton(selector, sourceEditor) { 33 | const button = document.querySelector(selector); 34 | const clipboard = new Clipboard(selector, { 35 | text: () => sourceEditor.getValue() 36 | }); 37 | const originalLabel = button.innerHTML; 38 | 39 | const changeHandler = () => { 40 | sourceEditor.off("change", changeHandler); 41 | button.innerHTML = originalLabel; 42 | }; 43 | 44 | clipboard.on("success", ({ trigger }) => { 45 | trigger.innerText = "Copied!"; 46 | 47 | sourceEditor.on("change", changeHandler); 48 | }); 49 | 50 | return clipboard; 51 | } 52 | 53 | function setFormValues(formContainer, options) { 54 | for (const key in options) { 55 | const input = formContainer.querySelector(`[name=${key}]`); 56 | 57 | if (input) { 58 | input.value = options[key]; 59 | } 60 | } 61 | } 62 | 63 | function configOptions(selector, modalSelector) { 64 | const STORAGE_KEY = "formatterOptions"; 65 | const button = document.querySelector(selector); 66 | const modal = document.querySelector(modalSelector); 67 | const inputs = modal.querySelectorAll("input, textarea"); 68 | const options = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}"); 69 | let isOpen = null; 70 | 71 | function updateOptions() { 72 | for (const input of inputs) { 73 | const { name, type, value } = input; 74 | 75 | if (value === "") { 76 | delete options[name]; 77 | } else { 78 | options[name] = type === "number" ? parseInt(value, 10) : value; 79 | } 80 | 81 | localStorage.setItem(STORAGE_KEY, JSON.stringify(options)); 82 | } 83 | } 84 | 85 | function setIsOpen(newState) { 86 | isOpen = newState; 87 | modal.style.display = isOpen ? "block" : "none"; 88 | updateOptions(); 89 | updateResult(); 90 | } 91 | 92 | button.addEventListener("click", () => { 93 | setIsOpen(!isOpen); 94 | }); 95 | 96 | document.addEventListener("click", e => { 97 | const { target } = e; 98 | if ( 99 | target !== button && 100 | target !== modal && 101 | !modal.contains(target) && 102 | isOpen 103 | ) { 104 | setIsOpen(false); 105 | } 106 | }); 107 | 108 | setFormValues(modal, options); 109 | setIsOpen(false); 110 | return options; 111 | } 112 | 113 | function formatError({ error, description, line }) { 114 | const lineInfo = line ? `line ${line}:\n ` : ""; 115 | return `${lineInfo}${error}: ${description}`; 116 | } 117 | 118 | function updateResult() { 119 | const code = inputEditor.getValue(); 120 | channel 121 | .push("format", { code, options }) 122 | .receive("ok", ({ result }) => { 123 | outputEditor.setValue(result, 1); 124 | }) 125 | .receive("error", error => { 126 | outputEditor.setValue(formatError(error), 1); 127 | }); 128 | } 129 | 130 | const channel = getChannel(); 131 | const inputEditor = getEditor("input"); 132 | const outputEditor = getEditor("output"); 133 | outputEditor.setReadOnly(true); 134 | 135 | const options = configOptions("#options-button", "#options-window"); 136 | 137 | inputEditor.getSession().on("change", () => { 138 | updateResult(); 139 | }); 140 | 141 | configCopyButton("#copy-button", outputEditor); 142 | inputEditor.setValue(sample, 1); 143 | -------------------------------------------------------------------------------- /assets/js/sample_code.js: -------------------------------------------------------------------------------- 1 | export default `defmodule Greetings do 2 | def hello_world, do: 3 | hello("world" ) 4 | 5 | 6 | defp hello(recipient) do 7 | IO.puts( 8 | "hello #{recipient}" 9 | ) 10 | end 11 | end`; 12 | -------------------------------------------------------------------------------- /assets/scss/app.scss: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Lato:400,900|Inconsolata:400,700); 2 | @import "~normalize.css/normalize.css"; 3 | 4 | $elixirColor: #4e2a8e; 5 | $uiFontFamily: Lato, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 6 | Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 7 | $monospaceFontFamily: Inconsolata, "SF Mono", Menlo, Courier, monospace; 8 | $mobileBreakpoint: 576px; 9 | 10 | * { 11 | box-sizing: border-box; 12 | } 13 | 14 | html, 15 | body { 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | body { 21 | font-family: $uiFontFamily; 22 | } 23 | 24 | a, 25 | a:visited, 26 | a:active { 27 | color: $elixirColor; 28 | text-decoration: none; 29 | } 30 | 31 | a:hover { 32 | text-decoration: underline; 33 | } 34 | 35 | h1, 36 | h2, 37 | h3, 38 | h4, 39 | h5, 40 | h6 { 41 | margin: 0; 42 | } 43 | 44 | h1 { 45 | font-size: 20px; 46 | font-weight: 900; 47 | 48 | @media (max-width: $mobileBreakpoint) { 49 | font-size: 18px; 50 | } 51 | } 52 | 53 | button { 54 | background: white; 55 | border-radius: 3px; 56 | border: 1px solid #eee; 57 | font-family: $uiFontFamily; 58 | padding: 5px 10px; 59 | 60 | &:hover { 61 | background: #f0f0f0; 62 | } 63 | 64 | &:active { 65 | background: #eee; 66 | } 67 | } 68 | 69 | button.button-link { 70 | background: transparent; 71 | border: none; 72 | color: rgba(255, 255, 255, 0.85); 73 | transition: all 0.25s ease; 74 | padding: 0; 75 | 76 | &:hover { 77 | color: rgba(255, 255, 255, 1); 78 | } 79 | } 80 | 81 | code, 82 | pre { 83 | font-family: $monospaceFontFamily; 84 | } 85 | 86 | .flat-list { 87 | list-style: none; 88 | margin: 0; 89 | padding: 0; 90 | 91 | li { 92 | float: left; 93 | 94 | &:not(:last-child) { 95 | &:after { 96 | display: inline; 97 | content: "\00b7"; 98 | margin: 0 5px; 99 | } 100 | } 101 | } 102 | } 103 | 104 | #app { 105 | display: flex; 106 | flex-direction: column; 107 | height: 100vh; 108 | 109 | header { 110 | align-items: center; 111 | background-color: $elixirColor; 112 | color: white; 113 | display: flex; 114 | justify-content: space-between; 115 | padding: 15px; 116 | 117 | > *:not(:last-child) { 118 | margin-right: 20px; 119 | } 120 | 121 | h1 .version { 122 | color: #eee; 123 | font-size: 12px; 124 | font-weight: normal; 125 | } 126 | 127 | #title { 128 | flex: 1; 129 | } 130 | 131 | @media (max-width: $mobileBreakpoint) { 132 | #description { 133 | display: none; 134 | } 135 | } 136 | } 137 | 138 | > footer { 139 | background-color: #eee; 140 | border-top: 1px solid #ccc; 141 | display: flex; 142 | font-size: 12px; 143 | padding: 10px; 144 | justify-content: space-between; 145 | } 146 | } 147 | 148 | #toolbox { 149 | display: flex; 150 | 151 | > *:not(:last-child) { 152 | margin-right: 20px; 153 | } 154 | } 155 | 156 | #options { 157 | position: relative; 158 | 159 | #options-window { 160 | background: white; 161 | border-radius: 3px; 162 | border: 1px solid #ccc; 163 | box-shadow: 0 1px 20px rgba(0, 0, 0, 0.2); 164 | color: black; 165 | display: none; 166 | margin-top: 5px; 167 | max-width: 80vw; 168 | padding: 20px; 169 | position: absolute; 170 | right: 0; 171 | top: 100%; 172 | width: 300px; 173 | z-index: 10; 174 | 175 | footer { 176 | text-align: right; 177 | } 178 | } 179 | 180 | h3 { 181 | color: #333; 182 | margin-bottom: 20px; 183 | } 184 | 185 | .options-list { 186 | list-style: none; 187 | margin: 0; 188 | padding: 0; 189 | 190 | code { 191 | display: inline-block; 192 | min-height: 20px; 193 | } 194 | 195 | input, 196 | textarea { 197 | border: 1px solid #ddd; 198 | border-radius: 3px; 199 | font-family: $monospaceFontFamily; 200 | } 201 | 202 | input[type="number"] { 203 | width: 40px; 204 | } 205 | 206 | input[type="text"] { 207 | width: 80px; 208 | } 209 | 210 | code + input { 211 | margin-left: 5px; 212 | } 213 | 214 | textarea { 215 | width: 100%; 216 | } 217 | } 218 | } 219 | 220 | #editors { 221 | display: flex; 222 | flex: 1; 223 | width: 100%; 224 | 225 | & > * { 226 | flex: 1; 227 | } 228 | 229 | @media (min-width: $mobileBreakpoint + 1px) { 230 | & > *:not(:last-child) { 231 | border-right: 1px solid #ccc; 232 | } 233 | } 234 | 235 | @media (max-width: $mobileBreakpoint) { 236 | flex-direction: column; 237 | 238 | & > *:not(:last-child) { 239 | border-bottom: 1px solid #ccc; 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | npm install 3 | npm run build 4 | mix do deps.get, deps.compile, compile, phx.digest 5 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | config :phoenix, :json_library, Poison 9 | 10 | # Configures the endpoint 11 | config :elixir_formatter, ElixirFormatterWeb.Endpoint, 12 | url: [host: "localhost"], 13 | secret_key_base: "JRxFOZnl8zT3HioE90VM3HwKMjILzfwjLcZ/eSS7FAMwvqSixYZUJvghz7EcPJCo", 14 | render_errors: [view: ElixirFormatterWeb.ErrorView, accepts: ~w(html json)], 15 | pubsub: [name: ElixirFormatter.PubSub, 16 | adapter: Phoenix.PubSub.PG2] 17 | 18 | # Configures Elixir's Logger 19 | config :logger, :console, 20 | format: "$time $metadata[$level] $message\n", 21 | metadata: [:request_id] 22 | 23 | # Import environment specific config. This must remain at the bottom 24 | # of this file so it overrides the configuration defined above. 25 | import_config "#{Mix.env}.exs" 26 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :elixir_formatter, ElixirFormatterWeb.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [npm: ["run", "watch"]] 15 | 16 | # ## SSL Support 17 | # 18 | # In order to use HTTPS in development, a self-signed 19 | # certificate can be generated by running the following 20 | # command from your terminal: 21 | # 22 | # openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem 23 | # 24 | # The `http:` config above can be replaced with: 25 | # 26 | # https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"], 27 | # 28 | # If desired, both `http:` and `https:` keys can be 29 | # configured to run both http and https servers on 30 | # different ports. 31 | 32 | # Watch static and templates for browser reloading. 33 | config :elixir_formatter, ElixirFormatterWeb.Endpoint, 34 | live_reload: [ 35 | patterns: [ 36 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 37 | ~r{priv/gettext/.*(po)$}, 38 | ~r{lib/elixir_formatter_web/views/.*(ex)$}, 39 | ~r{lib/elixir_formatter_web/templates/.*(eex)$} 40 | ] 41 | ] 42 | 43 | # Do not include metadata nor timestamps in development logs 44 | config :logger, :console, format: "[$level] $message\n" 45 | 46 | # Set a higher stacktrace during development. Avoid configuring such 47 | # in production as building large stacktraces may be expensive. 48 | config :phoenix, :stacktrace_depth, 20 49 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we often load configuration from external 4 | # sources, such as your system environment. For this reason, 5 | # you won't find the :http configuration below, but set inside 6 | # ElixirFormatterWeb.Endpoint.init/2 when load_from_system_env is 7 | # true. Any dynamic configuration should be done there. 8 | # 9 | # Don't forget to configure the url host to something meaningful, 10 | # Phoenix uses this information when generating URLs. 11 | # 12 | # Finally, we also include the path to a cache manifest 13 | # containing the digested version of static files. This 14 | # manifest is generated by the mix phx.digest task 15 | # which you typically run after static files are built. 16 | config :elixir_formatter, ElixirFormatterWeb.Endpoint, 17 | load_from_system_env: true, 18 | url: [scheme: "https", host: "elixirformatter.com", port: 443], 19 | cache_static_manifest: "priv/static/cache_manifest.json", 20 | check_origin: ["https://elixirformatter.com", "https://elixir-formatter.render.com"] 21 | 22 | # Do not print debug messages in production 23 | config :logger, level: :info 24 | 25 | # Google Analytics 26 | config :elixir_formatter, :ga, id: "UA-84928209-3" 27 | 28 | # ## SSL Support 29 | # 30 | # To get SSL working, you will need to add the `https` key 31 | # to the previous section and set your `:url` port to 443: 32 | # 33 | # config :elixir_formatter, ElixirFormatterWeb.Endpoint, 34 | # ... 35 | # url: [host: "example.com", port: 443], 36 | # https: [:inet6, 37 | # port: 443, 38 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 39 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 40 | # 41 | # Where those two env variables return an absolute path to 42 | # the key and cert in disk or a relative path inside priv, 43 | # for example "priv/ssl/server.key". 44 | # 45 | # We also recommend setting `force_ssl`, ensuring no data is 46 | # ever sent via http, always redirecting to https: 47 | # 48 | # config :elixir_formatter, ElixirFormatterWeb.Endpoint, 49 | # force_ssl: [hsts: true] 50 | # 51 | # Check `Plug.SSL` for all available options in `force_ssl`. 52 | 53 | # ## Using releases 54 | # 55 | # If you are doing OTP releases, you need to instruct Phoenix 56 | # to start the server for all endpoints: 57 | # 58 | # config :phoenix, :serve_endpoints, true 59 | # 60 | # Alternatively, you can configure exactly which server to 61 | # start per endpoint: 62 | # 63 | # config :elixir_formatter, ElixirFormatterWeb.Endpoint, server: true 64 | # 65 | 66 | # Finally import the config/prod.secret.exs 67 | # which should be versioned separately. 68 | # import_config "prod.secret.exs" 69 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :elixir_formatter, ElixirFormatterWeb.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | -------------------------------------------------------------------------------- /lib/elixir_formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirFormatter do 2 | @moduledoc """ 3 | ElixirFormatter keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /lib/elixir_formatter/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirFormatter.Application do 2 | use Application 3 | 4 | # See https://hexdocs.pm/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec 8 | 9 | # Define workers and child supervisors to be supervised 10 | children = [ 11 | # Start the endpoint when the application starts 12 | supervisor(ElixirFormatterWeb.Endpoint, []) 13 | # Start your own worker by calling: ElixirFormatter.Worker.start_link(arg1, arg2, arg3) 14 | # worker(ElixirFormatter.Worker, [arg1, arg2, arg3]), 15 | ] 16 | 17 | # See https://hexdocs.pm/elixir/Supervisor.html 18 | # for other strategies and supported options 19 | opts = [strategy: :one_for_one, name: ElixirFormatter.Supervisor] 20 | Supervisor.start_link(children, opts) 21 | end 22 | 23 | # Tell Phoenix to update the endpoint configuration 24 | # whenever the application is updated. 25 | def config_change(changed, _new, removed) do 26 | ElixirFormatterWeb.Endpoint.config_change(changed, removed) 27 | :ok 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/elixir_formatter_web.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirFormatterWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use ElixirFormatterWeb, :controller 9 | use ElixirFormatterWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: ElixirFormatterWeb 23 | import Plug.Conn 24 | import ElixirFormatterWeb.Router.Helpers 25 | import ElixirFormatterWeb.Gettext 26 | end 27 | end 28 | 29 | def view do 30 | quote do 31 | use Phoenix.View, 32 | root: "lib/elixir_formatter_web/templates", 33 | namespace: ElixirFormatterWeb 34 | 35 | # Import convenience functions from controllers 36 | import Phoenix.Controller, only: [get_flash: 2, view_module: 1] 37 | 38 | # Use all HTML functionality (forms, tags, etc) 39 | use Phoenix.HTML 40 | 41 | import ElixirFormatterWeb.Router.Helpers 42 | import ElixirFormatterWeb.ErrorHelpers 43 | import ElixirFormatterWeb.Gettext 44 | end 45 | end 46 | 47 | def router do 48 | quote do 49 | use Phoenix.Router 50 | import Plug.Conn 51 | import Phoenix.Controller 52 | end 53 | end 54 | 55 | def channel do 56 | quote do 57 | use Phoenix.Channel 58 | import ElixirFormatterWeb.Gettext 59 | end 60 | end 61 | 62 | @doc """ 63 | When used, dispatch to the appropriate controller/view/etc. 64 | """ 65 | defmacro __using__(which) when is_atom(which) do 66 | apply(__MODULE__, which, []) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/elixir_formatter_web/channels/formatter_channel.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirFormatterWeb.FormatterChannel do 2 | use ElixirFormatterWeb, :channel 3 | 4 | def join("formatter", payload, socket) do 5 | if authorized?(payload) do 6 | {:ok, socket} 7 | else 8 | {:error, %{reason: "unauthorized"}} 9 | end 10 | end 11 | 12 | # Channels can be used in a request/response fashion 13 | # by sending replies to requests from the client 14 | def handle_in("ping", payload, socket) do 15 | {:reply, {:ok, payload}, socket} 16 | end 17 | 18 | # It is also common to receive messages from the client and 19 | # broadcast to everyone in the current topic (formatter:lobby). 20 | # def handle_in("shout", payload, socket) do 21 | # broadcast socket, "shout", payload 22 | # {:noreply, socket} 23 | # end 24 | 25 | def handle_in("format", %{"code" => code} = payload, socket) do 26 | try do 27 | parse_options(payload["options"] || %{}) 28 | rescue 29 | _ -> 30 | { 31 | :reply, 32 | {:error, %{ 33 | "error" => "Unknown", 34 | "description" => "Could not parse the `:locals_without_parens` options." 35 | }}, 36 | socket 37 | } 38 | else 39 | options -> handle_formatting(socket, code, options) 40 | end 41 | end 42 | 43 | # Add authorization logic here as required. 44 | defp authorized?(_payload) do 45 | true 46 | end 47 | 48 | defp parse_options(options) do 49 | locals = options["locals_without_parens"] 50 | 51 | options = 52 | if not is_nil(locals) do 53 | locals = 54 | locals 55 | |> String.trim() 56 | |> String.split(~r(,?\n), trim: true) 57 | |> Enum.map(fn local -> 58 | [name, arity] = String.split(local, ~r/:\s?/) 59 | {String.to_atom(name), String.to_integer(arity)} 60 | end) 61 | 62 | Map.merge(options, %{"locals_without_parens" => locals}) 63 | else 64 | options 65 | end 66 | 67 | Enum.map(options, fn {key, value} -> {String.to_atom(key), value} end) 68 | end 69 | 70 | defp handle_formatting(socket, code, options) do 71 | try do 72 | result = code |> Code.format_string!(options) |> Enum.join() 73 | {:reply, {:ok, %{"result" => result}}, socket} 74 | rescue 75 | error in [SyntaxError, TokenMissingError, CompileError] -> 76 | { 77 | :reply, 78 | {:error, %{ 79 | "error" => error.__struct__ |> Module.split() |> Enum.join("."), 80 | "description" => error.description, 81 | "line" => error.line 82 | }}, 83 | socket 84 | } 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/elixir_formatter_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirFormatterWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", ElixirFormatterWeb.RoomChannel 6 | channel "formatter", ElixirFormatterWeb.FormatterChannel 7 | 8 | ## Transports 9 | transport(:websocket, Phoenix.Transports.WebSocket, timeout: 30_000) 10 | # transport :longpoll, Phoenix.Transports.LongPoll 11 | 12 | # Socket params are passed from the client and can 13 | # be used to verify and authenticate a user. After 14 | # verification, you can put default assigns into 15 | # the socket that will be set for all channels, ie 16 | # 17 | # {:ok, assign(socket, :user_id, verified_user_id)} 18 | # 19 | # To deny connection, return `:error`. 20 | # 21 | # See `Phoenix.Token` documentation for examples in 22 | # performing token verification on connect. 23 | def connect(_params, socket) do 24 | {:ok, socket} 25 | end 26 | 27 | # Socket id's are topics that allow you to identify all sockets for a given user: 28 | # 29 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 30 | # 31 | # Would allow you to broadcast a "disconnect" event and terminate 32 | # all active sockets and channels for a given user: 33 | # 34 | # ElixirFormatterWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 35 | # 36 | # Returning `nil` makes this socket anonymous. 37 | def id(_socket), do: nil 38 | end 39 | -------------------------------------------------------------------------------- /lib/elixir_formatter_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirFormatterWeb.PageController do 2 | use ElixirFormatterWeb, :controller 3 | 4 | def index(conn, _params) do 5 | conn 6 | |> assign(:elixir_version, System.version()) 7 | |> render("index.html") 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/elixir_formatter_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirFormatterWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :elixir_formatter 3 | 4 | socket "/socket", ElixirFormatterWeb.UserSocket 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug Plug.Static, 11 | at: "/", 12 | from: :elixir_formatter, 13 | gzip: false, 14 | only: ~w(css fonts images js favicon.ico manifest.json robots.txt) 15 | 16 | # Code reloading can be explicitly enabled under the 17 | # :code_reloader configuration of your endpoint. 18 | if code_reloading? do 19 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 20 | plug Phoenix.LiveReloader 21 | plug Phoenix.CodeReloader 22 | end 23 | 24 | plug Plug.RequestId 25 | plug Plug.Logger 26 | 27 | plug Plug.Parsers, 28 | parsers: [:urlencoded, :multipart, :json], 29 | pass: ["*/*"], 30 | json_decoder: Poison 31 | 32 | plug Plug.MethodOverride 33 | plug Plug.Head 34 | 35 | # The session will be stored in the cookie and signed, 36 | # this means its contents can be read but not tampered with. 37 | # Set :encryption_salt if you would also like to encrypt it. 38 | plug Plug.Session, 39 | store: :cookie, 40 | key: "_elixir_formatter_key", 41 | signing_salt: "tvCIxpXa" 42 | 43 | plug ElixirFormatterWeb.Router 44 | 45 | @doc """ 46 | Callback invoked for dynamically configuring the endpoint. 47 | 48 | It receives the endpoint configuration and checks if 49 | configuration should be loaded from the system environment. 50 | """ 51 | def init(_key, config) do 52 | if config[:load_from_system_env] do 53 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" 54 | {:ok, Keyword.put(config, :http, [:inet6, port: port])} 55 | else 56 | {:ok, config} 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/elixir_formatter_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirFormatterWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import ElixirFormatterWeb.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :elixir_formatter 24 | end 25 | -------------------------------------------------------------------------------- /lib/elixir_formatter_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirFormatterWeb.Router do 2 | use ElixirFormatterWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_flash 8 | plug :protect_from_forgery 9 | plug :put_secure_browser_headers 10 | end 11 | 12 | pipeline :api do 13 | plug :accepts, ["json"] 14 | end 15 | 16 | scope "/", ElixirFormatterWeb do 17 | # Use the default browser stack 18 | pipe_through(:browser) 19 | 20 | get("/", PageController, :index) 21 | end 22 | 23 | # Other scopes may use custom stacks. 24 | # scope "/api", ElixirFormatterWeb do 25 | # pipe_through :api 26 | # end 27 | end 28 | -------------------------------------------------------------------------------- /lib/elixir_formatter_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |