diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f655bb0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/exe/ diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..38bb22a --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +go build -o ./exe/ ./ diff --git a/htmlx/element.go b/htmlx/element.go new file mode 100644 index 0000000..8f437ae --- /dev/null +++ b/htmlx/element.go @@ -0,0 +1,97 @@ +package htmlx + +import ( + "github.com/d5/tengo/v2" + "strings" + "html" + "fmt" +) + +const RawTag = "raw" + +// The type implements basic +// way to structrize HTML elements. +type Element struct { + tengo.ObjectImpl + Tag string + Attr map[string] string + Children []*Element + // The value makes sense only if + // the tag is the "raw" + Content string +} + +func (el *Element) TypeName() string { + return "*HTMLElement" +} + +// The method renders the element to it's +// HTML representation. +func (el *Element) String() string { + if el.Tag == RawTag { + return html.EscapeString(el.Content) + } + + var b strings.Builder + + fmt.Fprintf(&b, "<%s", el.Tag) + for k, v := range el.Attr { + fmt.Fprintf(&b, " %s=%q", k, v) + } + fmt.Fprint(&b, ">") + + for _, child := range el.Children { + if child == nil { + continue + } + fmt.Fprint(&b, child.String()) + } + + fmt.Fprintf(&b, "", el.Tag) + return b.String() +} + +func (el *Element) Body(els ...*Element) *Element { + el.Children = els + return el +} + +func (el *Element) IndexGet( + index tengo.Object, +) (tengo.Object, error) { + arg, ok := tengo.ToString(index) + if !ok { + return nil, tengo.ErrInvalidIndexValueType + } + + switch arg { + case "body" : + return &tengo.UserFunction{ + Name: "Element.Body", + Value: func( + args ...tengo.Object, + ) (tengo.Object, error) { + s := []*Element{} + for _, arg := range args { + el, ok := arg.(*Element) + if !ok { + str, ok := tengo.ToString(arg) + if ok { + s = append(s, &Element{ + Tag: RawTag, + Content: str, + }) + } + continue + } + s = append(s, el) + } + return el.Body(s...), nil + }, + }, nil + } + return nil, nil +} + + + diff --git a/htmlx/html.go b/htmlx/html.go new file mode 100644 index 0000000..00219a3 --- /dev/null +++ b/htmlx/html.go @@ -0,0 +1,86 @@ +package htmlx + +import "github.com/d5/tengo/v2" + +type HTML struct{ + tengo.ObjectImpl +} + +/* + +html.div({ + id: "some-el-id", + value: "shit value" +}).body( + html.raw("cock "), + html.strong("something") +) + +*/ + +func (html *HTML) IndexGet( + index tengo.Object, +) (tengo.Object, error) { + str, ok := tengo.ToString(index) + if !ok { + return nil, tengo.ErrInvalidIndexValueType + } + + fn := func(args ...tengo.Object) (tengo.Object, error) { + if len(args) > 1 { + return nil, tengo.ErrWrongNumArguments + } + var arg tengo.Object + if len(args) == 1 { + arg = args[0] + } + + if arg == nil { + return &Element{ + Tag: str, + }, nil + } + + if can := arg.CanIterate() ; !can { + return nil, tengo.ErrInvalidArgumentType{ + Name: "first", + Expected: "iterable", + Found: arg.TypeName(), + } + } + attr := map[string] string{} + iter := arg.Iterate() + for iter.Next() { + key, val := iter.Key(), iter.Value() + skey, ok := tengo.ToString(key) + if !ok { + return nil, tengo.ErrInvalidArgumentType{ + Name: "attribute(key)", + Expected: "stringer", + Found: key.TypeName(), + } + } + sval, ok := tengo.ToString(val) + if !ok { + return nil, tengo.ErrInvalidArgumentType{ + Name: "attribute(value)", + Expected: "stringer", + Found: val.TypeName(), + } + } + attr[skey] = sval + } + return &Element{ + Tag: str, + Attr: attr, + }, nil + } + + return &tengo.UserFunction{ + Name: str, + Value: fn, + }, nil +} + + + diff --git a/httpx/handler.go b/httpx/handler.go new file mode 100644 index 0000000..9358f3e --- /dev/null +++ b/httpx/handler.go @@ -0,0 +1,191 @@ +package httpx + +import ( + //"github.com/d5/tengo/v2" + "surdeus.su/util/tpp/mdx" + "surdeus.su/util/tpp/htmlx" + "surdeus.su/util/tpp" + "path/filepath" + "net/http" + "context" + "path" + "mime" + "log" + "os" + "io" +) + +var _ = http.Handler(&Handler{}) + +// The type describes behaviour +// of handling stuff like in PHP. +type Handler struct { + // THe field represents + // where we store site's + // source files and request handlers. + sourcePath string + // Preprocessor must be set by user + // to be able to bring custom features in. + pp *tpp.Preprocessor + // Aditional extension. ".tpp" by default. + // For example "file.html.tpp" will be + // first preprocessed TPP and sent back as simple HTML. + ext string + + // The global map accessable from all the + // request so you can keep the states + // between requests. + global any + md *mdx.Markdown + html *htmlx.HTML +} + +// Returns the new Handler with +// specified preprocessor, source path, +// preprocessor extension and the global value. +func NewHandler( + pp *tpp.Preprocessor, + src, ext string, + global any, +) *Handler { + ret := &Handler{} + ret.sourcePath = src + ret.ext = ext + ret.pp = pp + ret.global = global + return ret +} + +// Returns the default tpp.Preprocessor suitable for +// the most needs. +func DefaultPP(mod string) *tpp.Preprocessor { + t := tpp.NewTengo().SetPreCompile(func( + ctx context.Context, + s *tpp.Script, + ) { + s.SetImportDir(mod) + s.SetImports(&ModuleGetter{}) + s.EnableFileImport(true) + + s.Add("__http_request__", ctx.Value(KeyRequest)) + s.Add("__global__", ctx.Value(KeyGlobal)) + s.Add("__markdown__", ctx.Value(KeyMarkdown)) + s.Add("__html__", ctx.Value(KeyHTML)) + }).SetPreCode(func(ctx context.Context) []byte { + return []byte(` + markdown := func(...args) { + pp.write_raw(__markdown__(args...)) + } + http := immutable({ + request : __http_request__ + }) + html := __html__ + context.http = http + context.pp = pp + context.global = __global__ + context.html = html + import("./pre")(context) + `) + }).SetPostCode(func(ctx context.Context) []byte { + return []byte(` + import("./post")(context) + `) + }) + + return tpp.New(t) +} + +func (h *Handler) SetMD(md *mdx.Markdown) *Handler { + h.md = md + return h +} +func (h *Handler) SetHTML(html *htmlx.HTML) *Handler { + h.html = html + return h +} + +func (h *Handler) ServeHTTP( + w http.ResponseWriter, + r *http.Request, +) { + shouldProcess := true + urlPath := r.URL.Path + // Cleaning URL path to prevent injections. + urlPath = path.Clean(urlPath) + urlExt := path.Ext(urlPath) + + filePath := filepath.Join( + filepath.FromSlash(h.sourcePath), + filepath.FromSlash(urlPath), + ) + filePathTpp := filePath + h.ext + + //log.Println("pth:", urlPath, filePathTpp) + file, err := os.Open(filePathTpp) + if err != nil { + shouldProcess = false + file, err = os.Open(filePath) + if err != nil { + http.NotFound(w, r) + return + } + } + + //process := true + fileData, err := io.ReadAll(file) + if err != nil { + http.NotFound(w, r) + return + } + + ctx := context.WithValue( + r.Context(), + KeyRequest, + &Request{ + Request: r, + }, + ) + + ctx = context.WithValue( + ctx, + KeyGlobal, + h.global, + ) + + ctx = context.WithValue( + ctx, + KeyMarkdown, + h.md, + ) + + ctx = context.WithValue( + ctx, + KeyHTML, + h.html, + ) + + // Setting before the code to let it change own + // content type. + contentType := mime.TypeByExtension(urlExt) + w.Header().Set("Content-Type", contentType) + + processedData := fileData + if shouldProcess { + processedData, err = h.pp.Process( + ctx, + true, + filePathTpp, + fileData, + ) + if err != nil { + http.NotFound(w, r) + log.Printf( + "Error: pp.Process(...): %s\n", + err, + ) + return + } + } + + w.Write(processedData) +} diff --git a/httpx/http.go b/httpx/http.go new file mode 100644 index 0000000..0e441e6 --- /dev/null +++ b/httpx/http.go @@ -0,0 +1,2 @@ +package httpx + diff --git a/httpx/module.go b/httpx/module.go new file mode 100644 index 0000000..f7a0765 --- /dev/null +++ b/httpx/module.go @@ -0,0 +1,24 @@ +package httpx + +import "github.com/d5/tengo/v2" +import "github.com/d5/tengo/v2/stdlib" +import "surdeus.su/util/tpp/paths" + +var Modules = map[string]tengo.Importable{ + "paths": paths.Module, +} + +var Stdlib = stdlib.GetModuleMap(stdlib.AllModuleNames()...) + +type ModuleGetter struct{} + +func (m *ModuleGetter) Get( + name string, +) tengo.Importable { + module, exist := Modules[name] + if exist { + return module + } + + return Stdlib.Get(name) +} diff --git a/httpx/request.go b/httpx/request.go new file mode 100644 index 0000000..8f72830 --- /dev/null +++ b/httpx/request.go @@ -0,0 +1,49 @@ +package httpx + +import ( + "github.com/d5/tengo/v2" + "net/http" + "io" + //"log" +) + +type Request struct { + tengo.ObjectImpl + *http.Request +} + +func (r *Request) TypeName() string { + return "*http.Request" +} + +func (r *Request) String() string { + return "*http.Request{...}" +} + +func (r *Request) IndexGet( + index tengo.Object, +) (tengo.Object, error) { + key, ok := tengo.ToString(index) + if !ok { + return nil, tengo.ErrInvalidIndexValueType + } + + switch key { + case "url" : + return &URL{ + URL: r.URL, + }, nil + case "body" : + bts, err := io.ReadAll(r.Body) + if err != nil { + return nil, err + } + return &tengo.Bytes{ + Value: bts, + }, nil + } + + // Nothing found. + return nil, nil +} + diff --git a/httpx/tool.go b/httpx/tool.go new file mode 100644 index 0000000..08a60f9 --- /dev/null +++ b/httpx/tool.go @@ -0,0 +1,52 @@ +package httpx + +import ( + + "github.com/d5/tengo/v2" + //"github.com/d5/tengo/v2/stdlib" + "surdeus.su/util/tpp/mdx" + "surdeus.su/core/cli/mtool" + "net/http" + "log" + //"context" +) + +// Context key type for internal usage. +type CKey string + +const ( + KeyRequest CKey = "http-request" + KeyGlobal = "global" + KeyMarkdown = "markdown" + KeyHTML = "html" +) + +// Simple PHP-like server implementation. +var Tool = mtool.T("tht").Func(func(flags *mtool.Flags) { + var ( + addr, ext, src, mod string + ) + + flags.StringVar(&addr, "addr", ":3000", "address to serve at") + flags.StringVar(&mod, "mod", "./mod", "path to store Tengo modules") + flags.StringVar(&src, "src", "./src", "directory with source files") + flags.StringVar(&ext, "ext", ".tpp", "extension for TPP files") + + flags.Parse() + + + srv := &http.Server{ + Addr: addr, + Handler: NewHandler( + DefaultPP(mod), + src, ext, + map[string] tengo.Object{}, + ).SetMD(mdx.MakeDefaultMarkdown()), + } + + log.Printf("Listening on %q\n", addr) + err := srv.ListenAndServe() + if err != nil { + log.Printf("Error: srv.ListenAndServe(...): %s\n", err) + } +}) diff --git a/httpx/url.go b/httpx/url.go new file mode 100644 index 0000000..1e0727e --- /dev/null +++ b/httpx/url.go @@ -0,0 +1,76 @@ +package httpx + +import ( + "github.com/d5/tengo/v2" + "net/url" + "fmt" +) + +var _ = tengo.Object(&Values{}) +type Values struct { + tengo.ObjectImpl + url.Values +} + +func (vs *Values) TypeName() string { + return "*url.Values" +} + +func (vs *Values) String() string { + return fmt.Sprintf("%v", vs.Values) +} + +func (vs *Values) IndexGet( + index tengo.Object, +) (tengo.Object, error) { + key, ok := tengo.ToString(index) + if !ok { + return nil, tengo.ErrInvalidIndexValueType + } + + val, ok := vs.Values[key] + if !ok { + return nil, nil + } + + arr := make([]tengo.Object, len(val)) + for i, v := range val { + arr[i], _ = tengo.FromInterface(v) + } + + return &tengo.Array{Value: arr}, nil +} + +type URL struct { + tengo.ObjectImpl + *url.URL +} + +func (u *URL) TypeName() string { + return "" +} + +func (u *URL) String() string { + return u.URL.String() +} + +func (u *URL) IndexGet( + index tengo.Object, +) (tengo.Object, error) { + key, ok := tengo.ToString(index) + if !ok { + return nil, tengo.ErrInvalidIndexValueType + } + + switch key { + case "path" : + return tengo.FromInterface(u.Path) + case "query" : + return &Values{ + Values: u.Query(), + }, nil + } + + // Nothing found. + return nil, nil +} diff --git a/sjson/dec.go b/sjson/dec.go new file mode 100644 index 0000000..269da02 --- /dev/null +++ b/sjson/dec.go @@ -0,0 +1,106 @@ +package sjson + +import "github.com/d5/tengo/v2" +import "encoding/json" +import "io" +import "errors" +import tjson "github.com/d5/tengo/v2/stdlib/json" +import "os" + +type Decoder struct { + tengo.ObjectImpl + dec *json.Decoder + end bool + inputName string +} + +func (d *Decoder) TypeName() string { + return "*sjson.Decoder" +} + +func (d *Decoder) String() string { + return "sjson.Decoder{...}" +} + +func NewDecoder(args ...tengo.Object) (tengo.Object, error) { + var inputName string + if len(args) == 0 { + inputName = StrStdin + } else if len(args) > 1 { + return nil, tengo.ErrWrongNumArguments + } + + inputObject := args[0] + inputName, ok := tengo.ToString(inputObject) + if !ok { + return nil, tengo.ErrInvalidArgumentType{ + Name: "first", + Expected: "stringer", + Found: inputObject.TypeName(), + } + } + + var reader io.Reader + var err error + switch { + case inputName == StrStdin : + reader = os.Stdin + default: + reader, err = os.Open(inputName) + if err != nil { + return nil, err + } + } + + ret := &Decoder{} + ret.dec = json.NewDecoder(reader) + + return ret, nil +} + +func (d *Decoder) IndexGet( + index tengo.Object, +) (tengo.Object, error) { + key, ok := tengo.ToString(index) + if !ok { + return nil, tengo.ErrInvalidIndexValueType + } + + switch key { + case "decode" : + return &tengo.UserFunction{ + Name: "decode", + Value: d.Decode, + }, nil + } + + // Nothing found. + return nil, nil +} + +func (d *Decoder) Decode( + args ...tengo.Object, +) (tengo.Object, error) { + if len(args) > 0 { + return nil, tengo.ErrWrongNumArguments + } + if d.end { + return nil, nil + } + + var v any + err := d.dec.Decode(&v) + if err != nil { + if errors.Is(err, io.EOF) { + d.end = true + } else { + return nil, err + } + } + + bts, err := json.Marshal(v) + if err != nil { + return nil, err + } + return tjson.Decode(bts) +} diff --git a/sjson/sjson.go b/sjson/sjson.go new file mode 100644 index 0000000..f53e04c --- /dev/null +++ b/sjson/sjson.go @@ -0,0 +1,33 @@ +package sjson + +import "github.com/d5/tengo/v2" +import "io" +import "encoding/json" +//import "github.com/d5/tengo/v2/stdlib" + +const ( + StrStdin = "" +) + +var Module = map[string]tengo.Object{ + "__check": &tengo.BuiltinFunction{ + Name: "__check", + Value: func( + args ...tengo.Object, + ) (tengo.Object, error) { + return tengo.FromInterface("Hello, Check!") + }, + }, + "new_decoder": &tengo.BuiltinFunction{ + Name: "new_decoder", + Value: NewDecoder, + }, + +} + +type Encoder struct { + enc *json.Encoder + output io.Writer +} + +//func diff --git a/tests/sjson.xgo b/tests/sjson.xgo new file mode 100644 index 0000000..cc499c3 --- /dev/null +++ b/tests/sjson.xgo @@ -0,0 +1,13 @@ +sjson := import("sjson") +fmt := import("fmt") +fmt.println(sjson.__check()) + +dec := sjson.new_decoder("") + +for { + v := dec.decode() + if !v { + break + } + fmt.println(v) +} diff --git a/xtool/main.go b/xtool/main.go index 62ef3ce..461b688 100644 --- a/xtool/main.go +++ b/xtool/main.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strings" + "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/parser" "github.com/d5/tengo/v2/stdlib" @@ -18,6 +19,7 @@ import ( ) import "surdeus.su/core/cli/mtool" +import "surdeus.su/core/xgo/sjson" const ( sourceFileExt = ".xgo" @@ -50,8 +52,8 @@ func Run(flags *mtool.Flags) { fmt.Println(version) return } - modules := stdlib.GetModuleMap(stdlib.AllModuleNames()...) + modules.AddBuiltinModule("sjson", sjson.Module) if len(iargs) == 0 { // REPL RunREPL(modules, os.Stdin, os.Stdout)