From 20ef8a090c2bc552e8f1e5dd773cd086c9782a21 Mon Sep 17 00:00:00 2001 From: uakci Date: Wed, 15 Sep 2021 01:19:55 +0200 Subject: initial --- .envrc | 1 + .gitignore | 2 + ToaqScript.ttf | Bin 0 -> 7200 bytes bot.go | 467 ++++++++++++++++++++++++++++++++++++++++++++++++ flake.lock | 132 ++++++++++++++ flake.nix | 74 ++++++++ go.mod | 9 + go.sum | 19 ++ gomod2nix.toml | 79 ++++++++ hoelai.go | 56 ++++++ patches/nui.patch | 10 ++ patches/spe.patch | 11 ++ vietoaq/vietoaq.go | 128 +++++++++++++ vietoaq/vietoaq_test.go | 33 ++++ 14 files changed, 1021 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 ToaqScript.ttf create mode 100644 bot.go create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 go.mod create mode 100644 go.sum create mode 100644 gomod2nix.toml create mode 100644 hoelai.go create mode 100644 patches/nui.patch create mode 100644 patches/spe.patch create mode 100644 vietoaq/vietoaq.go create mode 100644 vietoaq/vietoaq_test.go diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -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 new file mode 100644 index 0000000..fe87a17 Binary files /dev/null and b/ToaqScript.ttf differ diff --git a/bot.go b/bot.go new file mode 100644 index 0000000..5662b24 --- /dev/null +++ b/bot.go @@ -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; + }; + }); +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9f3762b --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f6405e6 --- /dev/null +++ b/go.sum @@ -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) + } + } +} + -- cgit v1.2.3