├── test ├── a.json ├── blog │ ├── build_tailwind.sh │ ├── tailwind.config.js │ ├── Header.jsx │ ├── component │ │ └── Container.tsx │ ├── page │ │ ├── BlogDetail.tsx │ │ └── Home.jsx │ ├── Index.tsx │ └── tailwind.css ├── preact │ ├── package.json │ ├── src │ │ ├── page │ │ │ ├── nav.tsx │ │ │ ├── index.tsx │ │ │ ├── about.tsx │ │ │ └── component.tsx │ │ ├── package.json │ │ └── index.tsx │ ├── yarn.lock │ └── preact_test.go ├── v8bind │ ├── readme.md │ └── test.js ├── md.md ├── sourcemap │ ├── readme.md │ ├── sourcemap_test.go │ └── sourcemap.go ├── Form.jsx ├── mdx.mdx ├── goja │ └── goja_test.go ├── StepList.tsx ├── App.jsx ├── Index.jsx └── esbuild │ └── esbuild_test.go ├── .gitignore ├── internal ├── pkg │ └── goja_nodejs │ │ ├── require │ │ ├── testdata │ │ │ └── m.js │ │ ├── readme.md │ │ ├── resolve.go │ │ ├── module.go │ │ └── module_test.go │ │ └── console │ │ ├── console.go │ │ └── util.go └── js │ ├── index.go │ └── jsx-runtime.js ├── pkg ├── htmlparser │ ├── README.md │ ├── lex_test.go │ ├── hash.go │ ├── util.go │ └── lex.go ├── timetrack │ └── timetrack.go ├── mdx │ ├── jsxparser.go │ ├── jsxparser_test.go │ ├── testdata │ │ ├── Introduction.md.out.txt │ │ ├── Introduction.md │ │ ├── fulldemo.md │ │ └── fulldemo.md.out.txt │ ├── mdx_test.go │ ├── jscodeparser.go │ └── mdx.go └── html2jsx │ ├── html2jsx_test.go │ └── html2jsx.go ├── util_test.go ├── tpool.go ├── go.mod ├── util.go ├── goja_fieldmapping.go ├── goja_exception_test.go ├── goja_exception.go ├── benchmark_test.go ├── README.md ├── transform_test.go ├── transform.go ├── go.sum ├── jsx_test.go └── LICENSE /test/a.json: -------------------------------------------------------------------------------- 1 | {"name": "gojsx"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | node_modules/ 3 | /.cache/ 4 | -------------------------------------------------------------------------------- /test/blog/build_tailwind.sh: -------------------------------------------------------------------------------- 1 | npx tailwindcss -o tailwind.css --watch -------------------------------------------------------------------------------- /test/preact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "preact": "^10.16.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/v8bind/readme.md: -------------------------------------------------------------------------------- 1 | v8bind 运行效率更快,但是由于无法与 golang 公用大对象,中间需要通过序列化等手段传输,反而在传输过程中更消耗性能。 2 | 3 | 因此对于大数据的场景,v8bind 并不适用。v8bind 更适用于 CPU 密集场景。 -------------------------------------------------------------------------------- /internal/pkg/goja_nodejs/require/testdata/m.js: -------------------------------------------------------------------------------- 1 | function test() { 2 | return "passed"; 3 | } 4 | 5 | module.exports = { 6 | test: test 7 | } 8 | -------------------------------------------------------------------------------- /test/md.md: -------------------------------------------------------------------------------- 1 | # md 2 | 3 | <> 4 | 133 5 | <> 6 | 7 | ```js 8 | a = {a""123<''} 9 | ``` 10 | 11 | a = {a""123<''} 12 | 13 | -------------------------------------------------------------------------------- /test/sourcemap/readme.md: -------------------------------------------------------------------------------- 1 | 由 https://github.com/neelance/sourcemap 改造 2 | 3 | - 从 0 开始计数 4 | - 使用 https://www.murzwin.com/base64vlq.html 校验生成的 mapping 是否正确 5 | -------------------------------------------------------------------------------- /test/preact/src/page/nav.tsx: -------------------------------------------------------------------------------- 1 | export default function Nav(){ 2 | return <> 3 | index 4 | about 5 | 6 | } -------------------------------------------------------------------------------- /test/blog/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | './**/*.{js,jsx,ts,tsx,vue}', 4 | ], 5 | theme: {}, 6 | variants: {}, 7 | plugins: [], 8 | } -------------------------------------------------------------------------------- /internal/pkg/goja_nodejs/require/readme.md: -------------------------------------------------------------------------------- 1 | fork from https://github.com/dop251/goja_nodejs 2 | 3 | ## Change 4 | - 优先使用主动注册的 module 5 | - 编译缓存使用 md5(body) 作为缓存 key 6 | - 优化 InvalidModuleError 报错信息 7 | -------------------------------------------------------------------------------- /test/blog/Header.jsx: -------------------------------------------------------------------------------- 1 | export default function Header(props) { 2 | return
5 | Hello: {props.name} 6 |
7 | } -------------------------------------------------------------------------------- /test/Form.jsx: -------------------------------------------------------------------------------- 1 | export default function Form({children, c}) { 2 | let x = 1 3 | x++ 4 | return
5 | {children.map(i => i)} x:{x} 6 | c: {c} 7 |
8 | } 9 | -------------------------------------------------------------------------------- /internal/js/index.go: -------------------------------------------------------------------------------- 1 | package js 2 | 3 | import _ "embed" 4 | 5 | //// Babel copy from https://babel.docschina.org/docs/en/babel-standalone/ 6 | ////go:embed babel.min.js 7 | //var Babel string 8 | 9 | //go:embed jsx-runtime.js 10 | var JsxRuntime []byte 11 | -------------------------------------------------------------------------------- /test/blog/component/Container.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | children?: any[] 3 | } 4 | 5 | export default function Container(props: Props) { 6 | return
7 | {props.children} 8 |
9 | } 10 | -------------------------------------------------------------------------------- /pkg/htmlparser/README.md: -------------------------------------------------------------------------------- 1 | Base on github.com/tdewolff/parse/v2/html 2 | 3 | The purpose is to parse html and jsx 4 | 5 | Jsx and html syntax are different, so I made the following modifications: 6 | 7 | - `<>`:现在会按照 tag 处理,原来会处理成 text。 8 | - 兼容 `` 语法,能正常解析出 attribute 和 闭合。 9 | -------------------------------------------------------------------------------- /test/mdx.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: mdx 3 | --- 4 | 5 | import Md from "./md.md" 6 | import StepList from "./StepList"; 7 | 8 | A: {props.a} 9 | # mdx 10 | 11 | <> 12 | {1} 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/preact/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-test", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/preact/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | preact@^10.16.0: 6 | version "10.16.0" 7 | resolved "https://registry.yarnpkg.com/preact/-/preact-10.16.0.tgz#68a06d70b191b8a313ea722d61e09c6b2a79a37e" 8 | integrity sha512-XTSj3dJ4roKIC93pald6rWuB2qQJO9gO2iLLyTe87MrjQN+HklueLsmskbywEWqCHlclgz3/M4YLL2iBr9UmMA== 9 | -------------------------------------------------------------------------------- /test/goja/goja_test.go: -------------------------------------------------------------------------------- 1 | package goja 2 | 3 | import ( 4 | "github.com/dop251/goja" 5 | "github.com/zbysir/gojsx/internal/pkg/goja_nodejs/require" 6 | "testing" 7 | ) 8 | 9 | func TestRun(t *testing.T) { 10 | v := goja.New() 11 | require.NewRegistry().Enable(v) 12 | va, err := v.RunString("module.exports = { name: \"gojsx\" };") 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | t.Logf("%+v", va) 18 | } 19 | -------------------------------------------------------------------------------- /test/preact/src/index.tsx: -------------------------------------------------------------------------------- 1 | export default function H({js,children}) { 2 | return 3 | 4 | test 5 | 6 | 7 | {children} 8 | 9 | {/**/} 10 | 11 | 12 | 13 | 14 | } -------------------------------------------------------------------------------- /test/blog/page/BlogDetail.tsx: -------------------------------------------------------------------------------- 1 | import Container from "../component/Container"; 2 | 3 | interface Props { 4 | title: string, 5 | html: string 6 | } 7 | 8 | export default function BlogDetail(props: Props) { 9 | return 10 |
11 |

{props.title}

12 |
13 | {props.html} 14 |
15 |
16 |
17 | 18 | } -------------------------------------------------------------------------------- /test/blog/page/Home.jsx: -------------------------------------------------------------------------------- 1 | import Container from "../component/Container"; 2 | 3 | export default function Home(props) { 4 | return 5 |
6 |

最新 Blogs

7 |
14 |
15 | 16 | } -------------------------------------------------------------------------------- /internal/js/jsx-runtime.js: -------------------------------------------------------------------------------- 1 | export function jsx(nodeName, attributes) { 2 | if (typeof nodeName === 'string') { 3 | return { 4 | nodeName, attributes, 5 | } 6 | } else { 7 | return nodeName(attributes) 8 | } 9 | } 10 | 11 | export function jsxs(nodeName, attributes) { 12 | return jsx(nodeName, attributes) 13 | } 14 | 15 | export function Fragment(args) { 16 | return { 17 | nodeName: "", attributes: args 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/StepList.tsx: -------------------------------------------------------------------------------- 1 | interface Item { 2 | time: string 3 | content: string 4 | } 5 | 6 | interface Props { 7 | items: Item[] 8 | } 9 | 10 | export default ({items}: Props) => { 11 | return
12 | {items.map(i => 13 |
14 |

{i.time}

15 |

{i.content}

16 |
) 17 | } 18 |
19 |
20 | } -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package gojsx 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | // ToKebabCase 用于实现 hyphenateStyleName 9 | // /node_modules/react-dom/cjs/react-dom-server.node.development.js hyphenateStyleName 10 | func TestToKebabCase(t *testing.T) { 11 | cases := map[string]string{ 12 | "fontWidth": "font-width", 13 | "FontWidth": "font-width", 14 | "color": "color", 15 | "Color": "color", 16 | "--color": "--color", 17 | } 18 | 19 | for in, out := range cases { 20 | assert.Equal(t, out, ToKebabCase(in)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/timetrack/timetrack.go: -------------------------------------------------------------------------------- 1 | package timetrack 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync/atomic" 7 | "time" 8 | ) 9 | 10 | type TimeTracker struct { 11 | timerDeep int32 12 | } 13 | 14 | func (b *TimeTracker) Start(span string) func() { 15 | if b == nil { 16 | return func() {} 17 | } 18 | deep := atomic.AddInt32(&b.timerDeep, 1) 19 | n := time.Now() 20 | fmt.Printf("[timer]%s %s start\n", strings.Repeat(" ", int((deep-1)*2)), span) 21 | return func() { 22 | tc := time.Since(n) 23 | fmt.Printf("[timer]%s %s end %v\n", strings.Repeat(" ", int((deep-1)*2)), span, tc) 24 | atomic.AddInt32(&b.timerDeep, -1) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pkg/htmlparser/lex_test.go: -------------------------------------------------------------------------------- 1 | package htmlparser 2 | 3 | import ( 4 | "bytes" 5 | "github.com/tdewolff/parse/v2" 6 | "io" 7 | "testing" 8 | ) 9 | 10 | var JsxDom = []byte(` 11 | {title} 12 | 13 | 14 | 15 | 16 | <> 17 | `) 18 | 19 | func TestName(t *testing.T) { 20 | l := NewLexer(parse.NewInput(bytes.NewBuffer(JsxDom))) 21 | for { 22 | err := l.Err() 23 | if err != nil { 24 | if err == io.EOF { 25 | break 26 | } 27 | break 28 | } 29 | 30 | tt, bs := l.Next() 31 | t.Logf("%+v %s", tt, bs) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/App.jsx: -------------------------------------------------------------------------------- 1 | 2 | export default function App(props) { 3 | const Form = require("./Form").default; 4 | 5 | return
6 | a /2 7 | { 8 |
f {props.li ? (<> 9 |
    10 | { 11 | props.li.map(i => ( 12 |
  • {i}
  • 13 | )) 14 | } 15 |
16 | ) : 'b'}
17 | } 18 | 19 | {`asdfsf"12312`} 20 |

{props.html}

21 |

22 | 23 |
24 | } -------------------------------------------------------------------------------- /test/Index.jsx: -------------------------------------------------------------------------------- 1 | // import App from "./App"; 2 | 3 | export default function Index(props) { 4 | const App = require("./App").default 5 | 6 | return 7 | 8 | 9 | {props.title || 'UnTitled'} 10 | 11 | 12 | 13 |
{}} b={1} c={1.1}>
14 | 17 | 18 | 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /test/preact/src/page/index.tsx: -------------------------------------------------------------------------------- 1 | import {useState} from 'preact/hooks'; 2 | import Nav from "./nav.tsx"; 3 | 4 | function List({items}) { 5 | return
6 | { 7 | items.map(item =>
{item}
) 8 | } 9 |
10 | } 11 | const itemsd = [1, 2, 3] 12 | for (let i = 0; i < 10000; i++) { 13 | itemsd.push(i) 14 | } 15 | // itemsd.sort(function() { 16 | // return (0.5-Math.random()); 17 | // }); 18 | export default function Root() { 19 | const [count, setCount] = useState(1) 20 | 21 | const [items, setItems] = useState(itemsd) 22 | 23 | return
24 |
37 | } 38 | -------------------------------------------------------------------------------- /test/preact/src/page/about.tsx: -------------------------------------------------------------------------------- 1 | import {useState} from 'preact/hooks'; 2 | import Nav from "./nav.tsx"; 3 | 4 | function List({items}) { 5 | return
6 | { 7 | items.map(item =>
{item}
) 8 | } 9 |
10 | } 11 | const itemsd = [1, 2, 3] 12 | for (let i = 0; i < 10; i++) { 13 | itemsd.push(i) 14 | } 15 | // itemsd.sort(function() { 16 | // return (0.5-Math.random()); 17 | // }); 18 | 19 | 20 | export default function Root() { 21 | const [count, setCount] = useState(1) 22 | 23 | const [items, setItems] = useState(itemsd) 24 | 25 | return
26 | 27 |
40 | } 41 | -------------------------------------------------------------------------------- /tpool.go: -------------------------------------------------------------------------------- 1 | package gojsx 2 | 3 | import ( 4 | "context" 5 | pool "github.com/jolestar/go-commons-pool/v2" 6 | ) 7 | 8 | type tPool[T any] struct { 9 | op *pool.ObjectPool 10 | } 11 | 12 | func newTPool[T any](maxTotal int, fun func() T) *tPool[T] { 13 | factory := pool.NewPooledObjectFactorySimple( 14 | func(context.Context) (interface{}, error) { 15 | return fun(), nil 16 | }) 17 | ctx := context.Background() 18 | p := pool.NewObjectPool(ctx, factory, &pool.ObjectPoolConfig{ 19 | MaxIdle: -1, 20 | MaxTotal: maxTotal, 21 | BlockWhenExhausted: true, 22 | }) 23 | 24 | return &tPool[T]{ 25 | op: p, 26 | } 27 | } 28 | 29 | func (p *tPool[T]) Get() (t T, err error) { 30 | o, err := p.op.BorrowObject(context.Background()) 31 | if err != nil { 32 | return 33 | } 34 | 35 | return o.(T), nil 36 | } 37 | 38 | func (p *tPool[T]) Put(t T) error { 39 | err := p.op.ReturnObject(context.Background(), t) 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zbysir/gojsx 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 7 | github.com/evanw/esbuild v0.20.2 8 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible 9 | github.com/hashicorp/golang-lru/v2 v2.0.1 10 | github.com/jolestar/go-commons-pool/v2 v2.1.2 11 | github.com/stoewer/go-strcase v1.2.0 12 | github.com/stretchr/testify v1.8.1 13 | github.com/tdewolff/parse/v2 v2.6.5 14 | github.com/yuin/goldmark v1.5.3 15 | github.com/yuin/goldmark-meta v1.1.0 16 | go.abhg.dev/goldmark/mermaid v0.4.0 17 | rogchap.com/v8go v0.9.0 18 | ) 19 | 20 | require ( 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/dlclark/regexp2 v1.7.0 // indirect 23 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect 24 | github.com/pmezard/go-difflib v1.0.0 // indirect 25 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect 26 | golang.org/x/text v0.3.8 // indirect 27 | gopkg.in/yaml.v2 v2.4.0 // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /internal/pkg/goja_nodejs/console/console.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/dop251/goja" 7 | ) 8 | 9 | type Printer interface { 10 | Log(string) 11 | Warn(string) 12 | Error(string) 13 | } 14 | 15 | type PrinterFunc func(s string) 16 | 17 | func (p PrinterFunc) Log(s string) { p(s) } 18 | 19 | func (p PrinterFunc) Warn(s string) { p(s) } 20 | 21 | func (p PrinterFunc) Error(s string) { p(s) } 22 | 23 | var defaultPrinter Printer = PrinterFunc(func(s string) { log.Print(s) }) 24 | 25 | func logx(rt *goja.Runtime, p func(string)) func(goja.FunctionCall) goja.Value { 26 | ut := Util{rt} 27 | return func(call goja.FunctionCall) goja.Value { 28 | p(ut.Js_format(call).String()) 29 | return nil 30 | } 31 | } 32 | 33 | func Enable(runtime *goja.Runtime, printer Printer) { 34 | if printer == nil { 35 | printer = defaultPrinter 36 | } 37 | runtime.Set("console", map[string]interface{}{ 38 | "log": logx(runtime, printer.Log), 39 | "error": logx(runtime, printer.Error), 40 | "warn": logx(runtime, printer.Warn), 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /test/blog/Index.tsx: -------------------------------------------------------------------------------- 1 | import Header from "./Header"; 2 | import Home from "./page/Home"; 3 | import BlogDetail from "./page/BlogDetail"; 4 | 5 | interface Props { 6 | page: 'home' | 'blog-detail' 7 | title: string 8 | pageData: any 9 | me: string 10 | time: string 11 | } 12 | 13 | export default function Index(props: Props) { 14 | return 15 | 16 | 17 | {props.title || 'UnTitled'} 18 | 19 | 20 | 21 |
22 | { 23 | (function () { 24 | switch (props.page) { 25 | case 'home': 26 | return 27 | case 'blog-detail': 28 | return 29 | } 30 | return props.page 31 | })() 32 | } 33 |
34 | {props.time} 35 |
36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package gojsx 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | ) 7 | 8 | // ToKebabCase converts a CamelCase string to a hyphen-separated lowercase string. 9 | // 用于实现 10 | // /node_modules/react-dom/cjs/react-dom-server.node.development.js hyphenateStyleName 11 | // github.com/gobeam/stringy 和 github.com/stoewer/go-strcase 在处理 “--color“ 都有问题。 12 | func ToKebabCase(s string) string { 13 | var result strings.Builder 14 | 15 | for i, r := range s { 16 | // 检查是否是大写字母 17 | if unicode.IsUpper(r) || unicode.IsDigit(r) { 18 | // 如果不是第一个字符,则在前面添加连字符 19 | if i > 0 { 20 | // 检查前一个字符是否是小写字母或数字 21 | if unicode.IsLower(rune(s[i-1])) || !unicode.IsDigit(rune(s[i-1])) { 22 | result.WriteRune('-') 23 | } else if i > 1 && unicode.IsUpper(rune(s[i-1])) { 24 | // 处理如 "XMLHttpRequest" 这样的情况 25 | if unicode.IsLower(rune(s[i-2])) { 26 | result.WriteRune('-') 27 | } 28 | } 29 | } 30 | // 将当前字符转换为小写并写入结果 31 | result.WriteRune(unicode.ToLower(r)) 32 | } else { 33 | // 直接写入小写字符 34 | result.WriteRune(r) 35 | } 36 | } 37 | return result.String() 38 | } 39 | -------------------------------------------------------------------------------- /goja_fieldmapping.go: -------------------------------------------------------------------------------- 1 | package gojsx 2 | 3 | import ( 4 | "github.com/dop251/goja" 5 | "github.com/dop251/goja/parser" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | type tagFieldNameMapper struct { 11 | tagName string 12 | uncapMethods bool 13 | fallbackToUncap bool // If the tag is not found, use lowercase initials. 14 | } 15 | 16 | func (tfm tagFieldNameMapper) FieldName(_ reflect.Type, f reflect.StructField) string { 17 | tag := f.Tag.Get(tfm.tagName) 18 | if idx := strings.IndexByte(tag, ','); idx != -1 { 19 | tag = tag[:idx] 20 | } 21 | if parser.IsIdentifier(tag) { 22 | return tag 23 | } 24 | if tfm.fallbackToUncap { 25 | return uncapitalize(f.Name) 26 | } 27 | return "" 28 | } 29 | 30 | func uncapitalize(s string) string { 31 | return strings.ToLower(s[0:1]) + s[1:] 32 | } 33 | 34 | func (tfm tagFieldNameMapper) MethodName(_ reflect.Type, m reflect.Method) string { 35 | if tfm.uncapMethods { 36 | return uncapitalize(m.Name) 37 | } 38 | return m.Name 39 | } 40 | 41 | func TagFieldNameMapper(tagName string, uncapMethods bool, fallbackToUncap bool) goja.FieldNameMapper { 42 | return tagFieldNameMapper{tagName, uncapMethods, fallbackToUncap} 43 | } 44 | -------------------------------------------------------------------------------- /pkg/mdx/jsxparser.go: -------------------------------------------------------------------------------- 1 | package mdx 2 | 3 | import ( 4 | "bytes" 5 | "github.com/tdewolff/parse/v2" 6 | "github.com/zbysir/gojsx/pkg/htmlparser" 7 | "io" 8 | ) 9 | 10 | func parseTagToClose(buf *bytes.Buffer) (start, end int, ok bool, err error) { 11 | input := parse.NewInput(buf) 12 | 13 | l := htmlparser.NewLexer(input) 14 | 15 | nesting := 0 16 | var currTag []byte 17 | var matchTag []byte 18 | pos := 0 19 | 20 | for end == 0 { 21 | err := l.Err() 22 | if err != nil { 23 | if err == io.EOF { 24 | break 25 | } 26 | 27 | return 0, 0, false, err 28 | } 29 | 30 | tp, bs := l.Next() 31 | 32 | //log.Printf("parseTagToClose: %s %s", tp, bs) 33 | 34 | begin := pos 35 | pos += len(bs) 36 | switch tp { 37 | case htmlparser.StartTagToken: 38 | currTag = bs[1:] 39 | if matchTag == nil { 40 | matchTag = bs[1:] 41 | nesting += 1 42 | start = begin 43 | } else if bytes.Equal(matchTag, bs[1:]) { 44 | nesting += 1 45 | } 46 | case htmlparser.StartTagVoidToken: 47 | if bytes.Equal(matchTag, currTag) { 48 | nesting -= 1 49 | if nesting == 0 { 50 | end = pos 51 | break 52 | } 53 | } 54 | case htmlparser.EndTagToken: 55 | if bytes.Equal(matchTag, bs[2:len(bs)-1]) { 56 | nesting -= 1 57 | if nesting == 0 { 58 | end = pos 59 | break 60 | } 61 | } 62 | } 63 | } 64 | if end != 0 { 65 | return start, end, true, nil 66 | } 67 | 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /goja_exception_test.go: -------------------------------------------------------------------------------- 1 | package gojsx 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestEx(t *testing.T) { 9 | cases := []struct { 10 | In string 11 | Out string 12 | }{ 13 | { 14 | In: `GoError: Invalid module: 'test/App2' 15 | at github.com/zbysir/gojsx/internal/pkg/goja_nodejs/require.(*RequireModule).require-fm (native) 16 | at test/Index.jsx:1:16(55) 17 | at github.com/zbysir/gojsx/internal/pkg/goja_nodejs/require.(*RequireModule).require-fm (native) 18 | at root.js:1:1(1)`, 19 | Out: `GoError: Invalid module: 'test/App2' 20 | at test/Index.jsx:1:16 21 | at root.js:1:1`, 22 | }, 23 | { 24 | In: `ReferenceError: i is not defined 25 | at Index (test/Index.jsx:14:23(64)) 26 | at root.js:1:32(5)`, 27 | Out: `ReferenceError: i is not defined 28 | at Index (test/Index.jsx:14:23) 29 | at root.js:1:32`, 30 | }, 31 | { 32 | In: `GoError: load file (test/Index.jsx) error :test/Index.jsx: (4:24) 33 | export default function 1 Index(props) { 34 | ^ Expected "(" but found "1" 35 | at github.com/zbysir/gojsx/internal/pkg/goja_nodejs/require.(*RequireModule).require-fm (native)`, 36 | Out: `GoError: load file (test/Index.jsx) error :test/Index.jsx: (4:24) 37 | export default function 1 Index(props) { 38 | ^ Expected "(" but found "1"`, 39 | }, 40 | } 41 | 42 | for _, c := range cases { 43 | assert.Equal(t, c.Out, parseException(c.In).Error()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /goja_exception.go: -------------------------------------------------------------------------------- 1 | package gojsx 2 | 3 | import ( 4 | "github.com/dop251/goja" 5 | "strings" 6 | ) 7 | 8 | type Exception struct { 9 | Text string 10 | Stacks []string 11 | } 12 | 13 | func (e *Exception) Error() string { 14 | var b strings.Builder 15 | b.WriteString(e.Text) 16 | for _, s := range e.Stacks { 17 | // skip golang require function Stack 18 | if strings.HasSuffix(s, "(native)") { 19 | continue 20 | } 21 | 22 | // rm pt 23 | if strings.HasSuffix(s, "))") { 24 | i := strings.LastIndex(s, "(") 25 | s = s[:i] + s[len(s)-1:] 26 | } else if strings.HasSuffix(s, ")") { 27 | i := strings.LastIndex(s, "(") 28 | s = s[:i] 29 | } 30 | 31 | b.WriteString("\n\t") 32 | b.WriteString(strings.TrimSpace(s)) 33 | } 34 | 35 | return b.String() 36 | } 37 | 38 | func parseException(s string) error { 39 | ss := strings.Split(s, "\n") 40 | if len(ss) == 0 { 41 | return nil 42 | } 43 | var errMsg strings.Builder 44 | stack := make([]string, 0) 45 | for _, s := range ss { 46 | if s == "" { 47 | continue 48 | } else if strings.HasPrefix(strings.TrimSpace(s), "at") { 49 | stack = append(stack, s) 50 | } else { 51 | if errMsg.Len() != 0 { 52 | errMsg.WriteByte('\n') 53 | } 54 | errMsg.WriteString(s) 55 | } 56 | } 57 | return &Exception{ 58 | Text: errMsg.String(), 59 | Stacks: stack, 60 | } 61 | } 62 | 63 | // PrettifyException make goja exceptions look more prettify. 64 | func PrettifyException(err error) error { 65 | // return err 66 | if ex, ok := err.(*goja.Exception); ok { 67 | return parseException(ex.String()) 68 | } 69 | 70 | return err 71 | } 72 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package gojsx 2 | 3 | import "testing" 4 | 5 | // 优化前 6 | // noCache 1,919,804 ns/op 7 | // 优化 编译缓存 8 | // 354,871 ns/op 9 | func BenchmarkName(b *testing.B) { 10 | j, err := NewJsx(Option{}) 11 | //j.debug = true 12 | if err != nil { 13 | b.Fatal(err) 14 | } 15 | b.ResetTimer() 16 | j.RegisterModule("@bysir/hollow", map[string]interface{}{ 17 | "getConfig": func() interface{} { 18 | return map[string]interface{}{} 19 | }, 20 | "getContents": func() interface{} { 21 | return map[string]interface{}{ 22 | "list": []interface{}{}, 23 | } 24 | }, 25 | }) 26 | 27 | b.Logf("-----begin-----") 28 | for i := 0; i < b.N; i++ { 29 | _, err := j.Exec("./test/Index", WithCache(false), WithAutoExecJsx(map[string]interface{}{})) 30 | if err != nil { 31 | b.Fatal(err) 32 | } 33 | } 34 | } 35 | 36 | func TestOne(t *testing.T) { 37 | j, err := NewJsx(Option{}) 38 | j.debug = true 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | j.RegisterModule("@bysir/hollow", map[string]interface{}{ 44 | "getConfig": func() interface{} { 45 | return map[string]interface{}{} 46 | }, 47 | "getContents": func() interface{} { 48 | return map[string]interface{}{ 49 | "list": []interface{}{}, 50 | } 51 | }, 52 | }) 53 | 54 | t.Logf("----- begin -----") 55 | _, err = j.Exec("./test/Index", WithCache(false)) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | t.Logf("----- second time -----") 60 | e, err := j.Exec("./test/Index", WithCache(false), WithAutoExecJsx(map[string]interface{}{})) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | t.Logf("%+v", e.Default.(VDom)) 66 | } 67 | -------------------------------------------------------------------------------- /test/sourcemap/sourcemap_test.go: -------------------------------------------------------------------------------- 1 | package sourcemap 2 | 3 | import ( 4 | "bytes" 5 | sourcemap2 "github.com/go-sourcemap/sourcemap" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestSourceMap(t *testing.T) { 11 | m := Map{ 12 | Version: 3, 13 | File: "a.md", 14 | SourceRoot: "", 15 | Sources: []string{"a.md"}, 16 | SourcesContent: []string{`# h1 17 | hihi {a} 18 | `}, 19 | Names: nil, 20 | Mappings: "", 21 | } 22 | 23 | // 0 开始计数, [0, 2] => [0, 6], [1, 0] => [0, 17], [1, 6] => [0, 22] 24 | // https://www.murzwin.com/base64vlq.html 25 | // MAAE,WACF,KAAM 26 | // 27 | // # h1 28 | // ^2 29 | // hihi {a} 30 | // ^0 ^6 31 | // 32 | // 33 | // <>

h1

hih {a}

34 | // ^6 ^17 ^22 35 | // 36 | 37 | m.AddMapping(&Mapping{ 38 | GeneratedLine: 0, 39 | GeneratedColumn: 6, 40 | OriginalFile: "a.md", 41 | OriginalLine: 0, 42 | OriginalColumn: 2, 43 | OriginalName: "", 44 | }) 45 | m.AddMapping(&Mapping{ 46 | GeneratedLine: 0, 47 | GeneratedColumn: 17, 48 | OriginalFile: "a.md", 49 | OriginalLine: 1, 50 | OriginalColumn: 0, 51 | OriginalName: "", 52 | }) 53 | m.AddMapping(&Mapping{ 54 | GeneratedLine: 0, 55 | GeneratedColumn: 22, 56 | OriginalFile: "a.md", 57 | OriginalLine: 1, 58 | OriginalColumn: 6, 59 | OriginalName: "", 60 | }) 61 | 62 | m.EncodeMappings() 63 | sm := bytes.Buffer{} 64 | err := m.WriteTo(&sm) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | c, err := sourcemap2.Parse("./", sm.Bytes()) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | source, name, line, column, ok := c.Source(1, 6) 74 | assert.Equal(t, true, ok) 75 | assert.Equal(t, line, 1) 76 | assert.Equal(t, column, 2) 77 | assert.Equal(t, name, "") 78 | assert.Equal(t, source, "a.md") 79 | } 80 | -------------------------------------------------------------------------------- /internal/pkg/goja_nodejs/console/util.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "bytes" 5 | "github.com/dop251/goja" 6 | ) 7 | 8 | type Util struct { 9 | runtime *goja.Runtime 10 | } 11 | 12 | func (u *Util) format(f rune, val goja.Value, w *bytes.Buffer) bool { 13 | switch f { 14 | case 's': 15 | w.WriteString(val.String()) 16 | case 'd': 17 | w.WriteString(val.ToNumber().String()) 18 | case 'j': 19 | if json, ok := u.runtime.Get("JSON").(*goja.Object); ok { 20 | if stringify, ok := goja.AssertFunction(json.Get("stringify")); ok { 21 | res, err := stringify(json, val) 22 | if err != nil { 23 | panic(err) 24 | } 25 | w.WriteString(res.String()) 26 | } 27 | } 28 | case '%': 29 | w.WriteByte('%') 30 | return false 31 | default: 32 | w.WriteByte('%') 33 | w.WriteRune(f) 34 | return false 35 | } 36 | return true 37 | } 38 | 39 | func (u *Util) Format(b *bytes.Buffer, f string, args ...goja.Value) { 40 | pct := false 41 | argNum := 0 42 | for _, chr := range f { 43 | if pct { 44 | if argNum < len(args) { 45 | if u.format(chr, args[argNum], b) { 46 | argNum++ 47 | } 48 | } else { 49 | b.WriteByte('%') 50 | b.WriteRune(chr) 51 | } 52 | pct = false 53 | } else { 54 | if chr == '%' { 55 | pct = true 56 | } else { 57 | b.WriteRune(chr) 58 | } 59 | } 60 | } 61 | 62 | for _, arg := range args[argNum:] { 63 | b.WriteByte(' ') 64 | b.WriteString(arg.String()) 65 | } 66 | } 67 | 68 | func (u *Util) Js_format(call goja.FunctionCall) goja.Value { 69 | var b bytes.Buffer 70 | var fmt string 71 | 72 | if arg := call.Argument(0); !goja.IsUndefined(arg) { 73 | fmt = arg.String() 74 | } 75 | 76 | var args []goja.Value 77 | if len(call.Arguments) > 0 { 78 | args = call.Arguments[1:] 79 | } 80 | u.Format(&b, fmt, args...) 81 | 82 | return u.runtime.ToValue(b.String()) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/mdx/jsxparser_test.go: -------------------------------------------------------------------------------- 1 | package mdx 2 | 3 | import ( 4 | "bytes" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestParseToClose(t *testing.T) { 10 | cases := []struct { 11 | Name string 12 | In string 13 | Out string 14 | }{ 15 | { 16 | Name: "Base", 17 | In: ` 18 | 19 | 20 | 21 | 133 22 | 23 | `, 24 | Out: "", 25 | }, 26 | { 27 | Name: "SelfClose", 28 | In: ` 29 | 30 | 333 31 | 32 | `, 33 | Out: "", 34 | }, 35 | { 36 | Name: "Lines", 37 | In: ` 38 | 39 | 333 40 | 41 | 42 | `, 43 | Out: "333\n\n", 44 | }, 45 | { 46 | Name: "Nesting", 47 | In: ` 48 | 49 | 333 50 | 51 | 52 | 53 | `, 54 | Out: "333\n\n\n", 55 | }, 56 | { 57 | Name: "Pure", 58 | In: ` 59 | `, 60 | Out: "", 61 | }, 62 | { 63 | Name: "OneLetter", 64 | In: ``, 65 | Out: "", 66 | }, 67 | { 68 | Name: "fragment", 69 | In: `<> 70 |

{1}

71 | `, 72 | Out: "<>\n

{1}

\n", 73 | }, 74 | { 75 | Name: "blankLine", 76 | In: ` 77 | 78 | <> 79 |
80 | `, 81 | Out: "<>\n
\n", 82 | }, 83 | } 84 | 85 | for _, c := range cases { 86 | t.Run(c.Name, func(t *testing.T) { 87 | buf := bytes.NewBuffer([]byte(c.In)) 88 | s, e, ok, err := parseTagToClose(buf) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | if !ok { 93 | panic("not ok") 94 | } 95 | 96 | assert.Equal(t, c.Out, string([]byte(c.In)[s:e])) 97 | }) 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /pkg/htmlparser/hash.go: -------------------------------------------------------------------------------- 1 | package htmlparser 2 | 3 | // generated by hasher -type=Hash -file=hash.go; DO NOT EDIT, except for adding more constants to the list and rerun go generate 4 | 5 | // uses github.com/tdewolff/hasher 6 | //go:generate hasher -type=Hash -file=hash.go 7 | 8 | // Hash defines perfect hashes for a predefined list of strings 9 | type Hash uint32 10 | 11 | // Unique hash definitions to be used instead of strings 12 | const ( 13 | Iframe Hash = 0x6 // iframe 14 | Math Hash = 0x604 // math 15 | Plaintext Hash = 0x1e09 // plaintext 16 | Script Hash = 0xa06 // script 17 | Style Hash = 0x1405 // style 18 | Svg Hash = 0x1903 // svg 19 | Textarea Hash = 0x2308 // textarea 20 | Title Hash = 0xf05 // title 21 | Xmp Hash = 0x1c03 // xmp 22 | ) 23 | 24 | // String returns the hash' name. 25 | func (i Hash) String() string { 26 | start := uint32(i >> 8) 27 | n := uint32(i & 0xff) 28 | if start+n > uint32(len(_Hash_text)) { 29 | return "" 30 | } 31 | return _Hash_text[start : start+n] 32 | } 33 | 34 | // ToHash returns the hash whose name is s. It returns zero if there is no 35 | // such hash. It is case sensitive. 36 | func ToHash(s []byte) Hash { 37 | if len(s) == 0 || len(s) > _Hash_maxLen { 38 | return 0 39 | } 40 | h := uint32(_Hash_hash0) 41 | for i := 0; i < len(s); i++ { 42 | h ^= uint32(s[i]) 43 | h *= 16777619 44 | } 45 | if i := _Hash_table[h&uint32(len(_Hash_table)-1)]; int(i&0xff) == len(s) { 46 | t := _Hash_text[i>>8 : i>>8+i&0xff] 47 | for i := 0; i < len(s); i++ { 48 | if t[i] != s[i] { 49 | goto NEXT 50 | } 51 | } 52 | return i 53 | } 54 | NEXT: 55 | if i := _Hash_table[(h>>16)&uint32(len(_Hash_table)-1)]; int(i&0xff) == len(s) { 56 | t := _Hash_text[i>>8 : i>>8+i&0xff] 57 | for i := 0; i < len(s); i++ { 58 | if t[i] != s[i] { 59 | return 0 60 | } 61 | } 62 | return i 63 | } 64 | return 0 65 | } 66 | 67 | const _Hash_hash0 = 0x9acb0442 68 | const _Hash_maxLen = 9 69 | const _Hash_text = "iframemathscriptitlestylesvgxmplaintextarea" 70 | 71 | var _Hash_table = [1 << 4]Hash{ 72 | 0x0: 0x2308, // textarea 73 | 0x2: 0x6, // iframe 74 | 0x4: 0xf05, // title 75 | 0x5: 0x1e09, // plaintext 76 | 0x7: 0x1405, // style 77 | 0x8: 0x604, // math 78 | 0x9: 0xa06, // script 79 | 0xa: 0x1903, // svg 80 | 0xb: 0x1c03, // xmp 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gojsx 2 | 3 | Render Jsx / Tsx / MD / MDX by Golang. 4 | 5 | 使用 Go 渲染 Jsx、Tsx、MD、MDX。 6 | 7 | Features: 8 | - Pure Golang, fast and simple 9 | 10 | ## Install 11 | 12 | ```shell 13 | go get github.com/zbysir/gojsx 14 | ``` 15 | 16 | ## Example 17 | 18 | ### TSX 19 | ```jsx 20 | import App from "./App"; 21 | 22 | export default function Index(props) { 23 | return 24 | 25 | 26 | Title 27 | 28 | 29 | 30 | 31 | 32 | 33 | } 34 | ``` 35 | 36 | ### Mdx 37 | ```mdx 38 | --- 39 | title: "Hi" 40 | --- 41 | 42 | import Footer from "./footer.md" 43 | 44 | # {meta.title} 45 | 46 |