commit 218086c8a3ca500940df4023ce8aabf5e331c535
parent 840d1abf3fe15f7c114790c27af92c0ae8f2a86c
Author: lash <dev@holbrook.no>
Date: Fri, 21 Apr 2023 20:20:02 +0100
Implement language
Diffstat:
15 files changed, 277 insertions(+), 15 deletions(-)
diff --git a/engine/engine.go b/engine/engine.go
@@ -27,6 +27,7 @@ type Config struct {
Root string
FlagCount uint32
CacheSize uint32
+ Language string
}
// Engine is an execution engine that handles top-level errors when running client inputs against code in the bytecode buffer.
@@ -56,6 +57,20 @@ func NewEngine(cfg Config, st *state.State, rs resource.Resource, ca cache.Memor
engine.root = cfg.Root
engine.session = cfg.SessionId
+ var err error
+ if st.Language != nil {
+ if cfg.Language != "" {
+ err = st.SetLanguage(cfg.Language)
+ panic(err)
+ }
+ }
+
+ if st.Language != nil {
+ _, err = st.SetFlag(state.FLAG_LANG)
+ if err != nil {
+ panic(err)
+ }
+ }
return engine
}
@@ -86,6 +101,7 @@ func(en *Engine) Init(ctx context.Context) (bool, error) {
Logg.DebugCtxf(ctx, "already initialized")
return true, nil
}
+
sym := en.root
if sym == "" {
return false, fmt.Errorf("start sym empty")
@@ -123,6 +139,9 @@ func(en *Engine) Init(ctx context.Context) (bool, error) {
// - input processing against bytcode failed
func (en *Engine) Exec(input []byte, ctx context.Context) (bool, error) {
var err error
+ if en.st.Language != nil {
+ ctx = context.WithValue(ctx, "Language", *en.st.Language)
+ }
if en.st.Moves == 0 {
cont, err := en.Init(ctx)
if err != nil {
@@ -176,7 +195,6 @@ func(en *Engine) exec(input []byte, ctx context.Context) (bool, error) {
_, err = en.reset(ctx)
return false, err
}
-
return true, nil
}
@@ -187,6 +205,9 @@ func(en *Engine) exec(input []byte, ctx context.Context) (bool, error) {
// - the template for the given node point is note available for retrieval using the resource.Resource implementer.
// - the supplied writer fails to process the writes.
func(en *Engine) WriteResult(w io.Writer, ctx context.Context) (int, error) {
+ if en.st.Language != nil {
+ ctx = context.WithValue(ctx, "Language", *en.st.Language)
+ }
r, err := en.vm.Render(ctx)
if err != nil {
return 0, err
diff --git a/engine/engine_test.go b/engine/engine_test.go
@@ -9,9 +9,11 @@ import (
"testing"
"git.defalsify.org/vise.git/cache"
+ "git.defalsify.org/vise.git/lang"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/state"
"git.defalsify.org/vise.git/testdata"
+ "git.defalsify.org/vise.git/vm"
)
var (
@@ -33,6 +35,8 @@ func NewFsWrapper(path string, st *state.State) FsWrapper {
wr.AddLocalFunc("one", wr.one)
wr.AddLocalFunc("inky", wr.inky)
wr.AddLocalFunc("pinky", wr.pinky)
+ wr.AddLocalFunc("set_lang", wr.set_lang)
+ wr.AddLocalFunc("translate", wr.translate)
return wr
}
@@ -55,6 +59,29 @@ func(fs FsWrapper) pinky(sym string, input []byte, ctx context.Context) (resourc
}, nil
}
+func(fs FsWrapper) translate(sym string, input []byte, ctx context.Context) (resource.Result, error) {
+ r := "cool"
+ v := ctx.Value("Language")
+ code := ""
+ lang, ok := v.(lang.Language)
+ if ok {
+ code = lang.Code
+ }
+ if code == "nor" {
+ r = "fett"
+ }
+ return resource.Result{
+ Content: r,
+ }, nil
+}
+
+func(fs FsWrapper) set_lang(sym string, input []byte, ctx context.Context) (resource.Result, error) {
+ return resource.Result{
+ Content: string(input),
+ FlagSet: []uint32{state.FLAG_LANG},
+ }, nil
+}
+
func(fs FsWrapper) GetCode(sym string) ([]byte, error) {
sym += ".bin"
fp := path.Join(fs.Path, sym)
@@ -186,3 +213,55 @@ func TestEngineResumeTerminated(t *testing.T) {
t.Fatalf("expected idx '0', got %v", idx)
}
}
+
+func TestLanguageSet(t *testing.T) {
+ generateTestData(t)
+ ctx := context.TODO()
+ st := state.NewState(0)
+ rs := NewFsWrapper(dataDir, &st)
+ ca := cache.NewCache().WithCacheSize(1024)
+
+ en := NewEngine(Config{
+ Root: "root",
+ }, &st, &rs, ca, ctx)
+
+ var err error
+ _, err = en.Init(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ b := vm.NewLine(nil, vm.LOAD, []string{"translate"}, []byte{0x01, 0xff}, nil)
+ b = vm.NewLine(b, vm.LOAD, []string{"set_lang"}, []byte{0x01, 0x00}, nil)
+ b = vm.NewLine(b, vm.MOVE, []string{"."}, nil, nil)
+ st.SetCode(b)
+
+ _, err = en.Exec([]byte("no"), ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r, err := ca.Get("translate")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if r != "cool" {
+ t.Fatalf("expected 'cool', got '%s'", r)
+ }
+
+
+ b = vm.NewLine(nil, vm.RELOAD, []string{"translate"}, nil, nil)
+ b = vm.NewLine(b, vm.MOVE, []string{"."}, nil, nil)
+ st.SetCode(b)
+
+ _, err = en.Exec([]byte("no"), ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r, err = ca.Get("translate")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if r != "fett" {
+ t.Fatalf("expected 'fett', got '%s'", r)
+ }
+}
diff --git a/go.mod b/go.mod
@@ -4,6 +4,7 @@ go 1.20
require (
github.com/alecthomas/participle/v2 v2.0.0
+ github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c
github.com/fxamacker/cbor/v2 v2.4.0
github.com/peteole/testdata-loader v0.3.0
)
diff --git a/lang/lang.go b/lang/lang.go
@@ -0,0 +1,31 @@
+package lang
+
+import (
+ "fmt"
+
+ "github.com/barbashov/iso639-3"
+)
+
+var (
+ Default = "eng" // ISO639-3
+)
+
+type Language struct {
+ Code string
+ Name string
+}
+
+func LanguageFromCode(code string) (Language, error) {
+ r := iso639_3.FromAnyCode(code)
+ if r == nil {
+ return Language{}, fmt.Errorf("invalid language code: %s", code)
+ }
+ return Language{
+ Code: r.Part3,
+ Name: r.Name,
+ }, nil
+}
+
+func(l Language) String() string {
+ return fmt.Sprintf("%s (%s)", l.Code, l.Name)
+}
diff --git a/lang/lang_test.go b/lang/lang_test.go
@@ -0,0 +1,20 @@
+package lang
+
+import (
+ "testing"
+)
+
+func TestLang(t *testing.T) {
+ var err error
+ _, err = LanguageFromCode("xxx")
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ l, err := LanguageFromCode("en")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if l.Code != "eng" {
+ t.Fatalf("expected 'eng', got '%s'", l.Code)
+ }
+}
diff --git a/render/page.go b/render/page.go
@@ -181,6 +181,7 @@ func(pg *Page) Reset() {
}
}
+
// render menu and all syms except sink, split sink into display chunks
// TODO: Function too long, split up
func(pg *Page) prepare(sym string, values map[string]string, idx uint16) (map[string]string, error) {
diff --git a/resource/fs.go b/resource/fs.go
@@ -7,6 +7,8 @@ import (
"path"
"path/filepath"
"strings"
+
+ "git.defalsify.org/vise.git/lang"
)
type FsResource struct {
@@ -50,7 +52,7 @@ func(fs FsResource) FuncFor(sym string) (EntryFunc, error) {
if ok {
return fn, nil
}
- _, err := fs.getFuncNoCtx(sym, nil)
+ _, err := fs.getFuncNoCtx(sym, nil, nil)
if err != nil {
return nil, fmt.Errorf("unknown sym: %s", sym)
}
@@ -62,13 +64,18 @@ func(fs FsResource) String() string {
}
func(fs FsResource) getFunc(sym string, input []byte, ctx context.Context) (Result, error) {
- return fs.getFuncNoCtx(sym, input)
+ v := ctx.Value("language")
+ if v == nil {
+ return fs.getFuncNoCtx(sym, input, nil)
+ }
+ language := v.(*lang.Language)
+ return fs.getFuncNoCtx(sym, input, language)
}
-func(fs FsResource) getFuncNoCtx(sym string, input []byte) (Result, error) {
+func(fs FsResource) getFuncNoCtx(sym string, input []byte, language *lang.Language) (Result, error) {
fb := sym + ".txt"
fp := path.Join(fs.Path, fb)
- Logg.Debugf("getfunc search dir", "dir", fs.Path, "path", fp, "sym", sym)
+ Logg.Debugf("getfunc search dir", "dir", fs.Path, "path", fp, "sym", sym, "language", language)
r, err := ioutil.ReadFile(fp)
if err != nil {
return Result{}, fmt.Errorf("failed getting data for sym '%s': %v", sym, err)
diff --git a/resource/fs_test.go b/resource/fs_test.go
@@ -8,3 +8,30 @@ func TestNewFs(t *testing.T) {
n := NewFsResource("./testdata")
_ = n
}
+//
+//func TestResourceLanguage(t *testing.T) {
+// var err error
+// generateTestData(t)
+// ctx := context.TODO()
+// st := state.NewState(0)
+// rs := NewFsWrapper(dataDir, &st)
+// ca := cache.NewCache()
+//
+// cfg := Config{
+// Root: "root",
+// }
+//
+// en := NewEngine(cfg, &st, &rs, ca, ctx)
+// _, err = en.Init(ctx)
+// if err == nil {
+// t.Fatalf("expected error")
+// }
+// cfg = Config{
+// Root: "root",
+// }
+// en = NewEngine(cfg, &st, &rs, ca, ctx)
+// _, err = en.Init(ctx)
+// if err != nil {
+// t.Fatal(err)
+// }
+//}
diff --git a/resource/resource.go b/resource/resource.go
@@ -71,4 +71,3 @@ func(m *MenuResource) GetCode(sym string) ([]byte, error) {
func(m *MenuResource) GetTemplate(sym string) (string, error) {
return m.templateFunc(sym)
}
-
diff --git a/state/debug_test.go b/state/debug_test.go
@@ -50,7 +50,7 @@ func TestDebugState(t *testing.T) {
st.Down("root")
r := fmt.Sprintf("%s", st)
- expect := "moves: 1 idx: 0 flags: INTERNAL_DIRTY(3),FOO(8) path: root"
+ expect := "moves: 1 idx: 0 flags: INTERNAL_DIRTY(3),FOO(8) path: root lang: (default)"
if r != expect {
t.Fatalf("expected '%s', got '%s'", expect, r)
}
diff --git a/state/flag.go b/state/flag.go
@@ -7,11 +7,13 @@ const (
FLAG_DIRTY
FLAG_WAIT
FLAG_LOADFAIL
+ FLAG_RESERVED
+ FLAG_LANG
FLAG_USERSTART = 8
)
func IsWriteableFlag(flag uint32) bool {
- if flag > 7 {
+ if flag > 6 {
return true
}
//if flag & FLAG_WRITEABLE > 0 {
diff --git a/state/state.go b/state/state.go
@@ -3,6 +3,8 @@ package state
import (
"fmt"
"strings"
+
+ "git.defalsify.org/vise.git/lang"
)
type IndexError struct {
@@ -30,10 +32,11 @@ func(err *IndexError) Error() string {
type State struct {
Code []byte // Pending bytecode to execute
ExecPath []string // Command symbols stack
- BitSize uint32 // size of (32-bit capacity) bit flag byte array
- SizeIdx uint16
+ BitSize uint32 // Size of (32-bit capacity) bit flag byte array
+ SizeIdx uint16 // Lateral page browse index in current frame
Flags []byte // Error state
Moves uint32 // Number of times navigation has been performed
+ Language *lang.Language // Language selector for rendering
input []byte // Last input
debug bool // Make string representation more human friendly
}
@@ -345,6 +348,20 @@ func(st *State) Restart() error {
return nil
}
+// SetLanguage validates and sets language according to the given ISO639 language code.
+func(st *State) SetLanguage(code string) error {
+ if code == "" {
+ st.Language = nil
+ }
+ l, err := lang.LanguageFromCode(code)
+ if err != nil {
+ return err
+ }
+ st.Language = &l
+ Logg.Infof("language set", "language", l)
+ return nil
+}
+
// String implements String interface
func(st State) String() string {
var flags string
@@ -353,7 +370,13 @@ func(st State) String() string {
} else {
flags = fmt.Sprintf("0x%x", st.Flags)
}
- return fmt.Sprintf("moves: %v idx: %v flags: %s path: %s", st.Moves, st.SizeIdx, flags, strings.Join(st.ExecPath, "/"))
+ var lang string
+ if st.Language == nil {
+ lang = "(default)"
+ } else {
+ lang = fmt.Sprintf("%s", *st.Language)
+ }
+ return fmt.Sprintf("moves: %v idx: %v flags: %s path: %s lang: %s", st.Moves, st.SizeIdx, flags, strings.Join(st.ExecPath, "/"), lang)
}
// initializes all flags not in control of client.
diff --git a/vm/input.go b/vm/input.go
@@ -156,7 +156,6 @@ func applyTarget(target []byte, st *state.State, ca cache.Memory, ctx context.Co
return sym, idx, err
}
idx = 0
- Logg.Debugf("inputsss", "sym", sym)
}
return sym, idx, nil
}
diff --git a/vm/runner.go b/vm/runner.go
@@ -11,6 +11,7 @@ import (
)
// Vm holds sub-components mutated by the vm execution.
+// TODO: Renderer should be passed to avoid proxy methods not strictly related to vm operation
type Vm struct {
st *state.State // Navigation and error states.
rs resource.Resource // Retrieves content, code, and templates for symbols.
@@ -30,7 +31,7 @@ func NewVm(st *state.State, rs resource.Resource, ca cache.Memory, sizer *render
sizer: sizer,
}
vmi.Reset()
- Logg.Infof("vm created with state", "state", st)
+ Logg.Infof("vm created with state", "state", st, "renderer", vmi.pg)
return vmi
}
@@ -57,16 +58,24 @@ func(vm *Vm) Run(b []byte, ctx context.Context) ([]byte, error) {
panic(err)
}
if r {
- //log.Printf("terminate set! bailing!")
Logg.InfoCtxf(ctx, "terminate set! bailing")
return []byte{}, nil
}
- //vm.st.ResetBaseFlags()
+
_, err = vm.st.ResetFlag(state.FLAG_TERMINATE)
if err != nil {
panic(err)
}
+ change, err := vm.st.ResetFlag(state.FLAG_LANG)
+ if err != nil {
+ panic(err)
+ }
+ if change {
+ ctx = context.WithValue(ctx, "Language", *vm.st.Language)
+ }
+
+
waitChange, err := vm.st.ResetFlag(state.FLAG_WAIT)
if err != nil {
panic(err)
@@ -497,5 +506,14 @@ func(vm *Vm) refresh(key string, rs resource.Resource, ctx context.Context) (str
}
}
+ haveLang, err := vm.st.MatchFlag(state.FLAG_LANG, false)
+ if err != nil {
+ panic(err)
+ }
+ if haveLang {
+ vm.st.SetLanguage(r.Content)
+ }
+
+
return r.Content, err
}
diff --git a/vm/runner_test.go b/vm/runner_test.go
@@ -62,6 +62,13 @@ func setFlag(sym string, input []byte, ctx context.Context) (resource.Result, er
}
+func set_lang(sym string, input []byte, ctx context.Context) (resource.Result, error) {
+ return resource.Result{
+ Content: string(input),
+ FlagSet: []uint32{state.FLAG_LANG},
+ }, nil
+}
+
type TestStatefulResolver struct {
state *state.State
}
@@ -103,6 +110,8 @@ func (r TestResource) FuncFor(sym string) (resource.EntryFunc, error) {
return getEcho, nil
case "setFlagOne":
return setFlag, nil
+ case "set_lang":
+ return set_lang, nil
}
return nil, fmt.Errorf("invalid function: '%s'", sym)
}
@@ -626,3 +635,28 @@ func TestCatchCleanMenu(t *testing.T) {
}
fmt.Printf("Result:\n%s", r)
}
+
+func TestSetLang(t *testing.T) {
+ st := state.NewState(0)
+ rs := TestResource{}
+ ca := cache.NewCache()
+ vm := NewVm(&st, &rs, ca, nil)
+
+ var err error
+
+ st.Down("root")
+
+ st.SetInput([]byte("no"))
+ b := NewLine(nil, LOAD, []string{"set_lang"}, []byte{0x01, 0x00}, nil)
+ b = NewLine(b, HALT, nil, nil, nil)
+
+ ctx := context.TODO()
+ b, err = vm.Run(b, ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ lang := *st.Language
+ if lang.Code != "nor" {
+ t.Fatalf("expected language 'nor',, got %s", lang.Code)
+ }
+}