diff options
| author | uakci <uakci@uakci.pl> | 2021-09-15 01:19:55 +0200 |
|---|---|---|
| committer | uakci <uakci@uakci.pl> | 2021-09-15 01:19:55 +0200 |
| commit | 20ef8a090c2bc552e8f1e5dd773cd086c9782a21 (patch) | |
| tree | 89b7efba3f301c11eaba53ce13bcca87a9648240 | |
| download | nuogai-20ef8a090c2bc552e8f1e5dd773cd086c9782a21.tar.gz nuogai-20ef8a090c2bc552e8f1e5dd773cd086c9782a21.zip | |
initial
| -rw-r--r-- | .envrc | 1 | ||||
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | ToaqScript.ttf | bin | 0 -> 7200 bytes | |||
| -rw-r--r-- | bot.go | 467 | ||||
| -rw-r--r-- | flake.lock | 132 | ||||
| -rw-r--r-- | flake.nix | 74 | ||||
| -rw-r--r-- | go.mod | 9 | ||||
| -rw-r--r-- | go.sum | 19 | ||||
| -rw-r--r-- | gomod2nix.toml | 79 | ||||
| -rw-r--r-- | hoelai.go | 56 | ||||
| -rw-r--r-- | patches/nui.patch | 10 | ||||
| -rw-r--r-- | patches/spe.patch | 11 | ||||
| -rw-r--r-- | vietoaq/vietoaq.go | 128 | ||||
| -rw-r--r-- | vietoaq/vietoaq_test.go | 33 |
14 files changed, 1021 insertions, 0 deletions
@@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b91777e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.direnv +/result diff --git a/ToaqScript.ttf b/ToaqScript.ttf Binary files differnew file mode 100644 index 0000000..fe87a17 --- /dev/null +++ b/ToaqScript.ttf @@ -0,0 +1,467 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "regexp" + "strconv" + "strings" + "net/url" + "net/http" + "encoding/json" + "os" + "os/exec" + "os/signal" + "syscall" + "log" + + "git.uakci.pl/toaq/nuogai/vietoaq" + "github.com/bwmarrin/discordgo" + "github.com/eaburns/toaq/ast" + "github.com/eaburns/toaq/logic" +) + +const ( + lozenge = '▯' + myself = "490175530537058314" + HELP = + "\u2003**commands:**" + + "\n`%` — Toadūa lookup (3 results at a time)" + + "\n\u2003(`%37` — show 37 results at a time)" + + "\n\u2003(`%!` — show one result, with extra info)" + + "\n\u2003(`%!37` — show 37 results, with extra info)" + + "\n\u2003(`% 59` — show 59th page of results)" + + "\n`%serial` — fagri's serial predicate engine" + + "\n`%nui` — uakci's serial predicate engine" + + "\n\u2003(`%serial` and `%nui` do not accept tone marks)" + + "\n`%hoe` — Hoelāı renderer (font version: v0.341)" + + "\n\u2003(`%hoe!` — same as above; raw input)" + + "\n`%miu` — jelca's semantic parser" + UNKNOWN = "unknown command — see `%help` for help" +) + +var ( + header = regexp.MustCompile(`^\*\*.*?\*\*: `) + whitespace = regexp.MustCompile(`[ ]+`) + spePort string + nuiPort string +) + +func init() { + spePort = os.Getenv("SPE_PORT") + nuiPort = os.Getenv("NUI_PORT") +} + +func min(a, b int) int { + if a < b { return a } + return b +} + +func get(uri string) ([]byte, error) { + resp, err := http.Get(uri) + if err != nil { + return []byte{}, err + } + cont, err := ioutil.ReadAll(resp.Body) + if err != nil { + return []byte{}, err + } + return cont, nil +} + +func post(uri string, ct string, body io.Reader) ([]byte, error) { + resp, err := http.Post(uri, ct, body) + if err != nil { + return []byte{}, err + } + cont, err := ioutil.ReadAll(resp.Body) + if err != nil { + return []byte{}, err + } + return cont, nil +} + +func Respond(dg *discordgo.Session, ms *discordgo.MessageCreate) { + log.Printf("\n* %s", strings.Join(strings.Split(ms.Message.Content, "\n"), "\n ")) + respond(ms.Message.Content, + func(i interface{}) { + switch t := i.(type) { + case string: + dg.ChannelMessageSend(ms.Message.ChannelID, t) + case []byte: + dg.ChannelMessageSendComplex(ms.Message.ChannelID, + &discordgo.MessageSend{ + "", nil, false, []*discordgo.File{ + &discordgo.File{ + "toaq.png", + "image/png", + bytes.NewReader(t), + }, + }, nil, nil, nil, + }) + } + }) +} + +func respond(message string, callback func(interface{})) { + defer func() { + if r := recover(); r != nil { + fmt.Printf("%v", r) + callback("lủı sa hủı tủoı") + } + }() + message = strings.Trim( + header.ReplaceAllLiteralString(message, ""), + " \n") + parts := whitespace.Split(message, -1) + cmd, args, rest := parts[0], parts[1:], strings.Join(parts[1:], " ") + if strings.HasPrefix(cmd, "?%") { + callback(cmd[1:] + " " + vietoaq.From(rest)) + return + } + if strings.HasPrefix(cmd, "%") { + cmd_ := cmd[1:] + showNotes := false + if strings.HasPrefix(cmd_, "!") { + cmd_ = cmd_[1:] + showNotes = true + } + var (n int; err error) + if len(cmd_) > 0 { + n, err = strconv.Atoi(cmd_) + } else { + if showNotes { + n = 1 + } else { + n = 3 + } + } + if err == nil { + if n >= 0 { + Toadua(args, callback, n, showNotes) + } else { + callback("less than zero, that's quite many") + } + return + } + } + switch cmd { + case "?": + if strings.HasPrefix(strings.Trim(rest, " \n"), "?") { + return + } + callback(vietoaq.From(rest)) + case "%serial": + if len(rest) == 0 { + callback("please supply input") + return + } + resp, err := get(`http://localhost:9011/query?` + rest) + if err != nil { + log.Print(err) + callback("connectivity error") + return + } + callback(string(resp)) + case "%nui": + if len(rest) == 0 { + callback("please supply input") + return + } + u, err := url.Parse(`http://localhost:7183/`) + resp, err := post(u.String(), "application/octet-stream", + bytes.NewBufferString(rest)) + if err != nil { + log.Print(err) + callback("connectivity error") + return + } + callback(string(resp)) + case "%help": + callback(HELP) + case "%)": + callback("(%") + case "%hoe", "%hoe!": + if len(rest) == 0 { + callback("please supply input") + return + } + rest = strings.ReplaceAll(rest, "\t", "\\t") + if cmd == "%hoe" { + parts := regexp.MustCompile(`[<>]`).Split(rest, -1) + var sb strings.Builder + for i := 0; i < len(parts); i++ { + s := parts[i] + if i % 2 == 1 { + sb.WriteString("<") + } else { + if i != 0 { + sb.WriteString(">") + } + s = Hoekai(s) + } + sb.WriteString(s) + } + rest = sb.String() + } + out, err := exec.Command("convert", + "-density", "300", + "-background", "none", + "-fill", "white", + "-strokewidth", "2", + "-stroke", "black", + "-font", "ToaqScript", + "-pointsize", "24", + "pango:" + rest, + "-bordercolor", "none", + "-border", "20", + "png:-").Output() + if err != nil { + log.Print(err) + callback("lủı sa tủoı") + return + } + callback(out) + case "%miu": + if len(rest) == 0 { + callback("please supply input") + return + } + input := strings.TrimSpace(rest) + p := ast.NewParser(input) + text, err := p.Text() + if err != nil { + callback("syntax error " + err.Error()) + return + } + parse := ast.BracesString(text) + if parse != "" { + parse += "\n" + } + var math string + stmt := logic.Interpret(text) + if stmt == nil { + math = "fragment" + } else { + math = logic.PrettyString(stmt) + } + callback(parse + math) + default: + if strings.HasPrefix(cmd, "%") { + callback(UNKNOWN) + } + } +} + +func Toadua(args []string, callback func(interface{}), howMany int, showNotes bool) { + if len(args) == 0 { + callback("please supply a query") + return + } + page, err := strconv.Atoi(args[0]) + if err != nil { + page = 1 + } else { + args = args[1:] + } + query := strings.Join(args, " ") + mars, err := json.Marshal(struct{ + S string `json:"action"` + I interface{} `json:"query"` + }{ + "search", + ToaduaQuery(query), + }) + if err != nil { + log.Print(err) + callback("error") + return + } + raw, err := http.Post(`https://toadua.uakci.pl/api`, + "application/json", bytes.NewReader(mars)) + if err != nil { + log.Print(err) + callback("connectivity error") + return + } + var resp struct{ + Success bool `json:"success"` + Error string `json:"error"` + Entries []struct{ + Id string `json:"id"` + User string `json:"user"` + Head string `json:"head"` + Body string `json:"body"` + Score int `json:"score"` + Notes []struct{ + User string `json:"user"` + Content string `json:"content"` + } `json:"notes"` + } `json:"results"` + } + body, err := ioutil.ReadAll(raw.Body) + if err != nil { + log.Print(err) + callback("connectivity error") + return + } + err = json.Unmarshal(body, &resp) + if err != nil { + log.Print(err) + callback("parse error") + return + } + if !resp.Success { + log.Print(resp.Error) + callback("search failed: " + resp.Error) + return + } + if len(resp.Entries) == 0 { + callback("results empty") + return + } + first := (page - 1) * howMany + if len(resp.Entries) <= first { + callback(fmt.Sprintf("invalid page number (%d results)", len(resp.Entries))) + return + } + last := min(first + howMany, len(resp.Entries)) + var b strings.Builder + b.Grow(2000) + fmt.Fprintf(&b, "\u2003(%d–%d/%d)", first + 1, last, len(resp.Entries)) + soFar := b.String() + for _, e := range resp.Entries[first:last] { + // if i != 0 { + b.WriteString("\n") + // } + // b.WriteString(" — ") + b.WriteString("**" + e.Head + "**") + if showNotes { + fmt.Fprintf(&b, " (%s)", e.User) + } else if e.User == "official" { + b.WriteString(" ❦") + } + if e.Score != 0 { + b.WriteString(" ") + if e.Score > 0 { + b.WriteString(strings.Repeat("+", e.Score)) + } else { + b.WriteString(strings.Repeat("−", -e.Score)) + } + } + // b.WriteString(" — ") + b.WriteString("\n\u2003") + b.WriteString(strings.Join(strings.Split(e.Body, "\n"), "\n\u2003")) + if showNotes { + b.WriteString("") + for _, note := range e.Notes { + fmt.Fprintf(&b, "\n\u2003\u2003• (%s) %s", note.User, note.Content) + } + } + old := soFar + soFar = b.String() + if len(soFar) > 2000 { + callback(old) + b.Reset() + b.WriteString(soFar[len(old):]) + } + } + callback(b.String()) +} + +func ToaduaQuery(s string) interface{} { + spaced := strings.Split(s, " ") + andArgs := make([]interface{}, len(spaced)) + for i, andArg := range spaced { + ored := strings.Split(andArg, "|") + orArgs := make([]interface{}, len(ored)) + for j, orArg := range ored { + neg := false + if strings.HasPrefix(orArg, "!") { + orArg = orArg[1:] + neg = true + } + parts := strings.SplitN(orArg, ":", 2) + var term interface{} + if len(parts) == 1 { + term = []interface{}{"term", orArg} + } else { + if parts[0] == "arity" { + conv, _ := strconv.Atoi(parts[1]) + term = []interface{}{"arity", conv} + } else { + term = []interface{}{parts[0], parts[1]} + } + } + if neg { + term = []interface{}{"not", term} + } + orArgs[j] = term + } + if len(orArgs) == 1 { + andArgs[i] = orArgs[0] + } else { + andArgs[i] = append([]interface{}{"or"}, orArgs...) + } + } + if len(andArgs) == 1 { + return andArgs[0] + } else { + return append([]interface{}{"and"}, andArgs...) + } +} + +func Hoekai(s string) string { + viet := vietoaq.To(s) + parts := vietoaq.Syllables(viet, vietoaq.VietoaqSyllable) + var sb strings.Builder + for i, part := range parts { + if i % 2 == 0 { + sb.WriteString(part[0]) + continue + } + onset, nucleus, coda := part[1], part[2], part[3] + switch onset { + case "ch": onset = "w" + case "sh": onset = "x" + case "x": onset = "q" + } + diph := "" + if len(nucleus) >= 2 { + flag := true + switch nucleus[len(nucleus) - 2:] { + case "ai": diph = "y" + case "ao": diph = "v" + case "oi": diph = "z" + case "ei": diph = "W" + default: flag = false + } + if flag { + nucleus = nucleus[:len(nucleus) - 2] + } + } + if len(nucleus) >= 2 { + diph = strings.ToUpper(nucleus[1:]) + nucleus = nucleus[:1] + } else if nucleus == "a" && diph == "" { + nucleus = "" + } + fmt.Fprintf(&sb, "%s%s%s%s", + diph, onset, strings.ToUpper(coda), nucleus) + } + return sb.String() +} + +func main() { + dg, err := discordgo.New("Bot " + os.Getenv("TOKEN")) + if err != nil { panic(err) } + dg.AddHandler(Respond) + err = dg.Open() + if err != nil { panic(err) } + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + dg.Close() +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..92f56e7 --- /dev/null +++ b/flake.lock @@ -0,0 +1,132 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1631561581, + "narHash": "sha256-3VQMV5zvxaVLvqqUrNz3iJelLw30mIVSfZmAaauM3dA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "7e5bf3925f6fbdfaf50a2a7ca0be2879c4261d19", + "type": "github" + }, + "original": { + "owner": "numtide", + "ref": "master", + "repo": "flake-utils", + "type": "github" + } + }, + "gomod2nix": { + "inputs": { + "nixpkgs": "nixpkgs", + "utils": "utils" + }, + "locked": { + "lastModified": 1627572165, + "narHash": "sha256-MFpwnkvQpauj799b4QTBJQFEddbD02+Ln5k92QyHOSk=", + "owner": "tweag", + "repo": "gomod2nix", + "rev": "67f22dd738d092c6ba88e420350ada0ed4992ae8", + "type": "github" + }, + "original": { + "owner": "tweag", + "ref": "master", + "repo": "gomod2nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1625191069, + "narHash": "sha256-M8/UH9pMgQEtuzY9bFwklYw8hx0pOKtUTyQC8E2UTHY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d8079260a3028ae3221d7a5467443ee3a9edd2b8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1631655525, + "narHash": "sha256-8U7zAdbjNItXo6eqI/rhtOa3LUPGD6yE9PTZQkrSGHo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "cf0caf529c33c140863ebfa43691f7b69fe2233c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "master", + "repo": "nixpkgs", + "type": "github" + } + }, + "nuigui-upstream": { + "flake": false, + "locked": { + "lastModified": 1551541396, + "narHash": "sha256-PZUzgumsvIpzEgODVpiBKlZzZQHXKhS+3lOB+2EDeeQ=", + "owner": "uakci", + "repo": "nuigui", + "rev": "87d41886f7c4693dfc5e3016799232c7e15e4c81", + "type": "github" + }, + "original": { + "owner": "uakci", + "ref": "master", + "repo": "nuigui", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "gomod2nix": "gomod2nix", + "nixpkgs": "nixpkgs_2", + "nuigui-upstream": "nuigui-upstream", + "serial-predicate-engine-upstream": "serial-predicate-engine-upstream" + } + }, + "serial-predicate-engine-upstream": { + "flake": false, + "locked": { + "lastModified": 1546479877, + "narHash": "sha256-jVDNyfYWlk5qYsZSQjcGdI4hdEKKdSciz6QK0nAqKGs=", + "owner": "acotis", + "repo": "serial-predicate-engine", + "rev": "9e02baf711c0b7402da2eeb50b8644bf7b6e415f", + "type": "github" + }, + "original": { + "owner": "acotis", + "ref": "master", + "repo": "serial-predicate-engine", + "type": "github" + } + }, + "utils": { + "locked": { + "lastModified": 1623875721, + "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..de458af --- /dev/null +++ b/flake.nix @@ -0,0 +1,74 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/master"; + flake-utils.url = "github:numtide/flake-utils/master"; + gomod2nix.url = "github:tweag/gomod2nix/master"; + nuigui-upstream.url = "github:uakci/nuigui/master"; + nuigui-upstream.flake = false; + serial-predicate-engine-upstream.url = + "github:acotis/serial-predicate-engine/master"; + serial-predicate-engine-upstream.flake = false; + }; + + outputs = { self, nixpkgs, gomod2nix, nuigui-upstream + , serial-predicate-engine-upstream, flake-utils, ... }: + { + inherit (gomod2nix) devShell; + nixosModule = { config, system, ... }: { + config.fonts.fonts = [ self.packages.toaqScript.${system} ]; + }; + } // flake-utils.lib.eachDefaultSystem (system: + let + pkgs = (import nixpkgs { + inherit system; + overlays = [ gomod2nix.overlay ]; + }).pkgs; + in with pkgs; + let + toaqScript = pkgs.writeTextDir "share/fonts/ToaqScript.ttf" ./ToaqScript.ttf; + schemePkgs = lib.mapAttrs (name: + { src, install, patches }: + pkgs.stdenv.mkDerivation { + inherit src name patches; + buildInputs = [ pkgs.guile ]; + installPhase = '' + mkdir -p $out/bin + cp -r ./* $out + echo "${install}" > $out/bin/${name} + chmod +x $out/bin/${name} + ''; + }) { + nuigui = { + src = nuigui-upstream; + patches = [ ./patches/nui.patch ]; + install = '' + cd \$(dirname \$0)/../; ${pkgs.guile}/bin/guile web.scm + ''; + }; + serial-predicate-engine = { + src = serial-predicate-engine-upstream; + patches = [ ./patches/spe.patch ]; + install = '' + cd \$(dirname \$0)/../web/; ${pkgs.guile}/bin/guile webservice.scm + ''; + }; + }; + nuogai = buildGoApplication { + vendorSha256 = null; + runVend = true; + name = "nuogai"; + src = ./.; + modules = ./gomod2nix.toml; + buildInputs = (builtins.attrValues schemePkgs) ++ [ + toaqScript + (imagemagick.overrideAttrs + (a: { buildInputs = a.buildInputs ++ [ pango ]; })) + ]; + }; + in { + defaultPackage = nuogai; + packages = schemePkgs // { + inherit toaqScript nuogai; + }; + }); +} @@ -0,0 +1,9 @@ +module git.uakci.pl/toaq/nuogai + +go 1.13 + +require ( + github.com/bwmarrin/discordgo v0.23.2 + github.com/eaburns/toaq v0.0.0-20210614121731-80ccb209650b + golang.org/x/text v0.3.7 +) @@ -0,0 +1,19 @@ +github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4= +github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= +github.com/eaburns/peggy v1.0.0 h1:cvZIO5GGq2Qy1l1Va0Mte6LFVTY6GtAGbzSvouFcjUk= +github.com/eaburns/peggy v1.0.0/go.mod h1:X2pbl0EV5erfnK8kSGwo0lBCgMGokvR1E6KerAoDKXg= +github.com/eaburns/pretty v1.0.0 h1:00W1wrrtMXUSqLPN0txS8j7g9qFXy6nA5vZVqVQOo6w= +github.com/eaburns/pretty v1.0.0/go.mod h1:retcK8A0KEgdmb0nuxhvyxixwCmEPO7SKlK0IJhjg8A= +github.com/eaburns/toaq v0.0.0-20210614121731-80ccb209650b h1:2mu0jEGoRhKMpkHMInz584EWFa0n3oEzpxoAo6PN1IE= +github.com/eaburns/toaq v0.0.0-20210614121731-80ccb209650b/go.mod h1:FKayyKQuuBv0TnmWcuGr0MNMLeqpWLskPHeRWIEihoM= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/velour/chat v0.0.0-20180713122344-fd1d1606cb89/go.mod h1:ejwOYCjnDMyO5LXFXRARQJGBZ6xQJZ3rgAHE5drSuMM= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/gomod2nix.toml b/gomod2nix.toml new file mode 100644 index 0000000..228e569 --- /dev/null +++ b/gomod2nix.toml @@ -0,0 +1,79 @@ +["github.com/bwmarrin/discordgo"] + sumVersion = "v0.23.2" + ["github.com/bwmarrin/discordgo".fetch] + type = "git" + url = "https://github.com/bwmarrin/discordgo" + rev = "c27ad65527ecbc264c674cd3d0e85bb09de942e3" + sha256 = "1k14f52s3wvp4jx7czicnfjwz9inbxiaidfg74z3gk9vlway69aq" + +["github.com/eaburns/peggy"] + sumVersion = "v1.0.0" + ["github.com/eaburns/peggy".fetch] + type = "git" + url = "https://github.com/eaburns/peggy" + rev = "e1cd35e5dd2c0c34e02e847e569dafe1afb30033" + sha256 = "1mhdxs7vvwy6nzvx33dvkby1amp6cidxy6vl5hyzr0m6biqxlncs" + +["github.com/eaburns/pretty"] + sumVersion = "v1.0.0" + ["github.com/eaburns/pretty".fetch] + type = "git" + url = "https://github.com/eaburns/pretty" + rev = "af23b1658732d295adfdc728db79838ce7e7c53e" + sha256 = "1kaw6chx3bvbvfs5b5v5xvm4xcga0kcds4m6cm6rj5wqrsamhrlm" + +["github.com/eaburns/toaq"] + sumVersion = "v0.0.0-20210614121731-80ccb209650b" + ["github.com/eaburns/toaq".fetch] + type = "git" + url = "https://github.com/eaburns/toaq" + rev = "80ccb209650b6c7ef05d3736e1434c7ccc0e1398" + sha256 = "1pxkwh6lr5ajzwn85acg9458g8npkm5kdvkiryikckxj413mp1b4" + +["github.com/google/go-cmp"] + sumVersion = "v0.3.1" + ["github.com/google/go-cmp".fetch] + type = "git" + url = "https://github.com/google/go-cmp" + rev = "2d0692c2e9617365a95b295612ac0d4415ba4627" + sha256 = "1caw49i0plkjxir7kdf5qhwls3krqwfmi7g4h392rdfwi3kfahx1" + +["github.com/gorilla/websocket"] + sumVersion = "v1.4.1" + ["github.com/gorilla/websocket".fetch] + type = "git" + url = "https://github.com/gorilla/websocket" + rev = "c3e18be99d19e6b3e8f1559eea2c161a665c4b6b" + sha256 = "03n1n0nwz3k9qshmriycqznnnvd3dkzsfwpnfjzzvafjxk9kyapv" + +["github.com/velour/chat"] + sumVersion = "v0.0.0-20180713122344-fd1d1606cb89" + ["github.com/velour/chat".fetch] + type = "git" + url = "https://github.com/velour/chat" + rev = "fd1d1606cb8966f7141d57428833bf76e5b184ea" + sha256 = "1n9dyvqy3fr2f9hfzmrcikj6blx3xz2r7jg8i4j8a78033rb2fn0" + +["golang.org/x/crypto"] + sumVersion = "v0.0.0-20181030102418-4d3f4d9ffa16" + ["golang.org/x/crypto".fetch] + type = "git" + url = "https://go.googlesource.com/crypto" + rev = "4d3f4d9ffa16a13f451c3b2999e9c49e9750bf06" + sha256 = "0sbsgjm6wqa162ssrf1gnpv62ak5wjn1bn8v7sxwwfg8a93z1028" + +["golang.org/x/text"] + sumVersion = "v0.3.7" + ["golang.org/x/text".fetch] + type = "git" + url = "https://go.googlesource.com/text" + rev = "383b2e75a7a4198c42f8f87833eefb772868a56f" + sha256 = "0xkw0qvfjyifdqd25y7nxdqkdh92inymw3q7841nricc9s01p4jy" + +["golang.org/x/tools"] + sumVersion = "v0.0.0-20180917221912-90fa682c2a6e" + ["golang.org/x/tools".fetch] + type = "git" + url = "https://go.googlesource.com/tools" + rev = "90fa682c2a6e6a37b3a1364ce2fe1d5e41af9d6d" + sha256 = "03ic2xsy51jw9749wl7gszdbz99iijbd2bckgygl6cm9w5m364ak" diff --git a/hoelai.go b/hoelai.go new file mode 100644 index 0000000..bf0e7eb --- /dev/null +++ b/hoelai.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "git.uakci.pl/toaq/nuogai/vietoaq" + "strings" +) + +func Hoelai(s string) string { + viet := vietoaq.To(s) + parts := vietoaq.Syllables(viet, vietoaq.VietoaqSyllable) + var sb strings.Builder + for i, part := range parts { + if i%2 == 0 { + sb.WriteString(part[0]) + continue + } + onset, nucleus, coda := part[1], part[2], part[3] + switch onset { + case "ch": + onset = "w" + case "sh": + onset = "x" + case "x": + onset = "q" + } + diph := "" + if len(nucleus) >= 2 { + flag := true + switch nucleus[len(nucleus)-2:] { + case "ai": + diph = "y" + case "ao": + diph = "v" + case "oi": + diph = "z" + case "ei": + diph = "W" + default: + flag = false + } + if flag { + nucleus = nucleus[:len(nucleus)-2] + } + } + if len(nucleus) >= 2 { + diph = strings.ToUpper(nucleus[1:]) + nucleus = nucleus[:1] + } else if nucleus == "a" && diph == "" { + nucleus = "" + } + fmt.Fprintf(&sb, "%s%s%s%s", + diph, onset, strings.ToUpper(coda), nucleus) + } + return sb.String() +} diff --git a/patches/nui.patch b/patches/nui.patch new file mode 100644 index 0000000..a99771c --- /dev/null +++ b/patches/nui.patch @@ -0,0 +1,10 @@ +diff --git a/web.scm b/web.scm +index 411a6cc..e76f8b2 100644 +--- a/web.scm ++++ b/web.scm +@@ -4,4 +4,4 @@ + (lambda (request request-body) + (values '((content-type . (text/plain))) + (process-line (utf8->string request-body)))) +- 'http '(#:port 7183)) ++ 'http '(#:port (getenv "PORT"))) diff --git a/patches/spe.patch b/patches/spe.patch new file mode 100644 index 0000000..af5a53d --- /dev/null +++ b/patches/spe.patch @@ -0,0 +1,11 @@ +diff --git a/web/webservice.scm b/web/webservice.scm +index 8193dda..0cc6ca4 100755 +--- a/web/webservice.scm ++++ b/web/webservice.scm +@@ -51,4 +51,4 @@ + + (run-server spe-handler + 'http +- '(#:host "0.0.0.0")) +\ No newline at end of file ++ `(#:host "127.0.0.1" #:port ,(string->number (getenv "PORT")))) diff --git a/vietoaq/vietoaq.go b/vietoaq/vietoaq.go new file mode 100644 index 0000000..b4375e7 --- /dev/null +++ b/vietoaq/vietoaq.go @@ -0,0 +1,128 @@ +package vietoaq + +import ( + "regexp" + "strings" + + "golang.org/x/text/unicode/norm" +) + +var ( + toneMap = "\u0304\u0301\u0308\u0309\u0302\u0300\u0303" + vietoaqMap = [7][2]rune{ + {'r', 'l'}, {'p', 'b'}, {'x', 'z'}, {'n', 'm'}, {'t', 'd'}, {'k', 'g'}, {'f', 'v'}} + RegularSyllable = regexp.MustCompile( + `([bcdfghjklmnprstz']?|[cs]h)` + // onset + `([aeiuoyı])` + // first vowel of nucleus + `([` + toneMap + `]?)` + // tone + `([aeiouyı]{0,2})` + // remaining nucleus vowels + `(q?)` ) // regular coda + VietoaqSyllable = regexp.MustCompile( + `([bcdfghjklmnprstxz]|[cs]h)` + // onset + `([aeiuoy]{1,3})` + // nucleus + `([qrlpbxznmtdkgfv]?)` ) // Vietoaq coda +) + +func toTransform(syll []string, padding bool) string { + onset, vow, tone, vows, coda := + syll[1], syll[2], syll[3], syll[4], syll[5] + if onset == "" || onset == "'" { + onset = "x" + } + if tone != "" { + var qful int + if coda == "q" { + qful = 1 + } else { + qful = 0 + } + coda = string(vietoaqMap[strings.Index(toneMap, tone) / 2][qful]) + } + return onset + strings.ReplaceAll(vow + vows, "ı", "i") + coda +} + +func fromTransform(syll []string, padding bool) string { + onset, vow, tone, vows, coda := + syll[1], syll[2][0:1], "", syll[2][1:], syll[3] + if coda != "" && coda != "q" { + codaRune := rune(coda[0]) + var ii, jj int + for i, arr := range vietoaqMap { + for j, char := range arr { + if char == codaRune { + ii, jj = i, j + break + } + } + } + if jj == 1 { + coda = "q" + } else { + coda = "" + } + tone = string([]rune(toneMap)[ii]) + if onset == "x" { + onset = "" + } + } else if vow == "i" { + vow = "ı" + } + if onset == "x" { + if padding || tone != "" { + onset = "" + } else { + onset = "'" + } + } + return onset + norm.NFC.String(vow + tone) + + strings.ReplaceAll(vows, "i", "ı") + coda +} + +func To(regular string) string { + return syllableTransform(regular, RegularSyllable, toTransform) +} + +func From(vietoaq string) string { + return syllableTransform(vietoaq, VietoaqSyllable, fromTransform) +} + +func syllableTransform(input string, r *regexp.Regexp, + transform func([]string, bool)string) string { + interleaved := Syllables(strings.ToLower(norm.NFD.String(input)), r) + var sb strings.Builder + for i, s := range interleaved { + if i % 2 == 1 { + sb.WriteString(transform(s, interleaved[i - 1][0] != "" || i == 1)) + } else { + sb.WriteString(s[0]) + } + } + return norm.NFC.String(sb.String()) +} + +// returns an array of junk and Toaq, interleaved +func Syllables(s string, r *regexp.Regexp) [][]string { + acc := [][]string{} + for { + bounds := r.FindStringSubmatchIndex(s) + if bounds == nil { + break + } + preemptive := r.FindStringSubmatchIndex(s[bounds[1] - 1:]) + if preemptive != nil && preemptive[0] == 0 && + bounds[len(bounds) - 1] - bounds[len(bounds) - 2] > 0 { + bounds[1]-- + bounds[len(bounds) - 1]-- + } + acc = append(acc, []string{s[:bounds[0]]}) + ln := len(bounds) / 2 + contentful := make([]string, ln) + for j := 0; j < ln; j++ { + contentful[j] = s[bounds[2 * j]:bounds[2 * j + 1]] + } + acc = append(acc, contentful) + s = s[bounds[1]:] + } + acc = append(acc, []string{s}) + return acc +} diff --git a/vietoaq/vietoaq_test.go b/vietoaq/vietoaq_test.go new file mode 100644 index 0000000..15a2455 --- /dev/null +++ b/vietoaq/vietoaq_test.go @@ -0,0 +1,33 @@ +package vietoaq + +import ( + "testing" +) + +var ( + examples = map[string]string{ + ``: ``, + `bbb`: `bbb`, + `jảq hủı óq`: `jam huin xob`, + `ýhō`: `xyphor`, + `gı'aq`: `gixaq`, + `gï'aq`: `gixxaq`, + `jảq'a`: `jamxa`, + `gï aq`: `gix xaq`, + `aq'aq aq`: `xaqxaq xaq`, + } +) + +func TestVietoaq(t *testing.T) { + for regular, vietoaq := range examples { + vietoaq_ := To(regular) + if vietoaq_ != vietoaq { + t.Errorf(" to: %s -> %s != %s", regular, vietoaq_, vietoaq) + } + regular_ := From(vietoaq) + if regular_ != regular { + t.Errorf("from: %s -> %s != %s", vietoaq, regular_, regular) + } + } +} + |
