Added golines playground
All checks were successful
/ publish (push) Successful in 2m43s

This commit is contained in:
Leon Mika 2026-09-06 09:23:55 +10:00
parent 09fceca2f9
commit 40e5379eb3
11 changed files with 250 additions and 2 deletions

View file

@ -15,6 +15,7 @@ build.wasm:
GOOS=js GOARCH=wasm go build -o target/wasm/gotemplate.wasm ./cmds/gotemplate
GOOS=js GOARCH=wasm go build -o target/wasm/timestamps.wasm ./cmds/timestamps
GOOS=js GOARCH=wasm go build -o target/wasm/android-icons.wasm ./cmds/android-icons
GOOS=js GOARCH=wasm go build -o target/wasm/golines.wasm ./cmds/golines
cp $(GOROOT)/lib/wasm/wasm_exec.js target/wasm/.
.Phony: build.site

29
cmds/golines/format.go Normal file
View file

@ -0,0 +1,29 @@
package main
import (
"fmt"
"github.com/golangci/golines/shorten"
)
func formatSource(source string, maxLen int) (output []byte, err error) {
// Dependency panics must not escape the JS callback and terminate the WASM runtime.
defer func() {
if recovered := recover(); recovered != nil {
output = nil
err = fmt.Errorf("golines could not format this input (internal panic: %v). Ensure the input is a complete Go file, including a package declaration", recovered)
}
}()
tabLen := 4
if maxLen < 80 {
tabLen = 2
}
return shorten.NewShortener(&shorten.Config{
MaxLen: maxLen,
TabLen: tabLen,
ShortenComments: true,
ReformatTags: true,
ChainSplitDots: true,
}).Process([]byte(source))
}

View file

@ -0,0 +1,45 @@
package main
import (
"go/parser"
"go/token"
"strings"
"testing"
)
func TestFormatWidths(t *testing.T) {
source := `package main
func main() {
println("first argument", "second argument", "third argument", "fourth argument")
}
`
var narrow, wide string
for width := 40; width <= 180; width += 5 {
output, err := formatSource(source, width)
if err != nil {
t.Fatalf("width %d: %v", width, err)
}
if _, err := parser.ParseFile(token.NewFileSet(), "output.go", output, parser.AllErrors); err != nil {
t.Fatalf("width %d produced invalid Go: %v", width, err)
}
if width == 40 {
narrow = string(output)
}
if width == 180 {
wide = string(output)
}
}
if strings.Count(narrow, "\n") <= strings.Count(wide, "\n") {
t.Fatal("narrow width did not split the call")
}
}
func TestInvalidSource(t *testing.T) {
if _, err := formatSource("package main\nfunc {", 100); err == nil {
t.Fatal("expected a syntax error")
}
// A failed edit must not prevent subsequent formatting.
if _, err := formatSource("package main\nfunc main() {}", 100); err != nil {
t.Fatal(err)
}
}

40
cmds/golines/main.go Normal file
View file

@ -0,0 +1,40 @@
//go:build js && wasm
package main
import "syscall/js"
func main() {
document := js.Global().Get("document")
element := func(id string) js.Value { return document.Call("getElementById", id) }
source, width := element("source"), element("max-len")
output, message := element("output"), element("message")
render := func() {
maxLen := width.Get("valueAsNumber").Int()
element("width-value").Set("textContent", maxLen)
tabLen := 4
if maxLen < 80 {
tabLen = 2
}
element("tab-value").Set("textContent", tabLen)
source.Get("style").Set("tabSize", tabLen)
output.Get("style").Set("tabSize", tabLen)
formatted, err := formatSource(source.Get("value").String(), maxLen)
if err != nil {
output.Set("value", "")
message.Set("textContent", err.Error())
source.Call("setAttribute", "aria-invalid", "true")
return
}
source.Call("removeAttribute", "aria-invalid")
output.Set("value", string(formatted))
message.Set("textContent", "")
}
listener := js.FuncOf(func(this js.Value, args []js.Value) any { render(); return nil })
source.Call("addEventListener", "input", listener)
width.Call("addEventListener", "input", listener)
source.Set("disabled", false)
width.Set("disabled", false)
render()
select {}
}

View file

@ -0,0 +1,25 @@
package main
import (
"strings"
"testing"
)
func TestFormatterPanicRecovery(t *testing.T) {
// go/format accepts fragments, but dst v0.27.3 panics when golines
// attempts to decorate this function without a package declaration.
source := `func example() {
println("first argument", "second argument", "third argument", "fourth argument")
}`
output, err := formatSource(source, 40)
if err == nil || !strings.Contains(err.Error(), "internal panic") {
t.Fatalf("expected recovered dependency panic, got output %q, error %v", output, err)
}
if output != nil {
t.Fatalf("unexpected output after panic: %q", output)
}
output, err = formatSource("package main\n"+source, 40)
if err != nil || !strings.Contains(string(output), "func example()") {
t.Fatalf("formatting after panic failed: output %q, error %v", output, err)
}
}

13
go.mod
View file

@ -3,6 +3,15 @@ module github.com/lmika/webtools
go 1.25.0
require (
github.com/alecthomas/participle/v2 v2.1.4 // indirect
golang.org/x/image v0.37.0 // indirect
github.com/alecthomas/participle/v2 v2.1.4
github.com/golangci/golines v0.15.0
golang.org/x/image v0.37.0
)
require (
github.com/dave/dst v0.27.3 // indirect
github.com/ldez/structtags v0.6.1 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/tools v0.41.0 // indirect
)

32
go.sum
View file

@ -1,4 +1,36 @@
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U=
github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY=
github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc=
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0=
github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk=
github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

41
site/golines/index.html Normal file
View file

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<title>Golines Playground - Tools</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<link rel="stylesheet" href="style.css">
</head>
<body class="container">
<header><hgroup>
<h1>Golines Playground</h1>
<p>Format Go code with <a href="https://github.com/golangci/golines">golines</a>. Everything runs in your browser.</p>
</hgroup></header>
<main>
<label for="max-len">Maximum line width: <output id="width-value" for="max-len">100</output></label>
<input id="max-len" type="range" min="40" max="180" step="5" value="100" disabled aria-describedby="config">
<p id="config"><small>Tab width: <span id="tab-value">4</span>. Comment shortening, struct tag formatting, and splitting method chains at dots are enabled.</small></p>
<div class="grid">
<div>
<label for="source">Go source</label>
<textarea id="source" spellcheck="false" autocapitalize="off" autocomplete="off" disabled aria-describedby="message">package main
import "fmt"
func main() {
fmt.Println("Hello from golines!", "Move the slider to explore how longer function calls are formatted.")
}
</textarea>
</div>
<div>
<label for="output">Formatted output</label>
<textarea id="output" readonly spellcheck="false" aria-label="Formatted output"></textarea>
</div>
</div>
<p id="message" role="status" aria-live="polite">Loading formatter…</p>
</main>
<script src="main.js" type="module"></script>
</body>
</html>

12
site/golines/main.js Normal file
View file

@ -0,0 +1,12 @@
try {
await import("/wasm/wasm_exec.js");
const go = new Go();
const response = await fetch("/wasm/golines.wasm");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await WebAssembly.instantiate(await response.arrayBuffer(), go.importObject);
await go.run(result.instance);
} catch (error) {
document.getElementById("message").textContent = `Unable to load the formatter: ${error.message}. Please reload to try again.`;
document.getElementById("source").disabled = true;
document.getElementById("max-len").disabled = true;
}

13
site/golines/style.css Normal file
View file

@ -0,0 +1,13 @@
textarea {
height: 28rem;
font-family: monospace;
font-size: 0.8rem;
white-space: pre;
overflow-wrap: normal;
tab-size: 4;
}
#message {
white-space: pre-wrap;
overflow-wrap: anywhere;
}

View file

@ -20,6 +20,7 @@
<ul>
<li><a href="/clocks/">Clocks</a></li>
<li><a href="/freelens-logo/">Freelens Logo Maker</a></li>
<li><a href="/golines/">Golines Playground</a></li>
<li><a href="/gotemplate/">Go Template Playground</a></li>
<li><a href="/gradient-bands/">Gradient Bands</a></li>
<li><a href="/2lcc/">Two-letter Country Codes</a></li>