jrFlint 2 years ago
commit
bd52ccf7e7
6 changed files with 286 additions and 0 deletions
  1. 5 0
      go.mod
  2. 31 0
      go.sum
  3. 43 0
      metla.go
  4. 165 0
      parser.go
  5. 1 0
      test/index.html
  6. 41 0
      z_test.go

+ 5 - 0
go.mod

@@ -0,0 +1,5 @@
+module git.ali33.ru/fcg-xvii/metla
+
+go 1.12
+
+require git.ali33.ru/fcg-xvii/mjs v0.0.0-20220517153042-199c5e0b2dcc // indirect

+ 31 - 0
go.sum

@@ -0,0 +1,31 @@
+git.ali33.ru/fcg-xvii/go-tools v0.0.0-20220506161429-319953bb5590 h1:e7c8b9TovkJN/cmAFYoNmiYCyjGd16HjXL3D/bG1ZCo=
+git.ali33.ru/fcg-xvii/go-tools v0.0.0-20220506161429-319953bb5590/go.mod h1:8XpQShSOR7fAiCJg56M2mhf1KxiqnTeGttW/CUuyyCk=
+git.ali33.ru/fcg-xvii/mjs v0.0.0-20220517153042-199c5e0b2dcc h1:0Wia3W2Wp6hAi+194wEqKoctbgQC4XqeUZoxbCXcwio=
+git.ali33.ru/fcg-xvii/mjs v0.0.0-20220517153042-199c5e0b2dcc/go.mod h1:f9ApGKec9HmiIR27bm1LsU+yRr5Vgi4wMvMFOXbIOro=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E=
+github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
+github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
+github.com/dop251/goja v0.0.0-20220516123900-4418d4575a41 h1:yRPjAkkuR/E/tsVG7QmhzEeEtD3P2yllxsT1/ftURb0=
+github.com/dop251/goja v0.0.0-20220516123900-4418d4575a41/go.mod h1:TQJQ+ZNyFVvUtUEtCZxBhfWiH7RJqR3EivNmvD6Waik=
+github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
+github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
+github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
+github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+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=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

+ 43 - 0
metla.go

@@ -0,0 +1,43 @@
+package metla
+
+import (
+	"fmt"
+	"io"
+	"path/filepath"
+
+	"git.ali33.ru/fcg-xvii/mjs"
+)
+
+func New(modifiedCallback func(string) int64, contentCallback func(string) ([]byte, error)) *Metla {
+	m := &Metla{
+		contentCallback: contentCallback,
+	}
+	m.js = mjs.New(modifiedCallback, m.content)
+	return m
+}
+
+type Metla struct {
+	js              *mjs.Mjs
+	contentCallback func(string) ([]byte, error)
+}
+
+func (s *Metla) content(name string) (content []byte, err error) {
+	if content, err = s.contentCallback(name); err == nil {
+		if filepath.Ext(name) != ".script" {
+			content, err = parseBytes(content, name)
+		}
+	}
+	return
+}
+
+func (s *Metla) Exec(name string, data map[string]interface{}, w io.Writer) (modified int64, err error) {
+	if data == nil {
+		data = make(map[string]interface{})
+	}
+	data["flush"] = func(vals ...interface{}) {
+		for _, v := range vals {
+			w.Write([]byte(fmt.Sprint(v)))
+		}
+	}
+	return s.js.Exec(name, data)
+}

+ 165 - 0
parser.go

@@ -0,0 +1,165 @@
+package metla
+
+import (
+	"bytes"
+	"fmt"
+)
+
+type tokenType uint8
+
+const (
+	tokenTypeUndefined tokenType = iota
+	tokenTypeText
+	tokenTypePrint
+	tokenTypeComment
+	tokenTypeExec
+	tokenTypeLiteral
+)
+
+func (s tokenType) closeTag() byte {
+	switch s {
+	case tokenTypePrint:
+		return '}'
+	case tokenTypeExec:
+		return '%'
+	case tokenTypeComment:
+		return '*'
+	default:
+		return 0
+	}
+}
+
+func (s tokenType) String() string {
+	switch s {
+	case tokenTypeUndefined:
+		return "Undefined"
+	case tokenTypeText:
+		return "Text"
+	case tokenTypePrint:
+		return "Print"
+	case tokenTypeComment:
+		return "Comment"
+	case tokenTypeExec:
+		return "Exec"
+	case tokenTypeLiteral:
+		return "Literal"
+	default:
+		return ""
+	}
+}
+
+type tError struct {
+	alias string
+	line  int
+	pos   int
+	text  string
+}
+
+func (s tError) Error() string {
+	return fmt.Sprintf("%v :: Parse error [%v:%v] : %v", s.alias, s.line, s.pos, s.text)
+}
+
+func parseBytes(src []byte, alias string) (res []byte, err error) {
+	var rb bytes.Buffer
+	lp, lastPos, lastLine := lineParser{src: src}, 0, 0
+
+	flushTokenText := func(fromToken bool) {
+		if lastPos != lp.Pos() {
+			offset := -1
+			if fromToken {
+				offset = 1
+			}
+			rb.Write([]byte("flush('"))
+			rb.Write(bytes.ReplaceAll(bytes.ReplaceAll(lp.Block(lastPos, offset), []byte("'"), []byte("\\'")), []byte("\n"), []byte("\\n")))
+			rb.Write([]byte("');"))
+			lastPos, lastLine = lp.Pos(), lp.Line()
+		}
+	}
+
+	initToken := func() error {
+		val, tType := lp.Val(), tokenTypeUndefined
+		switch val {
+		case '{':
+			tType = tokenTypePrint
+		case '*':
+			tType = tokenTypeComment
+		case '%':
+			tType = tokenTypeExec
+		default:
+			return nil
+		}
+		flushTokenText(true)
+		for lp.FindNext(tType.closeTag()) {
+			if lp.Next() && lp.Val() == '}' {
+				switch tType {
+				case tokenTypePrint:
+					{
+						rb.Write([]byte("flush("))
+						rb.Write(lp.Block(lastPos+2, 2))
+						rb.Write([]byte(");"))
+					}
+				case tokenTypeExec:
+					rb.Write(lp.Block(lastPos+2, 2))
+					rb.Write([]byte(";"))
+				}
+				lp.Next()
+				lastPos, lastLine = lp.Pos(), lp.Line()
+				return nil
+			}
+		}
+		return tError{
+			alias: alias,
+			line:  lastLine,
+			pos:   lastPos,
+			text:  fmt.Sprintf("Not found close token for '%v}'", string(tType.closeTag())),
+		}
+	}
+
+	for lp.CheckNext() {
+		if lp.FindNext('{') && lp.Next() {
+			if err = initToken(); err != nil {
+				return
+			}
+		}
+	}
+	flushTokenText(false)
+	res = rb.Bytes()
+	return
+}
+
+type lineParser struct {
+	src  []byte
+	pos  int
+	line int
+}
+
+func (s *lineParser) CheckNext() bool {
+	return s.pos < len(s.src)-1
+}
+
+func (s *lineParser) Next() bool {
+	if s.CheckNext() {
+		s.pos++
+		if s.src[s.pos] == '\n' {
+			s.line++
+		}
+		return true
+	}
+	return false
+}
+
+func (s *lineParser) Block(start int, offsetRight int) []byte { return s.src[start : s.pos-offsetRight] }
+func (s *lineParser) Line() int                               { return s.line }
+func (s *lineParser) Pos() int                                { return s.pos }
+func (s *lineParser) Val() byte                               { return s.src[s.pos] }
+
+func (s *lineParser) FindNext(val byte) bool {
+	for {
+		if s.Val() == val {
+			return true
+		}
+		if !s.Next() {
+			return false
+		}
+	}
+}

+ 1 - 0
test/index.html

@@ -0,0 +1 @@
+<p>Hi, {{ name }}</p>

+ 41 - 0
z_test.go

@@ -0,0 +1,41 @@
+package metla
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"testing"
+)
+
+var contentPath = "test"
+
+func filePath(name string) string {
+	return fmt.Sprintf("%v/%v", contentPath, name)
+}
+
+func modified(name string) (res int64) {
+	if info, err := os.Stat(filePath(name)); err == nil {
+		res = info.ModTime().Unix()
+	}
+	return
+}
+
+func content(name string) ([]byte, error) {
+	return ioutil.ReadFile(filePath(name))
+}
+
+func TestParser(t *testing.T) {
+	m := New(modified, content)
+	log.Println(m)
+
+	var b bytes.Buffer
+
+	params := map[string]interface{}{
+		"name": "Heya ))",
+	}
+	log.Println(m.Exec("index.html", params, &b))
+
+	log.Println(b.String())
+}