├── .gitignore ├── LICENSE ├── README.md ├── appengine ├── .gcloudignore ├── .gitignore ├── Pipfile ├── Pipfile.lock ├── README.md ├── app.yaml └── main.go ├── cmd ├── runes │ ├── .gitignore │ ├── README.md │ ├── cli.go │ └── cli_test.go └── runeweb │ ├── .gitignore │ └── main.go ├── filter.go ├── filter_test.go ├── go.mod ├── go.sum ├── img └── runefinder-runeweb.png ├── index.go ├── index_test.go └── web.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # go-bindata files 4 | bindata.go 5 | 6 | # IDEs 7 | .idea 8 | 9 | # Binaries for programs and plugins 10 | *.exe 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, build with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 22 | .glide/ 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Stand-up Programmer 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (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. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # runefinder 2 | 3 | A tool for finding Unicode characters by name. Command-line and Web interfaces. 4 | 5 | ## Building 6 | 7 | To compile the `runes` command, in the `cmd/runes` directory: 8 | 9 | ``` 10 | $ cd cmd/runes/ 11 | $ go build 12 | $ ls -lh runes 13 | -rwxrwxr-x 1 luciano luciano 2,6M Mar 7 00:11 runes 14 | ``` 15 | 16 | To use `runes`, provide one or more words to search: 17 | 18 | ``` 19 | $ ./runes party 20 | U+1F389 🎉 PARTY POPPER 21 | 1 character found 22 | 23 | $ ./runes cat eyes 24 | U+1F638 😸 GRINNING CAT FACE WITH SMILING EYES 25 | U+1F63B 😻 SMILING CAT FACE WITH HEART-SHAPED EYES 26 | U+1F63D 😽 KISSING CAT FACE WITH CLOSED EYES 27 | 3 characters found 28 | ``` 29 | 30 | 31 | ## Optional Web interface 32 | 33 | The `runeweb` command starts a local HTTP server on port 8000 offering a simple Web 34 | interface for searching. This is the best way to use `runefinder` on Windows until 35 | Microsoft improves the Unicode coverage of the fonts used in in cmd.exe or PowerShell. 36 | 37 | Run the server: 38 | 39 | ``` 40 | $ cd cmd/runeweb/ 41 | $ go run runeweb.go 42 | Serving on: localhost:8000 43 | ``` 44 | 45 | Open `http://localhost:8000` on your browser and search: 46 | 47 |  48 | 49 | 50 | ## Web interface on Google App Engine 51 | 52 | The `appengine/` directory has the `main.go` and configuration files for running Runefinder on the Google App Engine platform. 53 | 54 | Link to running app: [runefinder2018.appspot.com](https://runefinder2018.appspot.com/) 55 | -------------------------------------------------------------------------------- /appengine/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Binaries for programs and plugins 17 | *.exe 18 | *.exe~ 19 | *.dll 20 | *.so 21 | *.dylib 22 | # Test binary, build with `go test -c` 23 | *.test 24 | # Output of the go coverage tool, specifically when used with LiteIDE 25 | *.out -------------------------------------------------------------------------------- /appengine/.gitignore: -------------------------------------------------------------------------------- 1 | appengine 2 | -------------------------------------------------------------------------------- /appengine/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | 8 | [dev-packages] -------------------------------------------------------------------------------- /appengine/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "5f0257fe8c7a73db1c8de519faa92c658282a01087eb2bfafba7962704c23e27" 5 | }, 6 | "host-environment-markers": { 7 | "implementation_name": "cpython", 8 | "implementation_version": "0", 9 | "os_name": "posix", 10 | "platform_machine": "x86_64", 11 | "platform_python_implementation": "CPython", 12 | "platform_release": "17.5.0", 13 | "platform_system": "Darwin", 14 | "platform_version": "Darwin Kernel Version 17.5.0: Mon Mar 5 22:24:32 PST 2018; root:xnu-4570.51.1~1/RELEASE_X86_64", 15 | "python_full_version": "2.7.14", 16 | "python_version": "2.7", 17 | "sys_platform": "darwin" 18 | }, 19 | "pipfile-spec": 6, 20 | "requires": {}, 21 | "sources": [ 22 | { 23 | "name": "pypi", 24 | "url": "https://pypi.python.org/simple", 25 | "verify_ssl": true 26 | } 27 | ] 28 | }, 29 | "default": {}, 30 | "develop": {} 31 | } 32 | -------------------------------------------------------------------------------- /appengine/README.md: -------------------------------------------------------------------------------- 1 | # runefinder 2 | 3 | A simple Web app for finding Unicode characters by name. Runs on Google AppEngine. 4 | 5 | See it online: [runefinder2018.appspot.com](https://runefinder2018.appspot.com/) 6 | 7 | 8 | ## Local testing 9 | 10 | Using `pipenv` to run the ancient Python 2.7 required by Google: 11 | 12 | ``` 13 | $ pipenv --two run dev_appserver.py app.yaml --enable_watching_go_path False 14 | ``` 15 | 16 | 17 | ## Deploy to AppEngine and visit it there 18 | 19 | ``` 20 | $ gcloud app deploy 21 | $ gcloud app browse 22 | ``` 23 | -------------------------------------------------------------------------------- /appengine/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go114 2 | 3 | handlers: 4 | - url: /.* 5 | script: auto 6 | -------------------------------------------------------------------------------- /appengine/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/standupdev/runefinder" 9 | ) 10 | 11 | func main() { 12 | http.HandleFunc("/", runefinder.Home) 13 | port := getPort() 14 | log.Printf("listening on :%s", port) 15 | if err := http.ListenAndServe(":"+port, nil); err != nil { 16 | log.Fatal(err) 17 | } 18 | } 19 | 20 | func getPort() string { 21 | port := os.Getenv("PORT") 22 | if port == "" { 23 | port = "8080" 24 | } 25 | return port 26 | } 27 | -------------------------------------------------------------------------------- /cmd/runes/.gitignore: -------------------------------------------------------------------------------- 1 | runes 2 | -------------------------------------------------------------------------------- /cmd/runes/README.md: -------------------------------------------------------------------------------- 1 | # runes 2 | 3 | A command-line utility to find Unicode characters by name 4 | 5 | ## Usage 6 | 7 | To find Unicode characters, run `runes` with words as arguments: 8 | 9 | ``` 10 | $ ./runes cat face 11 | U+1F431 🐱 CAT FACE 12 | U+1F638 😸 GRINNING CAT FACE WITH SMILING EYES 13 | U+1F639 😹 CAT FACE WITH TEARS OF JOY 14 | U+1F63A 😺 SMILING CAT FACE WITH OPEN MOUTH 15 | U+1F63B 😻 SMILING CAT FACE WITH HEART-SHAPED EYES 16 | U+1F63C 😼 CAT FACE WITH WRY SMILE 17 | U+1F63D 😽 KISSING CAT FACE WITH CLOSED EYES 18 | U+1F63E 😾 POUTING CAT FACE 19 | U+1F63F 😿 CRYING CAT FACE 20 | U+1F640 🙀 WEARY CAT FACE 21 | 10 characters found 22 | ``` 23 | 24 | Use more words to narrow the results: 25 | 26 | ``` 27 | $ ./runes cat face eyes 28 | U+1F638 😸 GRINNING CAT FACE WITH SMILING EYES 29 | U+1F63B 😻 SMILING CAT FACE WITH HEART-SHAPED EYES 30 | U+1F63D 😽 KISSING CAT FACE WITH CLOSED EYES 31 | 3 characters found 32 | ``` 33 | -------------------------------------------------------------------------------- /cmd/runes/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/standupdev/runefinder" 9 | "github.com/standupdev/runeset" 10 | "golang.org/x/text/unicode/runenames" 11 | ) 12 | 13 | func display(s runeset.Set) { 14 | count := len(s) 15 | for _, c := range s.Sorted() { 16 | name := runenames.Name(c) 17 | fmt.Printf("%U\t%[1]c\t%s\n", c, name) 18 | } 19 | var msg string 20 | switch count { 21 | case 0: 22 | msg = "no character found" 23 | case 1: 24 | msg = "1 character found" 25 | default: 26 | msg = fmt.Sprintf("%d characters found", count) 27 | } 28 | fmt.Println(msg) 29 | } 30 | 31 | func main() { 32 | query := strings.TrimSpace(strings.Join(os.Args[1:], " ")) 33 | if len(query) == 0 { 34 | fmt.Println("Please provide one or more words or characters to search.") 35 | return 36 | } 37 | nameChars := runeset.MakeFromString(" -0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") 38 | charQuery := false 39 | for _, char := range query { 40 | if !nameChars.Contains(char) { 41 | charQuery = true 42 | break 43 | } 44 | } 45 | if charQuery { 46 | display(runeset.MakeFromString(query)) 47 | } else { 48 | index := runefinder.BuildIndex() 49 | display(runefinder.Filter(index, query)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cmd/runes/cli_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func Example() { 8 | oldArgs := os.Args 9 | defer func() { os.Args = oldArgs }() 10 | os.Args = []string{"", "EIGHTHS", "fraction"} 11 | main() 12 | // Output: 13 | // U+215C ⅜ VULGAR FRACTION THREE EIGHTHS 14 | // U+215D ⅝ VULGAR FRACTION FIVE EIGHTHS 15 | // U+215E ⅞ VULGAR FRACTION SEVEN EIGHTHS 16 | // 3 characters found 17 | } 18 | 19 | func Example_single_result() { 20 | oldArgs := os.Args 21 | defer func() { os.Args = oldArgs }() 22 | os.Args = []string{"", "registered"} 23 | main() 24 | // Output: 25 | // U+00AE ® REGISTERED SIGN 26 | // 1 character found 27 | } 28 | 29 | func Example_no_result() { 30 | oldArgs := os.Args 31 | defer func() { os.Args = oldArgs }() 32 | os.Args = []string{"", "nosuchcharacter"} 33 | main() 34 | // Output: 35 | // no character found 36 | } 37 | 38 | func Example_get_names() { 39 | oldArgs := os.Args 40 | defer func() { os.Args = oldArgs }() 41 | os.Args = []string{"", "?abc"} 42 | main() 43 | // Output: 44 | // U+003F ? QUESTION MARK 45 | // U+0061 a LATIN SMALL LETTER A 46 | // U+0062 b LATIN SMALL LETTER B 47 | // U+0063 c LATIN SMALL LETTER C 48 | // 4 characters found 49 | } 50 | 51 | func Example_no_args() { 52 | oldArgs := os.Args 53 | defer func() { os.Args = oldArgs }() 54 | os.Args = []string{""} 55 | main() 56 | // Output: 57 | // Please provide one or more words or characters to search. 58 | } 59 | -------------------------------------------------------------------------------- /cmd/runeweb/.gitignore: -------------------------------------------------------------------------------- 1 | runeweb 2 | -------------------------------------------------------------------------------- /cmd/runeweb/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/standupdev/runefinder" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | const hostAddr = "localhost:8000" 11 | 12 | func main() { 13 | fmt.Println("Serving on:", hostAddr) 14 | handler := http.HandlerFunc(runefinder.Home) 15 | log.Fatal(http.ListenAndServe(hostAddr, handler)) 16 | } 17 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package runefinder 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/standupdev/runeset" 7 | ) 8 | 9 | // Filter takes a runefinder.Index and a query; returns a matching set of runes. 10 | func Filter(index Index, query string) (result runeset.Set) { 11 | query = strings.Replace(query, "-", " ", -1) 12 | query = strings.ToUpper(query) 13 | words := strings.Fields(query) 14 | chars, found := index[words[0]] 15 | if !found { 16 | return runeset.Set{} 17 | } 18 | result = chars.Copy() 19 | for _, word := range words[1:] { 20 | chars, found := index[word] 21 | if !found { 22 | return runeset.Set{} 23 | } 24 | result.IntersectionUpdate(chars) 25 | if len(result) == 0 { 26 | break 27 | } 28 | } 29 | return result 30 | } 31 | -------------------------------------------------------------------------------- /filter_test.go: -------------------------------------------------------------------------------- 1 | package runefinder 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/standupdev/runeset" 8 | ) 9 | 10 | func TestFilter(t *testing.T) { 11 | var testCases = []struct { 12 | query string 13 | want runeset.Set 14 | }{ 15 | {"Registered", runeset.Make('®')}, 16 | {"ORDINAL", runeset.Make('ª', 'º')}, 17 | {"fraction eighths", runeset.Make('⅜', '⅝', '⅞')}, 18 | {"fraction eighths bang", runeset.Set{}}, 19 | {"fraction eighths five", runeset.Make('⅝')}, 20 | {"NoSuchRune", runeset.Set{}}, 21 | } 22 | for _, tc := range testCases { 23 | t.Run(tc.query, func(t *testing.T) { 24 | got := Filter(index, tc.query) 25 | if !reflect.DeepEqual(tc.want, got) { 26 | t.Errorf("query: %q\twant: %q\tgot: %q", 27 | tc.query, tc.want, got) 28 | } 29 | }) 30 | } 31 | } 32 | 33 | func TestFilter_hyphenatedQuery(t *testing.T) { 34 | query := "HYPHEN-MINUS" 35 | want := '-' 36 | got := Filter(index, query) 37 | if !got.Contains(want) { 38 | t.Errorf("query: %q\t%q absent, got: %v", 39 | query, want, got) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/standupdev/runefinder 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/standupdev/runeset v1.0.0 7 | golang.org/x/text v0.3.4 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/standupdev/runeset v1.0.0 h1:aKS5ummq5Mc3CtXOhXdyE29mSL1/LUL9Dg0PnaV8BSQ= 2 | github.com/standupdev/runeset v1.0.0/go.mod h1:tmBj7QsZ/7uaayqaql2sSLpaHrWQ6MLC1IXaKU9q9To= 3 | golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= 4 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 5 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 6 | -------------------------------------------------------------------------------- /img/runefinder-runeweb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramalho/runefinder/a3fdd8b3117e45f438da996fe25cf721354d3cda/img/runefinder-runeweb.png -------------------------------------------------------------------------------- /index.go: -------------------------------------------------------------------------------- 1 | package runefinder 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/standupdev/runeset" 7 | "golang.org/x/text/unicode/runenames" 8 | ) 9 | 10 | // Index is an inverted index: a mapping of words to 11 | // Unicode characters with those words in their names 12 | type Index map[string]runeset.Set 13 | 14 | const ( 15 | firstChar rune = 0x20 16 | lastChar rune = 0x10FFFF // http://unicode.org/faq/utf_bom.html 17 | ) 18 | 19 | func parseName(name string) []string { 20 | name = strings.Replace(name, "-", " ", -1) 21 | words := []string{} 22 | for _, word := range strings.Fields(name) { 23 | words = append(words, word) 24 | } 25 | return words 26 | } 27 | 28 | //buildIndex builds the inverted index for the given range of runes 29 | func buildIndex(first, last rune) (index Index) { 30 | index = Index{} 31 | for char := first; char <= last; char++ { 32 | name := runenames.Name(char) 33 | if len(name) > 0 { 34 | for _, word := range parseName(name) { 35 | runes, found := index[word] 36 | if found { 37 | runes.Add(char) 38 | } else { 39 | index[word] = runeset.Make(char) 40 | } 41 | } 42 | } 43 | } 44 | return index 45 | } 46 | 47 | //BuildIndex builds the inverted index for all runes 48 | func BuildIndex() (index Index) { 49 | return buildIndex(firstChar, lastChar) 50 | } 51 | -------------------------------------------------------------------------------- /index_test.go: -------------------------------------------------------------------------------- 1 | package runefinder 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/standupdev/runeset" 8 | ) 9 | 10 | func TestParseName(t *testing.T) { 11 | var testCases = []struct { 12 | name string 13 | words []string 14 | }{ 15 | {"EXCLAMATION MARK", 16 | []string{"EXCLAMATION", "MARK"}}, 17 | {"HYPHEN-MINUS", 18 | []string{"HYPHEN", "MINUS"}}, 19 | } 20 | for _, tc := range testCases { 21 | t.Run(tc.name, func(t *testing.T) { 22 | got := parseName(tc.name) 23 | if !reflect.DeepEqual(got, tc.words) { 24 | t.Errorf("\nParseName(%q)\nwant -> %q\ngot -> %q", 25 | tc.name, tc.words, got) 26 | } 27 | }) 28 | } 29 | } 30 | 31 | func TestBuildIndex_twoLines(t *testing.T) { 32 | // 003D;EQUALS SIGN;Sm;0;ON;;;;;N;;;;; 33 | // 003E;GREATER-THAN SIGN;Sm;0;ON;;;;;Y;;;;; 34 | index := buildIndex(0x3D, 0x3E) 35 | wantWords := Index{ 36 | "EQUALS": runeset.Make('='), 37 | "GREATER": runeset.Make('>'), 38 | "THAN": runeset.Make('>'), 39 | "SIGN": runeset.Make('=', '>'), 40 | } 41 | if !reflect.DeepEqual(wantWords, index) { 42 | t.Errorf("want: %v\n got: %v", wantWords, index) 43 | } 44 | } 45 | 46 | func TestBuildIndex_threeLines(t *testing.T) { 47 | // 0041;LATIN CAPITAL LETTER A;Lu;0;L;;;;;N;;;;0061; 48 | // 0042;LATIN CAPITAL LETTER B;Lu;0;L;;;;;N;;;;0062; 49 | // 0043;LATIN CAPITAL LETTER C;Lu;0;L;;;;;N;;;;0063; 50 | index := buildIndex(0x41, 0x43) 51 | wantWords := Index{ 52 | "A": runeset.Make('A'), 53 | "B": runeset.Make('B'), 54 | "C": runeset.Make('C'), 55 | "LATIN": runeset.MakeFromString("ABC"), 56 | "CAPITAL": runeset.MakeFromString("ABC"), 57 | "LETTER": runeset.MakeFromString("ABC"), 58 | } 59 | if !reflect.DeepEqual(wantWords, index) { 60 | t.Errorf("want: %v\n got: %v", wantWords, index) 61 | } 62 | } 63 | 64 | var registeredSign rune = 0xAE // ® 65 | 66 | func TestUnicodeDataIndex_Words(t *testing.T) { 67 | index := BuildIndex() 68 | wantWords := 10000 69 | if len(index) < wantWords { 70 | t.Errorf("len(index.Chars) < %d\t got: %d", wantWords, len(index)) 71 | } 72 | wantSet := runeset.Make(registeredSign) 73 | gotSet := index["REGISTERED"] 74 | if !reflect.DeepEqual(wantSet, gotSet) { 75 | t.Errorf("want: %v\t got: %v", wantSet, gotSet) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /web.go: -------------------------------------------------------------------------------- 1 | package runefinder 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/standupdev/runeset" 10 | "golang.org/x/text/unicode/runenames" 11 | ) 12 | 13 | const sampleWords = `bismillah box cat chess circle circled 14 | Egyptian face hexagram key Malayalam Roman symbol` 15 | 16 | var ( 17 | form = template.Must(template.New("form").Parse(formHTML)) 18 | index = BuildIndex() 19 | links = makeLinks(sampleWords) 20 | ) 21 | 22 | // Link represents an HTML link 23 | type Link struct { 24 | Location template.URL 25 | Text string 26 | } 27 | 28 | func makeLinks(text string) []Link { 29 | links := []Link{} 30 | for _, word := range strings.Fields(text) { 31 | location := template.URL("/?q=" + word) 32 | links = append(links, Link{location, word}) 33 | } 34 | return links 35 | } 36 | 37 | func makeMessage(count int) string { 38 | switch count { 39 | case 0: 40 | return "No character found." 41 | case 1: 42 | return "1 character found." 43 | default: 44 | return fmt.Sprintf("%d characters found", count) 45 | } 46 | } 47 | 48 | func getName(char rune) string { 49 | name := runenames.Name(char) 50 | if len(name) == 0 { 51 | name = "(no name)" 52 | } 53 | return name 54 | } 55 | 56 | // RuneRecord holds data about one Unicode character 57 | type RuneRecord struct { 58 | Code string 59 | Char string 60 | Name string 61 | } 62 | 63 | func makeResults(chars runeset.Set) []RuneRecord { 64 | result := []RuneRecord{} 65 | for _, char := range chars.Sorted() { 66 | result = append(result, RuneRecord{ 67 | Code: fmt.Sprintf("%U", char), 68 | Char: string(char), 69 | Name: getName(char), 70 | }) 71 | } 72 | return result 73 | } 74 | 75 | // Home handles the form in the homepage 76 | func Home(w http.ResponseWriter, req *http.Request) { 77 | chars := runeset.Set{} 78 | msg := "" 79 | query := strings.TrimSpace(req.URL.Query().Get("q")) 80 | if len(query) > 0 { 81 | chars = Filter(index, query) 82 | msg = makeMessage(len(chars)) 83 | } 84 | data := struct { 85 | Links []Link 86 | Query string 87 | Message string 88 | Result []RuneRecord 89 | }{ 90 | Links: links, 91 | Query: query, 92 | Message: msg, 93 | Result: makeResults(chars), 94 | } 95 | form.Execute(w, data) 96 | } 97 | 98 | const formHTML = ` 99 | 100 | 101 |
102 | 103 |119 |
127 | 128 | 129 |{{.Code}} | 134 |{{.Char}} | 135 |{{.Name}} | 136 |