├── LICENSE ├── README.md ├── decl ├── decl.go └── decl_test.go ├── examples.go ├── examples_test.go ├── go.mod ├── godecl.go └── index.html /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | godecl 2 | ====== 3 | 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/shurcooL/godecl.svg)](https://pkg.go.dev/github.com/shurcooL/godecl) 5 | 6 | A godecl experiment. Like cdecl, but for Go. 7 | 8 | https://godecl.org 9 | 10 | Inspired by @bradfitz at https://twitter.com/bradfitz/status/833048466456600576. 11 | 12 | Installation 13 | ------------ 14 | 15 | ```sh 16 | go install github.com/shurcooL/godecl@latest 17 | ``` 18 | 19 | Directories 20 | ----------- 21 | 22 | | Path | Synopsis | 23 | |------------------------------------------------------------|-----------------------------------------------------------------------------------------------------| 24 | | [decl](https://pkg.go.dev/github.com/shurcooL/godecl/decl) | Package decl implements functionality to convert fragments of Go code to an English representation. | 25 | 26 | License 27 | ------- 28 | 29 | - [BSD-style License](LICENSE) 30 | -------------------------------------------------------------------------------- /decl/decl.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package decl implements functionality to convert 6 | // fragments of Go code to an English representation. 7 | package decl 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "go/ast" 13 | "go/parser" 14 | "go/token" 15 | "strings" 16 | 17 | "github.com/shurcooL/go/parserutil" 18 | ) 19 | 20 | // GoToEnglish returns a (possibly simplified) English representation 21 | // for the fragment of Go code. 22 | func GoToEnglish(frag string) (english string, err error) { 23 | var errors []string 24 | if expr, err := parser.ParseExpr(frag); err == nil { 25 | return exprString(expr), nil 26 | } else { 27 | errors = append(errors, "as an expression: "+err.Error()) 28 | } 29 | if decl, err := parserutil.ParseDecl(frag); err == nil { 30 | return declString(decl), nil 31 | } else { 32 | errors = append(errors, "as a declaration: "+err.Error()) 33 | } 34 | if stmt, err := parserutil.ParseStmt(frag); err == nil { 35 | return stmtString(stmt), nil 36 | } else { 37 | errors = append(errors, "as a statement: "+err.Error()) 38 | } 39 | return "", fmt.Errorf("failed to parse fragment of Go code:\n%v", strings.Join(errors, "\n")) 40 | } 41 | 42 | // stmtString returns the (possibly simplified) English representation for x. 43 | func stmtString(x ast.Stmt) string { 44 | var buf bytes.Buffer 45 | writeStmt(&buf, x) 46 | return buf.String() 47 | } 48 | 49 | // declString returns the (possibly simplified) English representation for x. 50 | func declString(x ast.Decl) string { 51 | var buf bytes.Buffer 52 | writeDecl(&buf, x) 53 | return buf.String() 54 | } 55 | 56 | // exprString returns the (possibly simplified) English representation for x. 57 | func exprString(x ast.Expr) string { 58 | var buf bytes.Buffer 59 | writeExpr(&buf, x) 60 | return buf.String() 61 | } 62 | 63 | // writeStmt writes the (possibly simplified) English representation for x to buf. 64 | func writeStmt(buf *bytes.Buffer, x ast.Stmt) { 65 | switch x := x.(type) { 66 | default: 67 | fmt.Fprintf(buf, "", x) 68 | 69 | case *ast.EmptyStmt: 70 | // Do nothing. 71 | 72 | case *ast.DeclStmt: 73 | writeDecl(buf, x.Decl) 74 | 75 | case *ast.ExprStmt: 76 | writeExpr(buf, x.X) 77 | 78 | case *ast.AssignStmt: 79 | switch x.Tok { 80 | case token.DEFINE: 81 | buf.WriteString("short declare variable") 82 | if len(x.Lhs) > 1 { 83 | buf.WriteByte('s') // Plural. 84 | } 85 | buf.WriteByte(' ') 86 | for i, e := range x.Lhs { 87 | if i > 0 { 88 | buf.WriteString(" and ") 89 | } 90 | writeExpr(buf, e) 91 | } 92 | switch len(x.Rhs) { 93 | case 0: // Do nothing. 94 | case 1: 95 | buf.WriteString(" with initial value ") 96 | default: 97 | buf.WriteString(" with initial values ") 98 | } 99 | for i, e := range x.Rhs { 100 | if i > 0 { 101 | buf.WriteString(" and ") 102 | } 103 | writeExpr(buf, e) 104 | } 105 | case token.ASSIGN: 106 | buf.WriteString("assign to ") 107 | for i, e := range x.Lhs { 108 | if i > 0 { 109 | buf.WriteString(" and ") 110 | } 111 | writeExpr(buf, e) 112 | } 113 | switch len(x.Rhs) { 114 | case 0: // Do nothing. 115 | case 1: 116 | buf.WriteString(" the value ") 117 | default: 118 | buf.WriteString(" the values ") 119 | } 120 | for i, e := range x.Rhs { 121 | if i > 0 { 122 | buf.WriteString(" and ") 123 | } 124 | writeExpr(buf, e) 125 | } 126 | default: 127 | fmt.Fprintf(buf, "", x.Tok) 128 | } 129 | } 130 | } 131 | 132 | // writeDecl writes the (possibly simplified) English representation for x to buf. 133 | func writeDecl(buf *bytes.Buffer, x ast.Decl) { 134 | switch x := x.(type) { 135 | default: 136 | fmt.Fprintf(buf, "", x) 137 | 138 | case *ast.GenDecl: 139 | switch x.Tok { 140 | default: 141 | buf.WriteString("declare ") 142 | case token.IMPORT: 143 | buf.WriteString("import package") 144 | if len(x.Specs) > 1 { 145 | buf.WriteByte('s') // Plural. 146 | } 147 | buf.WriteByte(' ') 148 | } 149 | for i, s := range x.Specs { 150 | writeSep(buf, i, len(x.Specs)) 151 | switch x.Tok { 152 | case token.VAR: 153 | buf.WriteString("variable") 154 | case token.CONST: 155 | buf.WriteString("constant") 156 | case token.TYPE: 157 | buf.WriteString("type") 158 | case token.IMPORT: 159 | // Do nothing. 160 | default: 161 | fmt.Fprintf(buf, "", x.Tok) 162 | } 163 | if isValueSpecPlural(s) { 164 | buf.WriteByte('s') // Plural. 165 | } 166 | if x.Tok != token.IMPORT { 167 | buf.WriteByte(' ') 168 | } 169 | writeSpec(buf, s) 170 | } 171 | case *ast.FuncDecl: 172 | buf.WriteString("function ") 173 | buf.WriteString(x.Name.Name) 174 | if x.Type.Params.NumFields() > 0 || x.Type.Results.NumFields() > 0 { 175 | buf.WriteByte(' ') 176 | } 177 | writeSigExpr(buf, x.Type) 178 | } 179 | } 180 | 181 | // writeSep writes a separator that comes before entry i out of total. 182 | // It creates English phrases like "first, second, third, forth and fifth". 183 | func writeSep(buf *bytes.Buffer, i int, total int) { 184 | switch { 185 | case i > 0 && i < total-1: 186 | buf.WriteString(", ") 187 | case i > 0 && i == total-1: 188 | buf.WriteString(" and ") 189 | } 190 | } 191 | 192 | // isValueSpecPlural reports whether spec x is a value spec containing multiple names. 193 | func isValueSpecPlural(x ast.Spec) bool { 194 | v, ok := x.(*ast.ValueSpec) 195 | if !ok { 196 | return false 197 | } 198 | return len(v.Names) > 1 199 | } 200 | 201 | // writeSpec writes the (possibly simplified) English representation for x to buf. 202 | func writeSpec(buf *bytes.Buffer, x ast.Spec) { 203 | switch x := x.(type) { 204 | case *ast.ValueSpec: 205 | for i, n := range x.Names { 206 | if i > 0 { 207 | buf.WriteString(" and ") 208 | } 209 | buf.WriteString(n.Name) 210 | } 211 | 212 | if x.Type != nil { 213 | buf.WriteString(" as ") 214 | writeExpr(buf, x.Type) 215 | } 216 | 217 | switch len(x.Values) { 218 | case 0: // Do nothing. 219 | case 1: 220 | buf.WriteString(" with initial value ") 221 | default: 222 | buf.WriteString(" with initial values ") 223 | } 224 | for i, v := range x.Values { 225 | if i > 0 { 226 | buf.WriteString(" and ") 227 | } 228 | writeExpr(buf, v) 229 | } 230 | case *ast.TypeSpec: 231 | buf.WriteString(x.Name.Name) 232 | buf.WriteString(" as ") 233 | if x.Assign.IsValid() { 234 | buf.WriteString("alias of ") 235 | } 236 | writeExpr(buf, x.Type) 237 | 238 | case *ast.ImportSpec: 239 | buf.WriteString(x.Path.Value) 240 | if x.Name == nil { 241 | break 242 | } 243 | switch x.Name.Name { 244 | default: 245 | buf.WriteString(" as ") 246 | buf.WriteString(x.Name.Name) 247 | case "_": 248 | buf.WriteString(" for side-effects") 249 | } 250 | 251 | default: 252 | fmt.Fprintf(buf, "", x) 253 | } 254 | } 255 | 256 | // writeExpr writes the (possibly simplified) English representation for x to buf. 257 | func writeExpr(buf *bytes.Buffer, x ast.Expr) { 258 | // The AST preserves source-level parentheses so there is 259 | // no need to introduce them here to correct for different 260 | // operator precedences. (This assumes that the AST was 261 | // generated by a Go parser.) 262 | 263 | switch x := x.(type) { 264 | default: 265 | buf.WriteString("(bad expr)") // nil, ast.BadExpr, ast.KeyValueExpr 266 | 267 | case *ast.Ident: 268 | buf.WriteString(x.Name) 269 | 270 | case *ast.Ellipsis: 271 | buf.WriteString("...") 272 | if x.Elt != nil { 273 | writeExpr(buf, x.Elt) 274 | } 275 | 276 | case *ast.BasicLit: 277 | buf.WriteString(x.Value) 278 | 279 | case *ast.FuncLit: 280 | buf.WriteByte('(') 281 | writeExpr(buf, x.Type) 282 | buf.WriteString(" literal)") // simplified 283 | 284 | case *ast.CompositeLit: 285 | buf.WriteByte('(') 286 | writeExpr(buf, x.Type) 287 | buf.WriteString(" literal)") // simplified 288 | 289 | case *ast.ParenExpr: 290 | buf.WriteByte('(') 291 | writeExpr(buf, x.X) 292 | buf.WriteByte(')') 293 | 294 | case *ast.SelectorExpr: 295 | writeExpr(buf, x.X) 296 | buf.WriteByte('.') 297 | buf.WriteString(x.Sel.Name) 298 | 299 | case *ast.IndexExpr: 300 | writeExpr(buf, x.X) 301 | buf.WriteByte('[') 302 | writeExpr(buf, x.Index) 303 | buf.WriteByte(']') 304 | 305 | case *ast.SliceExpr: 306 | writeExpr(buf, x.X) 307 | buf.WriteByte('[') 308 | if x.Low != nil { 309 | writeExpr(buf, x.Low) 310 | } 311 | buf.WriteByte(':') 312 | if x.High != nil { 313 | writeExpr(buf, x.High) 314 | } 315 | if x.Slice3 { 316 | buf.WriteByte(':') 317 | if x.Max != nil { 318 | writeExpr(buf, x.Max) 319 | } 320 | } 321 | buf.WriteByte(']') 322 | 323 | case *ast.TypeAssertExpr: 324 | writeExpr(buf, x.X) 325 | buf.WriteString(".(") 326 | writeExpr(buf, x.Type) 327 | buf.WriteByte(')') 328 | 329 | case *ast.CallExpr: 330 | writeExpr(buf, x.Fun) 331 | buf.WriteByte('(') 332 | for i, arg := range x.Args { 333 | if i > 0 { 334 | buf.WriteString(", ") 335 | } 336 | writeExpr(buf, arg) 337 | } 338 | if x.Ellipsis.IsValid() { 339 | buf.WriteString("...") 340 | } 341 | buf.WriteByte(')') 342 | 343 | case *ast.StarExpr: 344 | buf.WriteString("pointer to ") 345 | writeExpr(buf, x.X) 346 | 347 | case *ast.UnaryExpr: 348 | switch x.Op { 349 | default: 350 | buf.WriteString(x.Op.String()) 351 | case token.AND: 352 | buf.WriteString("address of ") 353 | } 354 | writeExpr(buf, x.X) 355 | 356 | case *ast.BinaryExpr: 357 | writeExpr(buf, x.X) 358 | buf.WriteByte(' ') 359 | switch x.Op { 360 | default: 361 | buf.WriteString(x.Op.String()) 362 | case token.ADD: 363 | buf.WriteString("plus") 364 | case token.SUB: 365 | buf.WriteString("minus") 366 | case token.QUO: 367 | buf.WriteString("divided by") 368 | } 369 | buf.WriteByte(' ') 370 | writeExpr(buf, x.Y) 371 | 372 | case *ast.ArrayType: 373 | if x.Len == nil { 374 | buf.WriteString("slice of ") 375 | } else { 376 | writeExpr(buf, x.Len) 377 | buf.WriteString("-element array of ") 378 | } 379 | writeExpr(buf, x.Elt) 380 | 381 | case *ast.StructType: 382 | buf.WriteString("struct{") 383 | writeFieldList(buf, x.Fields, "; ", false) 384 | buf.WriteByte('}') 385 | 386 | case *ast.FuncType: 387 | buf.WriteString("function") 388 | if x.Params.NumFields() > 0 || x.Results.NumFields() > 0 { 389 | buf.WriteByte(' ') 390 | } 391 | writeSigExpr(buf, x) 392 | 393 | case *ast.InterfaceType: 394 | buf.WriteString("interface{") 395 | writeFieldList(buf, x.Methods, "; ", true) 396 | buf.WriteByte('}') 397 | 398 | case *ast.MapType: 399 | buf.WriteString("map of ") 400 | writeExpr(buf, x.Key) 401 | buf.WriteString(" to ") 402 | writeExpr(buf, x.Value) 403 | 404 | case *ast.ChanType: 405 | var s string 406 | switch x.Dir { 407 | case ast.SEND: 408 | s = "chan<- " 409 | case ast.RECV: 410 | s = "<-chan " 411 | default: 412 | s = "chan " 413 | } 414 | buf.WriteString(s) 415 | writeExpr(buf, x.Value) 416 | } 417 | } 418 | 419 | // writeSigExpr writes the (possibly simplified) English representation 420 | // for a function signature to buf. 421 | func writeSigExpr(buf *bytes.Buffer, sig *ast.FuncType) { 422 | if sig.Params.NumFields() > 0 { 423 | buf.WriteString("taking ") 424 | writeFieldList(buf, sig.Params, " and ", false) 425 | } 426 | if sig.Params.NumFields() > 0 && sig.Results.NumFields() > 0 { 427 | buf.WriteString(" and ") 428 | } 429 | if sig.Results.NumFields() > 0 { 430 | buf.WriteString("returning ") 431 | writeFieldList(buf, sig.Results, " and ", false) 432 | } 433 | } 434 | 435 | // writeFieldList writes the (possibly simplified) English representation 436 | // for a field list to buf. 437 | func writeFieldList(buf *bytes.Buffer, fields *ast.FieldList, sep string, iface bool) { 438 | for i, f := range fields.List { 439 | if i > 0 { 440 | buf.WriteString(sep) 441 | } 442 | 443 | // Field list names. 444 | for i, name := range f.Names { 445 | if i > 0 { 446 | buf.WriteString(", ") 447 | } 448 | buf.WriteString(name.Name) 449 | } 450 | 451 | // Types of interface methods consist of signatures only. 452 | if sig, _ := f.Type.(*ast.FuncType); sig != nil && iface { 453 | writeSigExpr(buf, sig) 454 | continue 455 | } 456 | 457 | // Named fields are separated with a blank from the field type. 458 | if len(f.Names) > 0 { 459 | buf.WriteByte(' ') 460 | } 461 | 462 | writeExpr(buf, f.Type) 463 | 464 | // Ignore tag. 465 | } 466 | } 467 | -------------------------------------------------------------------------------- /decl/decl_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package decl_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/shurcooL/godecl/decl" 11 | ) 12 | 13 | func TestGoToEnglish(t *testing.T) { 14 | tests := []struct { 15 | in string 16 | want string 17 | }{ 18 | { 19 | "", 20 | "", 21 | }, 22 | { 23 | "var x int", 24 | "declare variable x as int", 25 | }, 26 | { 27 | "var x, y int", 28 | "declare variables x and y as int", 29 | }, 30 | { 31 | "var x int = 1", 32 | "declare variable x as int with initial value 1", 33 | }, 34 | { 35 | "var x, y int = 1, 2", 36 | "declare variables x and y as int with initial values 1 and 2", 37 | }, 38 | { 39 | "var x = 1", 40 | "declare variable x with initial value 1", 41 | }, 42 | { 43 | "var x, y = 1, 2", 44 | "declare variables x and y with initial values 1 and 2", 45 | }, 46 | { 47 | "var (x int; y string)", 48 | "declare variable x as int and variable y as string", 49 | }, 50 | { 51 | "var (x, y int; a, b string)", 52 | "declare variables x and y as int and variables a and b as string", 53 | }, 54 | { 55 | "x := 1", 56 | "short declare variable x with initial value 1", 57 | }, 58 | { 59 | "x, y := 1, 2", 60 | "short declare variables x and y with initial values 1 and 2", 61 | }, 62 | 63 | { 64 | "*[]map[int]string", 65 | "pointer to slice of map of int to string", 66 | }, 67 | { 68 | "var x *[]map[int][2]string", 69 | "declare variable x as pointer to slice of map of int to 2-element array of string", 70 | }, 71 | { 72 | "**[][]*map[int32][][3]string", 73 | "pointer to pointer to slice of slice of pointer to map of int32 to slice of 3-element array of string", 74 | }, 75 | { 76 | "func(string, bool) (int, error)", 77 | "function taking string and bool and returning int and error", 78 | }, 79 | { 80 | "var x, y int = (2+5) / 3, 4", 81 | "declare variables x and y as int with initial values (2 plus 5) divided by 3 and 4", 82 | }, 83 | { 84 | "var x func() *[5]*func() rune", 85 | "declare variable x as function returning pointer to 5-element array of pointer to function returning rune", 86 | }, 87 | 88 | { 89 | "a = 5", 90 | "assign to a the value 5", 91 | }, 92 | { 93 | "a, b = 5, 6", 94 | "assign to a and b the values 5 and 6", 95 | }, 96 | 97 | { 98 | "&a", 99 | "address of a", 100 | }, 101 | { 102 | "var x *int = &a", 103 | "declare variable x as pointer to int with initial value address of a", 104 | }, 105 | 106 | { 107 | `import "fmt"`, 108 | `import package "fmt"`, 109 | }, 110 | { 111 | `import myfmt "fmt"`, 112 | `import package "fmt" as myfmt`, 113 | }, 114 | { 115 | `import ("fmt"; "net/http"; _ "image/png")`, 116 | `import packages "fmt", "net/http" and "image/png" for side-effects`, 117 | }, 118 | 119 | { 120 | "func Foo()", 121 | "function Foo", 122 | }, 123 | { 124 | "func Foo() {}", 125 | "function Foo", 126 | }, 127 | { 128 | "func Foo(x int) string", 129 | "function Foo taking x int and returning string", 130 | }, 131 | 132 | { 133 | "type T1 T2", 134 | "declare type T1 as T2", 135 | }, 136 | { 137 | "type T1 = T2", 138 | "declare type T1 as alias of T2", 139 | }, 140 | } 141 | for _, tc := range tests { 142 | got, err := decl.GoToEnglish(tc.in) 143 | if err != nil { 144 | t.Error("got error:", err) 145 | continue 146 | } 147 | if got != tc.want { 148 | t.Errorf("\ngot: %q\nwant: %q\n", got, tc.want) 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /examples.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | // examples is a list of example input values. On page load, 8 | // one of these is randomly chosen to display to the user. 9 | // These should be gofmted, to keep the examples nicer. 10 | var examples = []string{ 11 | "var x *[]map[int][2]string", 12 | "var x func() *[5]*func() rune", 13 | "var x, y int = 1, 2", 14 | "var x = (2+5)/3.0 + 4", 15 | "type T1 = T2", 16 | "var x *int = &a", 17 | 18 | // TODO: Add more fun and interesting example inputs. 19 | // See decl tests for inspiration. 20 | } 21 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "go/format" 9 | "testing" 10 | ) 11 | 12 | // Ensure all examples are nicely gofmted. 13 | func TestExamplesGofmt(t *testing.T) { 14 | for _, example := range examples { 15 | gofmted, err := format.Source([]byte(example)) 16 | if err != nil { 17 | t.Errorf("failed to gofmt example %q:\n%v", example, err) 18 | continue 19 | } 20 | if example != string(gofmted) { 21 | t.Errorf("\nexample %q is not gofmted\n want: %q", example, gofmted) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shurcooL/godecl 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /godecl.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // A godecl experiment. Like cdecl, but for Go. 6 | // 7 | // https://godecl.org 8 | // 9 | // Inspired by @bradfitz at https://twitter.com/bradfitz/status/833048466456600576. 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "math/rand" 15 | "net/url" 16 | "os" 17 | "strings" 18 | "time" 19 | 20 | "github.com/gopherjs/gopherjs/js" 21 | "github.com/shurcooL/godecl/decl" 22 | "honnef.co/go/js/dom" 23 | ) 24 | 25 | func init() { 26 | rand.Seed(time.Now().UnixNano()) 27 | } 28 | 29 | func main() { 30 | if js.Global == nil || js.Global.Get("document") == js.Undefined { 31 | fmt.Fprintln(os.Stderr, `Where's the DOM at? It looks like you're running godecl in an environment 32 | where the DOM is not available. You'll need to run it inside a browser.`) 33 | os.Exit(1) 34 | } 35 | 36 | document := dom.GetWindow().Document() 37 | c := context{ 38 | input: document.GetElementByID("input").(*dom.HTMLInputElement), 39 | output: document.GetElementByID("output").(*dom.HTMLDivElement), 40 | permalink: document.GetElementByID("permalink").(*dom.HTMLAnchorElement), 41 | issue: document.GetElementByID("issue").(*dom.HTMLAnchorElement), 42 | } 43 | c.SetInitialInput() 44 | c.Update() 45 | c.input.AddEventListener("input", false, func(dom.Event) { 46 | deleteQuery() 47 | c.Update() 48 | }) 49 | c.permalink.AddEventListener("click", false, func(e dom.Event) { 50 | me, ok := e.(*dom.MouseEvent) 51 | if !ok { 52 | me = e.(*dom.PointerEvent).MouseEvent 53 | } 54 | if me.CtrlKey || me.AltKey || me.MetaKey || me.ShiftKey { 55 | // Only override normal clicks. 56 | return 57 | } 58 | setQuery(c.input.Value) 59 | e.PreventDefault() 60 | }) 61 | document.GetElementByID("random").AddEventListener("click", false, func(e dom.Event) { 62 | me, ok := e.(*dom.MouseEvent) 63 | if !ok { 64 | me = e.(*dom.PointerEvent).MouseEvent 65 | } 66 | if me.CtrlKey || me.AltKey || me.MetaKey || me.ShiftKey { 67 | // Only override normal clicks. 68 | return 69 | } 70 | deleteQuery() 71 | // Pick a random example (but avoid picking same as current input). 72 | for { 73 | new := examples[rand.Intn(len(examples))] 74 | if new == c.input.Value { 75 | 76 | continue 77 | } 78 | c.input.Value = new 79 | break 80 | } 81 | c.Update() 82 | e.PreventDefault() 83 | }) 84 | } 85 | 86 | type context struct { 87 | input *dom.HTMLInputElement 88 | output *dom.HTMLDivElement 89 | permalink *dom.HTMLAnchorElement 90 | issue *dom.HTMLAnchorElement 91 | } 92 | 93 | // SetInitialInput sets initial input value. 94 | func (c context) SetInitialInput() { 95 | if c.input.Value != "" { 96 | // If the user has managed to already type some input, don't steamroll over it. 97 | return 98 | } 99 | query, _ := url.ParseQuery(strings.TrimPrefix(dom.GetWindow().Location().Search, "?")) 100 | q, ok := query["q"] 101 | if !ok { 102 | // Random initial example. 103 | c.input.Value = examples[rand.Intn(len(examples))] 104 | c.input.Focus() 105 | return 106 | } 107 | c.input.Value = q[0] 108 | c.input.Focus() 109 | } 110 | 111 | // Update updates the output, permalink anchor href, 112 | // and issue anchor href, based on current input. 113 | func (c context) Update() { 114 | c.updateOutput() 115 | c.updatePermalink() 116 | c.updateIssue() 117 | } 118 | 119 | // updateOutput updates the output text based on current input. 120 | func (c context) updateOutput() { 121 | out, err := decl.GoToEnglish(c.input.Value) 122 | if err != nil { 123 | c.output.SetTextContent("error: " + err.Error()) 124 | return 125 | } 126 | c.output.SetTextContent(out) 127 | } 128 | 129 | // updatePermalink updates the permalink anchor href based on current input. 130 | func (c context) updatePermalink() { 131 | v := url.Values{} 132 | v.Set("q", c.input.Value) 133 | url := url.URL{RawQuery: v.Encode()} 134 | c.permalink.Href = url.String() 135 | } 136 | 137 | // updateIssue updates the "report an issue" anchor href based on current input. 138 | func (c context) updateIssue() { 139 | v := url.Values{} 140 | v.Set("title", fmt.Sprintf("decl: Unexpected handling of %q.", c.input.Value)) 141 | v.Set("body", fmt.Sprintf(`### What did you do? 142 | 143 | I typed the following input at godecl.org: 144 | 145 | `+"```Go"+` 146 | %v 147 | `+"```"+` 148 | 149 | ### What did you expect to see? 150 | 151 | I expected to see ... 152 | 153 | ### What did you see instead? 154 | 155 | `+"```"+` 156 | %v 157 | `+"```"+` 158 | `, c.input.Value, c.output.TextContent())) 159 | url := url.URL{ 160 | Scheme: "https", Host: "github.com", Path: "/shurcooL/godecl/issues/new", 161 | RawQuery: v.Encode(), 162 | } 163 | c.issue.Href = url.String() 164 | } 165 | 166 | // setQuery sets q in the window URL query to value. 167 | func setQuery(value string) { 168 | url, err := url.Parse(dom.GetWindow().Location().Href) 169 | if err != nil { 170 | // We don't expect this can ever happen, so treat it as an internal error if it does. 171 | panic(fmt.Errorf("internal error: parsing window.location.href as URL failed: %v", err)) 172 | } 173 | query := url.Query() 174 | query.Set("q", value) 175 | url.RawQuery = query.Encode() 176 | // TODO: dom.GetWindow().History().ReplaceState(...), blocked on https://github.com/dominikh/go-js-dom/issues/41. 177 | js.Global.Get("window").Get("history").Call("replaceState", nil, nil, url.String()) 178 | } 179 | 180 | // deleteQuery deletes q in the window URL query. 181 | func deleteQuery() { 182 | url, err := url.Parse(dom.GetWindow().Location().Href) 183 | if err != nil { 184 | // We don't expect this can ever happen, so treat it as an internal error if it does. 185 | panic(fmt.Errorf("internal error: parsing window.location.href as URL failed: %v", err)) 186 | } 187 | query := url.Query() 188 | query.Del("q") 189 | url.RawQuery = query.Encode() 190 | // TODO: dom.GetWindow().History().ReplaceState(...), blocked on https://github.com/dominikh/go-js-dom/issues/41. 191 | js.Global.Get("window").Get("history").Call("replaceState", nil, nil, url.String()) 192 | } 193 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | godecl 4 | 40 | 41 |
42 |

godecl

43 | 44 |

Go syntax → English

45 | 46 | 47 | 48 | 53 | 54 | 63 | 64 | 65 |
66 | 67 | --------------------------------------------------------------------------------