├── .gitignore ├── static ├── robots.txt ├── logo.ico ├── index.html ├── style.css └── playground.js ├── .github └── workflows │ └── ci.yml ├── go.mod ├── handlers_test.go ├── LICENSE ├── main.go ├── README.md ├── go.sum └── handlers.go /.gitignore: -------------------------------------------------------------------------------- 1 | zig-play 2 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /static/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gsquire/zig-play/HEAD/static/logo.ico -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: CI 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.25.x] 8 | os: [ubuntu-latest, macos-latest, windows-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v5 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | - name: Test 18 | run: | 19 | go vet ./... 20 | go test ./... 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gsquire/zig-play 2 | 3 | go 1.25.4 4 | 5 | require ( 6 | github.com/gorilla/handlers v1.5.2 7 | github.com/julienschmidt/httprouter v1.3.0 8 | github.com/justinas/alice v1.2.0 9 | github.com/rs/zerolog v1.34.0 10 | github.com/sethvargo/go-limiter v1.1.0 11 | github.com/unrolled/secure v1.17.0 12 | ) 13 | 14 | require ( 15 | github.com/felixge/httpsnoop v1.0.4 // indirect 16 | github.com/mattn/go-colorable v0.1.14 // indirect 17 | github.com/mattn/go-isatty v0.0.20 // indirect 18 | golang.org/x/sys v0.38.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /handlers_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/rs/zerolog" 10 | ) 11 | 12 | func TestBodySizeLimit(t *testing.T) { 13 | payload := bytes.Repeat([]byte("big"), 6*1024*1024) 14 | 15 | req, err := http.NewRequest(http.MethodPost, "/server/run", bytes.NewBuffer(payload)) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | rr := httptest.NewRecorder() 21 | handler := LoggingMiddleware(http.HandlerFunc(Run), zerolog.Nop()) 22 | 23 | handler.ServeHTTP(rr, req) 24 | 25 | if status := rr.Code; status != http.StatusInternalServerError { 26 | t.Errorf("handler returned wrong status code: got %v want %v", 27 | status, http.StatusInternalServerError) 28 | } 29 | 30 | expected := "reading body\n" 31 | if rr.Body.String() != expected { 32 | t.Errorf("handler returned unexpected body: got %v want %v", 33 | rr.Body.String(), expected) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Garrett Squire 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/gorilla/handlers" 10 | "github.com/julienschmidt/httprouter" 11 | "github.com/justinas/alice" 12 | "github.com/rs/zerolog" 13 | "github.com/sethvargo/go-limiter/httplimit" 14 | "github.com/sethvargo/go-limiter/memorystore" 15 | "github.com/unrolled/secure" 16 | ) 17 | 18 | func securitySettings() *secure.Secure { 19 | return secure.New(secure.Options{ 20 | BrowserXssFilter: true, 21 | ContentTypeNosniff: true, 22 | FrameDeny: true, 23 | STSPreload: true, 24 | STSSeconds: 31536000, 25 | }) 26 | } 27 | 28 | func main() { 29 | // Users can compile code 5 times per minute. 30 | rateLimiter, err := memorystore.New(&memorystore.Config{ 31 | Tokens: 5, 32 | Interval: time.Minute, 33 | }) 34 | if err != nil { 35 | log.Fatal("error making rate limiter", err) 36 | } 37 | rlMiddle, err := httplimit.NewMiddleware(rateLimiter, httplimit.IPKeyFunc()) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | logger := zerolog.New(os.Stdout).With().Timestamp().Logger() 43 | 44 | router := httprouter.New() 45 | 46 | router.Handler(http.MethodPost, "/server/run", http.HandlerFunc(Run)) 47 | router.Handler(http.MethodPost, "/server/fmt", http.HandlerFunc(Fmt)) 48 | 49 | chain := alice.New(rlMiddle.Handle, securitySettings().Handler, handlers.CompressHandler, handlers.RecoveryHandler()).Then(LoggingMiddleware(router, logger)) 50 | log.Fatal(http.ListenAndServe(":8080", chain)) 51 | } 52 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Zig Playground 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

Zig Playground

16 |
17 | 18 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 |
32 | 33 |
34 |
35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zig Playground 2 | 3 | This is a rudimentary online compiler for the [Zig](https://ziglang.org) programming language. It 4 | is inspired by the [Go](https://play.golang.org) playground. 5 | 6 | It's currently served from this [page](https://zig-play.dev). 7 | 8 | ### Setup 9 | The main server is a Go binary that serves up a single HTML page that allows you to enter your Zig 10 | code and then run it. To host it yourself, you will need a Go tool chain which can be installed via 11 | `brew` on a Mac. If you wish to run it locally, you must compile it for your `GOOS` and `GOARCH`. 12 | You should also have Zig installed and accessible from within your `$PATH` on the host. 13 | 14 | ### Hosting 15 | I currently am using a VPS and have [Caddy](https://caddyserver.com) as a reverse proxy. 16 | 17 | ### FAQ 18 | > What can this playground do? 19 | 20 | It is currently set up to simply run and format a single Zig source file. (i.e. `zig run source.zig` & `zig fmt source.zig`) 21 | 22 | > Are there any timeouts? 23 | 24 | If your code doesn't build within 30 seconds, the server will quit your request. 25 | 26 | > Why am I getting rate-limited? 27 | 28 | You're allowed five executions per minute which I think is fairly generous. 29 | 30 | > Is it secure? 31 | 32 | Go read the source. I do not collect logs of any kind and am not interested in your data. Unless it 33 | is causing issues to the service. 34 | 35 | > Will this always be available? 36 | 37 | To the best of my ability, I will try and keep this online. 38 | 39 | ### Contact 40 | Feel free to write to hello@zig-play.dev with any questions or comments. 41 | 42 | ### License 43 | MIT 44 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background-color: #FFFFFF; 3 | --color: #000000; 4 | --border: rgb(171, 171, 171); 5 | } 6 | 7 | [data-theme="dark"] { 8 | --background-color: #111; 9 | --color: #bbb; 10 | --border: rgb(83, 84, 85); 11 | } 12 | 13 | [data-theme="dark"] #toggle-dark { 14 | display: none; 15 | } 16 | 17 | [data-theme="light"] #toggle-light { 18 | display: none; 19 | } 20 | 21 | h1, 22 | p, 23 | label, 24 | select, 25 | button { 26 | font-family: sans-serif; 27 | } 28 | 29 | div { 30 | box-sizing: border-box; 31 | } 32 | 33 | body { 34 | color: var(--color); 35 | background-color: var(--background-color); 36 | } 37 | 38 | main { 39 | display: block; 40 | width: 90vw; 41 | margin: 0 auto; 42 | } 43 | 44 | .header { 45 | display: flex; 46 | justify-content: space-between; 47 | flex-wrap: wrap; 48 | } 49 | 50 | .playground-controls { 51 | display: flex; 52 | align-items: center; 53 | gap: 0.5rem; 54 | padding-top: 0.4rem; 55 | } 56 | 57 | h1 { 58 | font-size: 2.5rem; 59 | margin-right: 0.5rem; 60 | } 61 | 62 | #stdout { 63 | display: block; 64 | padding: 1rem; 65 | margin: 2rem 0; 66 | font-family: monospace; 67 | width: 100%; 68 | height: 20vh; 69 | border: 1px dashed gray; 70 | overflow: scroll; 71 | white-space: pre; 72 | } 73 | 74 | select, 75 | button { 76 | font-size: 1rem; 77 | padding: 0.6rem; 78 | border: none; 79 | background: lightblue; 80 | cursor: pointer; 81 | } 82 | 83 | #playground { 84 | display: flex; 85 | flex-direction: column; 86 | } 87 | 88 | #editor { 89 | border: solid 1px var(--border); 90 | font-size: 0.8rem; 91 | /* Ensure the font for ace editor is monospace! */ 92 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'Source Code Pro', 'source-code-pro', monospace !important; 93 | height: 60vh; 94 | } 95 | 96 | /* On smaller screen widths, the flex-based header and controls 97 | * will start to wrap. Make our editor take up less height. */ 98 | @media only screen and (max-width: 800px) { 99 | #editor { 100 | height: 50vh; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 2 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 3 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 4 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 5 | github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= 6 | github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= 7 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 8 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 9 | github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo= 10 | github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA= 11 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 12 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 13 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 14 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 15 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 16 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 17 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 18 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 19 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 20 | github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= 21 | github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= 22 | github.com/sethvargo/go-limiter v1.1.0 h1:eLeZVQ2zqJOiEs03GguqmBVG6/T6lsZB+6PP1t7J6fA= 23 | github.com/sethvargo/go-limiter v1.1.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II= 24 | github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= 25 | github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= 26 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 30 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 31 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "path/filepath" 11 | "time" 12 | 13 | "github.com/rs/zerolog" 14 | ) 15 | 16 | type Command int 17 | type CtxKey string 18 | 19 | const ( 20 | R Command = iota 21 | F 22 | ) 23 | 24 | const CtxLogger CtxKey = "logger" 25 | 26 | func LoggingMiddleware(h http.Handler, logger zerolog.Logger) http.Handler { 27 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 28 | ctx := context.WithValue(r.Context(), CtxLogger, logger) 29 | h.ServeHTTP(w, r.WithContext(ctx)) 30 | }) 31 | } 32 | 33 | func whichZig(r *http.Request) string { 34 | const zigVersion = "X-Zig-Version" 35 | 36 | return r.Header.Get(zigVersion) 37 | } 38 | 39 | func execute(w http.ResponseWriter, r *http.Request, command Command) { 40 | logger := r.Context().Value(CtxLogger).(zerolog.Logger) 41 | 42 | // Limit how big a source file can be. 5MB here. 43 | r.Body = http.MaxBytesReader(w, r.Body, 5*1024*1024) 44 | zigSource, err := io.ReadAll(r.Body) 45 | if err != nil { 46 | logger.Error().Err(err).Msg("reading the request body") 47 | http.Error(w, "reading body", http.StatusInternalServerError) 48 | return 49 | } 50 | defer r.Body.Close() 51 | 52 | // Set up the temporary resources. 53 | playgroundDir := os.Getenv("PLAYGROUND_DIR") 54 | dir, err := os.MkdirTemp(playgroundDir, "playground") 55 | if err != nil { 56 | logger.Error().Err(err).Msg("making the temporary directory") 57 | http.Error(w, "creating temporary directory", http.StatusInternalServerError) 58 | return 59 | } 60 | defer os.RemoveAll(dir) 61 | 62 | tmpSource := filepath.Join(dir, "play.zig") 63 | if err := os.WriteFile(tmpSource, []byte(zigSource), 0666); err != nil { 64 | logger.Error().Err(err).Msg("copying the source") 65 | http.Error(w, "copying zig source", http.StatusInternalServerError) 66 | return 67 | } 68 | 69 | // Currently we cap compilation times at thirty seconds. 70 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 71 | defer cancel() 72 | 73 | // We only have two commands for now. 74 | var output []byte 75 | if command == R { 76 | home := os.Getenv("HOME") 77 | output, err = exec.CommandContext(ctx, path.Join(home, "zrun.sh"), whichZig(r), tmpSource).CombinedOutput() 78 | } else { 79 | fd, ferr := os.Open(tmpSource) 80 | if ferr != nil { 81 | logger.Error().Err(err).Msg("opening the tmp source during fmt command") 82 | } 83 | cmd := exec.CommandContext(ctx, "zvm", "run", whichZig(r), "fmt", "--stdin") 84 | cmd.Stdin = fd 85 | output, err = cmd.CombinedOutput() 86 | } 87 | 88 | if err != nil { 89 | logger.Error().Err(err).Msg("running the command") 90 | w.WriteHeader(http.StatusBadRequest) 91 | } 92 | 93 | _, err = w.Write(output) 94 | if err != nil { 95 | logger.Error().Err(err).Msg("writing the response") 96 | http.Error(w, "writing response", http.StatusInternalServerError) 97 | } 98 | } 99 | 100 | func Run(w http.ResponseWriter, r *http.Request) { 101 | execute(w, r, R) 102 | } 103 | 104 | func Fmt(w http.ResponseWriter, r *http.Request) { 105 | execute(w, r, F) 106 | } 107 | -------------------------------------------------------------------------------- /static/playground.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global ace editor variable. 3 | */ 4 | var editor; 5 | 6 | const runCmd = '/server/run'; 7 | const fmtCmd = '/server/fmt'; 8 | 9 | /** 10 | * Execute a function. 11 | * @param {string} route - The function to execute. 12 | */ 13 | async function execute(route) { 14 | const stdout = document.getElementById('stdout'); 15 | if (!stdout) { 16 | console.error("Couldn't find element #stdout."); 17 | return; 18 | } 19 | stdout.innerHTML = "Waiting for server..."; 20 | 21 | const code = editor.getValue(); 22 | var version = document.getElementById("version-select").value; 23 | 24 | try { 25 | const res = await fetch(route, { 26 | method: 'POST', 27 | headers: { 28 | 'Content-Type': 'text/plain', 29 | 'X-Zig-Version': version 30 | }, 31 | body: code 32 | }); 33 | const text = await res.text(); 34 | let msg = text; 35 | if (res.status === 429) { 36 | msg = 'Too many requests. Please wait a minute and then try again.'; 37 | } else if (!res.ok) { 38 | msg = 'An error occurred:\n' + text; 39 | } 40 | // If run command, we display the resulting text. 41 | if (route === runCmd) { 42 | stdout.innerHTML = msg; 43 | } else if (route === fmtCmd) { 44 | // For format command, we set the editor text to the output and 45 | // let the user know the command is complete. 46 | if (code != msg) { 47 | // Preserve selection to try to put the cursor about where 48 | // it was before format. 49 | selection = editor.selection.toJSON(); 50 | editor.setValue(msg); 51 | editor.selection.fromJSON(selection) 52 | } 53 | stdout.innerHTML = ''; 54 | } 55 | } catch (e) { 56 | stdout.innerHTML = 'Could not connect to server.'; 57 | } 58 | } 59 | 60 | async function runCode() { 61 | execute(runCmd); 62 | } 63 | 64 | async function fmtCode() { 65 | execute(fmtCmd); 66 | } 67 | 68 | function toggleTheme() { 69 | // Determine new value of toggle and set data-theme. 70 | let newValue = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; 71 | document.documentElement.setAttribute('data-theme', newValue); 72 | // Update our ace editor with new theme as well. 73 | let editorTheme = newValue === 'dark' ? 'ace/theme/vibrant_ink' : 'ace/theme/clouds'; 74 | editor.setTheme(editorTheme); 75 | } 76 | 77 | /** 78 | * Our demo code. Setting this up as a variable to keep the HTML clean. 79 | * In the future, we can add a map of name/code pairs to allow a select 80 | * list with various code examples. For example this snippet would be "hello world" 81 | * but we would also allow them to select "zigg zagg" from the examples: 82 | * 83 | * https://ziglang.org/learn/samples/ 84 | * */ 85 | const demoCode = `// You can edit this code! 86 | // Click into the editor and start typing. 87 | const std = @import("std"); 88 | const builtin = @import("builtin"); 89 | 90 | pub fn main() void { 91 | std.debug.print("Hello, {s}! (using Zig version: {f})", .{ "world", builtin.zig_version }); 92 | } 93 | `; // Adding trailing space so it matches "format" output. 94 | 95 | // On content loaded, set up ace editor and detect dark/light mode and set data-theme appropriately. 96 | document.addEventListener('DOMContentLoaded', function () { 97 | editor = ace.edit("editor"); 98 | // Set the value of our editor to our demo code. 99 | // The second param prevents default "select all" behavior. 100 | editor.setValue(demoCode, -1); 101 | if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { 102 | editor.setTheme("ace/theme/vibrant_ink"); 103 | document.documentElement.setAttribute('data-theme', 'dark'); 104 | } else { 105 | editor.setTheme("ace/theme/clouds"); 106 | document.documentElement.setAttribute('data-theme', 'light'); 107 | } 108 | // Set editor mode to zig. 109 | editor.session.setMode("ace/mode/zig"); 110 | }); 111 | --------------------------------------------------------------------------------