This commit is contained in:
Andrey Parhomenko 2024-06-09 17:16:38 +05:00
commit 5db9429bd8
22 changed files with 657 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/tpp
/exe
Session.vim

4
build.sh Executable file
View file

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

10
cmd/tht/main.go Normal file
View file

@ -0,0 +1,10 @@
package main
import (
"surdeus.su/util/tht/server"
"os"
)
func main() {
server.Tool.Run(os.Args[1:])
}

11
go.mod Normal file
View file

@ -0,0 +1,11 @@
module surdeus.su/util/tht
go 1.22.3
require (
github.com/d5/tengo/v2 v2.17.0
github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2
surdeus.su/core/cli v0.1.2
surdeus.su/core/xgo v0.5.0
surdeus.su/util/tpp v0.2.0
)

10
go.sum Normal file
View file

@ -0,0 +1,10 @@
github.com/d5/tengo/v2 v2.17.0 h1:BWUN9NoJzw48jZKiYDXDIF3QrIVZRm1uV1gTzeZ2lqM=
github.com/d5/tengo/v2 v2.17.0/go.mod h1:XRGjEs5I9jYIKTxly6HCF8oiiilk5E/RYXOZ5b0DZC8=
github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2 h1:yEt5djSYb4iNtmV9iJGVday+i4e9u6Mrn5iP64HH5QM=
github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
surdeus.su/core/cli v0.1.2 h1:qPzjawqPyZsO4Z5SaA1u141recVE65yioA83Qs7Jecs=
surdeus.su/core/cli v0.1.2/go.mod h1:r9JtQz3aEJzpYzMaNUNQHJoYkoWKNPi047qhd5uGlmA=
surdeus.su/core/xgo v0.5.0 h1:/Rk3scfFkoSb0qjHRlkUNOp9sr/fd7wAvCiT4fBRo+U=
surdeus.su/core/xgo v0.5.0/go.mod h1:6C/AHbjfvAMvt3TOzLB4eIZ40eU3ahJXtdY+kr4yXoc=
surdeus.su/util/tpp v0.2.0 h1:9N78JPEUovDWdKnrdfrf05Jkz5xMA5hcBEWCIh4RWW0=
surdeus.su/util/tpp v0.2.0/go.mod h1:3bcSjGNd3vAevr6gPMI1/qJofxL7x53v0yB+Dr18CRM=

3
install.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/sh
go install ./cmd/tht/

83
mdx/main.go Normal file
View file

@ -0,0 +1,83 @@
package mdx
import "github.com/gomarkdown/markdown"
import "github.com/gomarkdown/markdown/html"
import "github.com/gomarkdown/markdown/parser"
import "github.com/d5/tengo/v2"
import "bytes"
type Markdown struct {
tengo.ObjectImpl
MakeParser func() *parser.Parser
MakeRenderer func() *html.Renderer
}
func MakeDefaultMarkdown() *Markdown {
return &Markdown{
MakeParser: MakeDefaultParser,
MakeRenderer: MakeDefaultRenderer,
}
}
func (md *Markdown) TypeName() string {
return "*Markdown"
}
func (md *Markdown) String() string {
if md == nil {
return "<nil>"
}
return "&Markdown{...}"
}
func (md *Markdown) Render(
data []byte,
) ([]byte, error) {
doc := md.MakeParser().Parse(data)
rendered := markdown.Render(doc, md.MakeRenderer())
return rendered, nil
}
func (md *Markdown) CanCall() bool {
return md != nil
}
func (md *Markdown) Call(
args ...tengo.Object,
) (tengo.Object, error) {
var b bytes.Buffer
for _, arg := range args {
str, ok := tengo.ToString(arg)
if !ok {
return nil, tengo.ErrInvalidArgumentType{
Name: "v",
Expected: "stringer",
Found: arg.TypeName(),
}
}
b.Write([]byte(str))
}
rendered, err := md.Render(b.Bytes())
if err != nil {
return nil, err
}
return tengo.FromInterface(rendered)
}
func MakeDefaultParser() *parser.Parser {
return parser.NewWithExtensions(
parser.CommonExtensions | parser.AutoHeadingIDs |
parser.NoEmptyLineBeforeBlock | parser.Attributes |
parser.Tables,
)
}
func MakeDefaultRenderer() *html.Renderer {
return html.NewRenderer(
html.RendererOptions{
Flags: html.CommonFlags | html.HrefTargetBlank,
},
)
}

26
mod/post.tengo Normal file
View file

@ -0,0 +1,26 @@
fmt := import("fmt")
paths := import("paths")
htmExt := func(c){
if c.is_compo {
return
}
c.pp.print(`</body></html>`)
}
exts := {
".htm": htmExt,
".html": htmExt
}
export func(c){
ext := paths.ext(c.pp.filepath)
ext = paths.ext(
c.pp.filepath[:len(c.pp.filepath) - len(ext)]
)
cl := exts[ext]
if !is_undefined(cl) {
cl(c)
}
}

40
mod/pre.tengo Normal file
View file

@ -0,0 +1,40 @@
fmt := import("fmt")
paths := import("paths")
html := import("html").new_render()
htmExt := func(c){
if c.is_compo {
return
}
c.pp.print(`<!doctype html><html>`)
c.pp.print(html.head().body(
func(){
if c.title {
return html.title().body(c.title)
}
}(),
html.script({
src: `https://unpkg.com/htmx.org@1.9.2`
})
))
c.pp.print(`<body>`)
}
exts := {
".htm": htmExt,
".html": htmExt
}
export func(c){
ext := paths.ext(c.pp.filepath)
ext = paths.ext(
c.pp.filepath[:len(c.pp.filepath) - len(ext)]
)
cl := exts[ext]
if cl {
cl(c)
}
}

189
server/handler.go Normal file
View file

@ -0,0 +1,189 @@
package server
import (
//"github.com/d5/tengo/v2"
"surdeus.su/util/tpp/mdx"
//"surdeus.su/core/xgo/xmodules/htmlx"
"surdeus.su/core/xgo/xmodules/httpx"
"surdeus.su/core/xgo/xmodules"
"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
indexSuffix string
}
// Returns the new Handler with
// specified preprocessor, source path,
// preprocessor extension and the global value.
func NewHandler(
pp *tpp.Preprocessor,
src, ext, index string,
global any,
) *Handler {
ret := &Handler{}
ret.sourcePath = src
ret.ext = ext
ret.pp = pp
ret.global = global
ret.indexSuffix = index
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(xmodules.Modules)
s.EnableFileImport(true)
s.Add("__http_request__", ctx.Value(KeyRequest))
s.Add("__global__", ctx.Value(KeyGlobal))
s.Add("__markdown__", ctx.Value(KeyMarkdown))
}).SetPreCode(func(ctx context.Context) []byte {
return []byte(`
markdown := func(...args) {
pp.write_raw(__markdown__(args...))
}
__http__ := immutable({
request : __http_request__
})
context.http = __http__
context.pp = pp
context.global = __global__
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) 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),
)
fileStat, err := os.Stat(filePath)
if err == nil {
if fileStat.IsDir() {
filePath = filepath.Join(
filePath,
h.indexSuffix,
)
}
}
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,
&httpx.Request{
Request: r,
},
)
ctx = context.WithValue(
ctx,
KeyGlobal,
h.global,
)
ctx = context.WithValue(
ctx,
KeyMarkdown,
h.md,
)
// 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)
}

53
server/tool.go Normal file
View file

@ -0,0 +1,53 @@
package server
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, index 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.StringVar(&index, "index", "index.htm", "index file name")
flags.Parse()
srv := &http.Server{
Addr: addr,
Handler: NewHandler(
DefaultPP(mod),
src, ext, index,
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)
}
})

5
src/check.htm.tpp Normal file
View file

@ -0,0 +1,5 @@
<main>shit</main>
{{
pp.print("cock")
}}
<main>later</main>

12
src/check/index.htm.tpp Normal file
View file

@ -0,0 +1,12 @@
{{#
html := import("html").new_render()
}}
{{
h := html.main().body(
html.p().body(
"some paragraph shit"
)
)
pp.print(h)
}}

20
src/contact/edit.htm.tpp Normal file
View file

@ -0,0 +1,20 @@
{{#
context.is_compo = true
}}
<form hx-put="/contact/1" hx-target="this" hx-swap="outerHTML">
<div>
<label>First Name</label>
<input type="text" name="firstName" value="Joe">
</div>
<div class="form-group">
<label>Last Name</label>
<input type="text" name="lastName" value="Blow">
</div>
<div class="form-group">
<label>Email Address</label>
<input type="email" name="email" value="joe@blow.com">
</div>
<button class="btn">Submit</button>
<button class="btn" hx-get="/contact/view.htm">Cancel</button>
</form>

13
src/contact/view.htm.tpp Normal file
View file

@ -0,0 +1,13 @@
{{#
//context.is_compo = true
}}
<div hx-target="this" hx-swap="outerHTML">
<div><label>First Name</label>: Joe</div>
<div><label>Last Name</label>: Blow</div>
<div><label>Email</label>: joe@blow.com</div>
<button hx-get="/contact/edit.htm" class="btn btn-primary">
Click To Edit
</button>
</div>

11
src/incorrect.htm.tpp Normal file
View file

@ -0,0 +1,11 @@
html := import("html").new_render()
pp.print(html.html().body(
html.head().body(
html.title().body(
"some title"
)
),
html.body().body(
)
))

78
src/index.htm.tpp Normal file
View file

@ -0,0 +1,78 @@
{{#
context.title = "The example.site main page"
fmt := import("fmt")
html := import("html").new_render()
}}
<h1>This is the example page for the THT</h1>
{{
/*if !context.global.int {
context.global.int = 0
} else {
context.global.int++
}*/
if !context.global.val {
context.global.val = 1
} else {
context.global.val += 1
}
fmt.println("global: ", context.global)
req := context.http.request
q := req.url.query
// List checking.
list := []
rng := int(q.range[0])
fmt.println("range:", q.range)
if rng {
pp.print(`<ul name="range">`)
for i:=0 ; i < rng ; i++ {
pp.printf("<li>%d</li>", i)
}
pp.print("</ul>")
}
if q.name {
pp.print("<div id=\"name\">", q.name[0], "</div>")
}
}}
<div name="counter">
{{ pp.print(context.global.val) }}
</div>
<div>
Hello, Cock!
</div>
<div check>
{{
pp.print(req.url.path)
}}
</div>
<ul>{{
for v in list {
pp.print("<li>", v, "</li>")
}
}}</ul>
{{
vals := ["die", "with", "them", "as", "you", 1, "could", "do"]
pp.print(html.div({
id: "the-uniq-shit"
}).body(
html.ul(
).body(
func(){
ret := []
for i:=0 ; i<len(vals) ; i++{
ret += [html.li({
class: "someclass"
}).body(i, ": ", vals[i])]
}
return ret
}()...
)
))
}}
<script src="/script.js" ></script>
<div></div>

37
src/main.htm.tpp Normal file
View file

@ -0,0 +1,37 @@
{{#
context.title = "The example.site main page"
fmt := import("fmt")
html := import("html").new_render()
}}
{{
pp.print(
html.h1().body("This is the example page for THT")
)
if !context.global.val {
context.global.val = 1
} else {
context.global.val += 1
}
}}
{{
req := context.http.request
q := req.url.query
// List checking.
rng := int(q.range[0])
fmt.println("range:", q.range)
if rng {
pp.print(`<ul name="range">`)
for i:=0 ; i < rng ; i++ {
pp.printf("<li>%d</li>", i)
}
pp.print("</ul>")
}
if q.name {
pp.print("<div id=\"name\">", q.name[0], "</div>")
}
}}

35
src/page.htm.tpp Normal file
View file

@ -0,0 +1,35 @@
{{#
context.title = "Let's celebrate and suck, SOME DICK"
}}
{{markdown(`
# Hello, World!
This is the markdown example shit.
func main() {
print("Fuck you")
}
## Yet another level of header
And even more text
> Cheap is talk, show me the code.
(c) Linus Torvalds
`)}}
<div id="uniq">
Even <strong>more</strong> HTML
</div>
{{markdown(`
### Third level header
Some shit
#### `, 135, `
`)}}

8
src/req.json.tpp Normal file
View file

@ -0,0 +1,8 @@
{
"error": null,
"data": {
"name": "Andrey",
"surname": "Parhomenko",
"age": 22
}
}

3
src/request.json Normal file
View file

@ -0,0 +1,3 @@
{
"name":"Andrew"
}

3
src/script.js Normal file
View file

@ -0,0 +1,3 @@
console.log("Hello, World!")