feat: added the sjson module.

This commit is contained in:
Andrey Parhomenko 2024-06-06 02:13:12 +05:00
parent c5b303bd15
commit 784b2dbfdf
14 changed files with 736 additions and 1 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/exe/

3
build.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/sh
go build -o ./exe/ ./

97
htmlx/element.go Normal file
View file

@ -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, "</%s>", 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
}

86
htmlx/html.go Normal file
View file

@ -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
}

191
httpx/handler.go Normal file
View file

@ -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)
}

2
httpx/http.go Normal file
View file

@ -0,0 +1,2 @@
package httpx

24
httpx/module.go Normal file
View file

@ -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)
}

49
httpx/request.go Normal file
View file

@ -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
}

52
httpx/tool.go Normal file
View file

@ -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)
}
})

76
httpx/url.go Normal file
View file

@ -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 "<URL>"
}
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
}

106
sjson/dec.go Normal file
View file

@ -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)
}

33
sjson/sjson.go Normal file
View file

@ -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 = "<stdin>"
)
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

13
tests/sjson.xgo Normal file
View file

@ -0,0 +1,13 @@
sjson := import("sjson")
fmt := import("fmt")
fmt.println(sjson.__check())
dec := sjson.new_decoder("<stdin>")
for {
v := dec.decode()
if !v {
break
}
fmt.println(v)
}

View file

@ -11,6 +11,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/d5/tengo/v2" "github.com/d5/tengo/v2"
"github.com/d5/tengo/v2/parser" "github.com/d5/tengo/v2/parser"
"github.com/d5/tengo/v2/stdlib" "github.com/d5/tengo/v2/stdlib"
@ -18,6 +19,7 @@ import (
) )
import "surdeus.su/core/cli/mtool" import "surdeus.su/core/cli/mtool"
import "surdeus.su/core/xgo/sjson"
const ( const (
sourceFileExt = ".xgo" sourceFileExt = ".xgo"
@ -50,8 +52,8 @@ func Run(flags *mtool.Flags) {
fmt.Println(version) fmt.Println(version)
return return
} }
modules := stdlib.GetModuleMap(stdlib.AllModuleNames()...) modules := stdlib.GetModuleMap(stdlib.AllModuleNames()...)
modules.AddBuiltinModule("sjson", sjson.Module)
if len(iargs) == 0 { if len(iargs) == 0 {
// REPL // REPL
RunREPL(modules, os.Stdin, os.Stdout) RunREPL(modules, os.Stdin, os.Stdout)