├── 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
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 |
17 | }
18 |
19 |
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 |
19 |
24 |
28 |
29 |
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 |
25 |
{
26 | console.log('click');
27 | items.sort(function() {
28 | return (0.5-Math.random());
29 | });
30 | setItems(items)
31 |
32 | setCount(count + 1)
33 | }}>Click Me!
34 | {count}
35 |
36 |
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 |
28 |
{
29 | console.log('click');
30 | items.sort(function() {
31 | return (0.5-Math.random());
32 | });
33 | setItems(items)
34 |
35 | setCount(count + 1)
36 | }}>About
37 | {count}
38 |
39 |
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 |
47 |
48 | ```
49 |
50 | ### Render File
51 |
52 | Then use `gojsx` to render .tsx or .mdx file.
53 |
54 | ```go
55 | package jsx
56 |
57 | func TestJsx(t *testing.T) {
58 | j, err := gojsx.NewJsx(gojsx.Option{})
59 | if err != nil {
60 | t.Fatal(err)
61 | }
62 |
63 | s, err := j.Render("./test/Index.jsx", map[string]interface{}{"li": []int64{1, 2, 3, 4}})
64 | if err != nil {
65 | t.Fatal(err)
66 | }
67 |
68 | t.Logf("%+v", s)
69 | }
70 | ```
71 |
72 | ## Extended syntax
73 | In addition to supporting most of the syntax of jsx, gojsx also supports some special syntax
74 |
75 | ### Render raw html
76 |
77 | Use `{{__dangerousHTML}}` to render raw html without any tag.
78 |
79 | ```jsx
80 | export default function Index(props) {
81 | return <>
82 | {{__dangerousHTML: props.rawHtml}}
83 | >
84 | }
85 | ```
86 |
87 | ## Defects
88 |
89 | ### How to bind event? e.g. onClick
90 | Since the binding event must happen on the browser, and jsx is js code, the browser needs to run the entire jsx component to bind the event correctly,
91 | which requires the introduction of react at the front end, otherwise it is very complicated to implement,
92 | but the use react will cause jsx to no longer be pure jsx, which in turn will cause `gojsx` to become more complicated.
93 |
94 | So `gojsx` can't implement event binding that uses simple react syntax.
95 |
96 | To save the day, you can either write your own js to manipulate the dom (as everyone did in the JQuery days), or use a library like AlpineJs.
97 |
98 | ## Dependents
99 | - [goja](https://github.com/dop251/goja)
100 | - [esbuild](https://github.com/evanw/esbuild)
101 | - [goldmark](github.com/yuin/goldmark)
102 |
--------------------------------------------------------------------------------
/pkg/mdx/testdata/Introduction.md.out.txt:
--------------------------------------------------------------------------------
1 |
2 | This documentation website is a work in progress. The best source of information is still the Yjs README and the yjs-demos repository.
3 |
4 | Yjs is a high-performance CRDT for building collaborative applications that sync automatically.
5 | It exposes its internal CRDT model as shared data types that can be manipulated concurrently. Shared types are similar to common data types like Map and Array. They can be manipulated, fire events when changes happen, and automatically merge without merge conflicts.
6 | Quick Start
7 | This is a working example of how shared types automatically sync. We also have a getting-started guide, API documentation, and lots of live demos with source code.
8 | import * as Y from 'yjs'\n\n// Yjs documents are collections of\n// shared objects that sync automatically.\nconst ydoc = new Y.Doc()\n// Define a shared Y.Map instance\nconst ymap = ydoc.getMap()\nymap.set('keyA', 'valueA')\n\n// Create another Yjs document (simulating a remote user)\n// and create some conflicting changes\nconst ydocRemote = new Y.Doc()\nconst ymapRemote = ydocRemote.getMap()\nymapRemote.set('keyB', 'valueB')\n\n// Merge changes from remote\nconst update = Y.encodeStateAsUpdate(ydocRemote)\nY.applyUpdate(ydoc, update)\n\n// Observe that the changes have merged\nconsole.log(ymap.toJSON()) // => { keyA: 'valueA', keyB: 'valueB' }\n" }}>
9 | Editor Support
10 | Yjs supports several popular text and rich-text editors. We are working with different projects to enable collaboration-support through Yjs.
11 | Network Agnostic 📡
12 | Yjs doesn't make any assumptions about the network technology you are using. As long as all changes eventually arrive, the documents will sync. The order in which document updates are applied doesn't matter.
13 | You can integrate Yjs into your existing communication infrastructure, or use one of the several existing network providers that allow you to jump-start your application backend.
14 | Scaling shared editing backends is not trivial. Most shared editing solutions depend on a single source of truth - a central server - to perform conflict resolution. Yjs doesn't need a central source of truth. This enables you to design the backend using ideas from distributed system architecture. In fact, Yjs can be scaled indefinitely as it is shown in the y-redis section.
15 | Another interesting application for Yjs as a data model for decentralized and Local-First software.
16 |
--------------------------------------------------------------------------------
/pkg/mdx/testdata/Introduction.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Introduction
3 | slug: introduction
4 | desc: Modular building blocks for building collaborative applications like Google Docs and Figma.
5 | sort: 1
6 | ---
7 |
8 | > This documentation website is a work in progress. The best source of information is still the Yjs README and the yjs-demos repository.
9 |
10 | Yjs is a high-performance [CRDT](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) for building collaborative applications that sync automatically.
11 | It exposes its internal CRDT model as shared data types that can be manipulated concurrently. Shared types are similar to common data types like Map and Array. They can be manipulated, fire events when changes happen, and automatically merge without merge conflicts.
12 |
13 | # Quick Start
14 |
15 | This is a working example of how shared types automatically sync. We also have a getting-started guide, API documentation, and lots of live demos with source code.
16 |
17 | ```js
18 | import * as Y from 'yjs'
19 |
20 | // Yjs documents are collections of
21 | // shared objects that sync automatically.
22 | const ydoc = new Y.Doc()
23 | // Define a shared Y.Map instance
24 | const ymap = ydoc.getMap()
25 | ymap.set('keyA', 'valueA')
26 |
27 | // Create another Yjs document (simulating a remote user)
28 | // and create some conflicting changes
29 | const ydocRemote = new Y.Doc()
30 | const ymapRemote = ydocRemote.getMap()
31 | ymapRemote.set('keyB', 'valueB')
32 |
33 | // Merge changes from remote
34 | const update = Y.encodeStateAsUpdate(ydocRemote)
35 | Y.applyUpdate(ydoc, update)
36 |
37 | // Observe that the changes have merged
38 | console.log(ymap.toJSON()) // => { keyA: 'valueA', keyB: 'valueB' }
39 | ```
40 |
41 | # Editor Support
42 |
43 | Yjs supports several popular text and rich-text editors. We are working with different projects to enable collaboration-support through Yjs.
44 |
45 | # Network Agnostic 📡
46 |
47 |
48 | Yjs doesn't make any assumptions about the network technology you are using. As long as all changes eventually arrive, the documents will sync. The order in which document updates are applied doesn't matter.
49 | You can integrate Yjs into your existing communication infrastructure, or use one of the several existing network providers that allow you to jump-start your application backend.
50 | Scaling shared editing backends is not trivial. Most shared editing solutions depend on a single source of truth - a central server - to perform conflict resolution. Yjs doesn't need a central source of truth. This enables you to design the backend using ideas from distributed system architecture. In fact, Yjs can be scaled indefinitely as it is shown in the y-redis section.
51 | Another interesting application for Yjs as a data model for decentralized and Local-First software.
52 |
53 |
--------------------------------------------------------------------------------
/pkg/mdx/mdx_test.go:
--------------------------------------------------------------------------------
1 | package mdx
2 |
3 | import (
4 | "bytes"
5 | "github.com/stretchr/testify/assert"
6 | "github.com/yuin/goldmark"
7 | meta "github.com/yuin/goldmark-meta"
8 | "github.com/yuin/goldmark/extension"
9 | "github.com/yuin/goldmark/parser"
10 | "os"
11 | "testing"
12 | )
13 |
14 | func readFile(name string) string {
15 | bs, err := os.ReadFile(name)
16 | if err != nil {
17 | panic(err)
18 | }
19 | return string(bs)
20 | }
21 |
22 | func TestMdx(t *testing.T) {
23 | cases := []struct {
24 | Name string
25 | In string
26 | Out string
27 | }{
28 | {
29 | Name: "Base",
30 | In: `
31 | import Logo from "./logo"
32 | import Footer from "./footer.md"
33 | const a = "3233"
34 |
35 | {a}
36 |
37 | <>
38 |
39 |
40 |
41 | Hollow
42 |
43 |
44 |
45 |
46 |
47 | >
48 | `,
49 | Out: `{a}
50 | <>
51 |
52 |
53 |
54 | Hollow
55 |
56 |
57 |
58 |
59 |
60 | >`,
61 | },
62 | {
63 | Name: "FULL",
64 | In: readFile("./testdata/fulldemo.md"),
65 | Out: readFile("./testdata/fulldemo.md.out.txt"),
66 | },
67 | {
68 | Name: "D",
69 | In: readFile("./testdata/introduction.md"),
70 | Out: readFile("./testdata/introduction.md.out.txt"),
71 | },
72 |
73 | {
74 | Name: "Inline",
75 | In: `# h1 <> { 1 } > { 2} hh`,
76 | Out: `h1 <> { 1 } > { 2} hh
77 | `,
78 | },
79 | {
80 | Name: "InlineX",
81 | In: `# h1 <> { 1 } > hh {1}`,
83 | Out: `h1 <B
84 | a={1}/> <> { 1 } > hh {1}
85 | `,
86 | },
87 | {
88 | Name: "Code",
89 | In: "```js\n console.log(\"a c\\ \")\n```",
90 | Out: ` console.log("a c\\ ")\n" }}>
91 | `,
92 | },
93 | {
94 | Name: "Custom ID",
95 | In: `# A {id} {#a-A-id}`,
96 | Out: `A {id}
97 | `,
98 | },
99 | }
100 |
101 | opts := []goldmark.Option{
102 | goldmark.WithExtensions(
103 | meta.Meta,
104 | extension.GFM,
105 | NewMdJsx("mdx"),
106 | ),
107 | goldmark.WithParserOptions(
108 | parser.WithAutoHeadingID(),
109 | parser.WithHeadingAttribute(), // handles special case like ### heading ### {#id}
110 | ),
111 | }
112 | for _, c := range cases {
113 | t.Run(c.Name, func(t *testing.T) {
114 | var buf bytes.Buffer
115 | md := goldmark.New(opts...)
116 | err := md.Convert([]byte(c.In), &buf)
117 | if err != nil {
118 | t.Fatal(err)
119 | }
120 |
121 | assert.Equal(t, c.Out, buf.String())
122 | })
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/pkg/mdx/jscodeparser.go:
--------------------------------------------------------------------------------
1 | package mdx
2 |
3 | import (
4 | "bytes"
5 | "github.com/yuin/goldmark/ast"
6 | "github.com/yuin/goldmark/parser"
7 | "github.com/yuin/goldmark/text"
8 | "github.com/yuin/goldmark/util"
9 | "regexp"
10 | )
11 |
12 | type jsCodeParser struct {
13 | }
14 |
15 | // NewJsCodeParser 解析在最开始的 js 代码,支持 import、function、const、let 开头的代码,已空行结束
16 | func NewJsCodeParser() parser.BlockParser {
17 | return &jsCodeParser{}
18 | }
19 |
20 | func (b *jsCodeParser) Trigger() []byte {
21 | return nil
22 | }
23 |
24 | type jsCodeNode struct {
25 | ast.BaseBlock
26 | }
27 |
28 | var jsCodeKind = ast.NewNodeKind("JsCode")
29 |
30 | func (j *jsCodeNode) Kind() ast.NodeKind {
31 | return jsCodeKind
32 | }
33 |
34 | // IsRaw return true 不解析 block 中的内容
35 | func (j *jsCodeNode) IsRaw() bool {
36 | return true
37 | }
38 |
39 | func (j *jsCodeNode) Dump(source []byte, level int) {
40 | ast.DumpHelper(j, source, level, nil, nil)
41 | }
42 |
43 | func (b *jsCodeParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) {
44 | // 只支持放在头部的代码
45 | // parent.Dump(reader.Source(), 1)
46 | // meta 解析过程是 先 open meta,在 open jsCode,然后再 close meta,再删 node,所以 meta 下要有一行空格。
47 | if parent.Type() != ast.TypeDocument || parent.ChildCount() > 1 {
48 | return nil, parser.NoChildren
49 | }
50 |
51 | line, segment := reader.PeekLine()
52 | segment = segment.TrimLeftSpace(reader.Source())
53 | if segment.IsEmpty() {
54 | return nil, parser.NoChildren
55 | }
56 |
57 | if !jsCodeRegexp.Match(line) {
58 | return nil, parser.NoChildren
59 | }
60 |
61 | node := &jsCodeNode{}
62 | node.Lines().Append(segment)
63 | reader.Advance(segment.Len() - 1)
64 | return node, parser.NoChildren
65 | }
66 |
67 | var jsCodeRegexp = regexp.MustCompile("^(import )|(const )|(let )|(export )|(function )")
68 |
69 | func (b *jsCodeParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State {
70 | line, segment := reader.PeekLine()
71 | if util.IsBlank(line) {
72 | return parser.Close | parser.NoChildren
73 | }
74 |
75 | node.Lines().Append(segment)
76 | reader.Advance(segment.Len() - 1)
77 | return parser.Continue | parser.NoChildren
78 | }
79 |
80 | var jsCodeKey = parser.NewContextKey()
81 |
82 | func (b *jsCodeParser) Close(node ast.Node, reader text.Reader, pc parser.Context) {
83 | lines := node.Lines()
84 | before := GetJsCode(pc)
85 |
86 | var buf bytes.Buffer
87 | if before != "" {
88 | buf.WriteString(before)
89 | buf.WriteString(";\n")
90 | }
91 |
92 | for i := 0; i < lines.Len(); i++ {
93 | segment := lines.At(i)
94 | buf.Write(segment.Value(reader.Source()))
95 | }
96 |
97 | pc.Set(jsCodeKey, buf.String())
98 |
99 | // remove self
100 | node.Parent().RemoveChild(node.Parent(), node)
101 | }
102 |
103 | func (b *jsCodeParser) CanInterruptParagraph() bool {
104 | return false
105 | }
106 |
107 | func (b *jsCodeParser) CanAcceptIndentedLine() bool {
108 | return false
109 | }
110 |
111 | func GetJsCode(pc parser.Context) string {
112 | v := pc.Get(jsCodeKey)
113 | if v == nil {
114 | return ""
115 | }
116 | d := v.(string)
117 | return d
118 | }
119 |
--------------------------------------------------------------------------------
/test/v8bind/test.js:
--------------------------------------------------------------------------------
1 | const Frame = ({ node }) => {
2 | return (
3 |
7 | {node.props.link ? (
8 |
16 | {node.children.map((ch) => (
17 |
18 | ))}
19 |
20 | ) : (
21 | <>
22 | {node.props.fillType === 'image' && node.props.img.src ? (
23 |
24 |
34 |
35 | ) : null}
36 | {node.children?.map((ch) => (
37 |
38 | ))}
39 | >
40 | )}
41 |
42 | );
43 | };
44 |
45 |
46 | const Breakpoint = ({ node }) => {
47 | return (
48 |
49 | {node.children.map((ch) => (
50 |
51 | ))}
52 |
53 | );
54 | };
55 |
56 | function Text({ node }) {
57 | return
;
58 | }
59 |
60 | function Comp({ type, node, parentProps }) {
61 | switch (type) {
62 | case 'frame':
63 | return ;
64 | case 'breakpoint':
65 | return ;
66 | case 'text':
67 | return ;
68 | }
69 |
70 |
71 | return 'uncase type - ' + type;
72 | }
73 |
74 | export default function Index({ root, lang, title, style, hydrate_json }) {
75 |
76 | return
77 |
78 |
79 |
80 | {title}
81 |
85 |
86 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | ;
103 |
104 |
105 |
106 |
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/pkg/mdx/testdata/fulldemo.md:
--------------------------------------------------------------------------------
1 |
2 | # Markdown Demo
3 |
4 | - - -
5 |
6 | ## 一、标题
7 |
8 | ### 1. 使用 `#` 表示标题,其中 `#` 号必须在行首,例如:
9 |
10 | # 一号标题
11 | ## 二号标题
12 | ### 三号标题
13 | #### 四号标题
14 | ##### 五号标题
15 | ###### 六号标题
16 |
17 | ### 2. 使用 `===` 或者 `---` 表示,例如:
18 |
19 | 一级标题
20 | ===
21 |
22 | 二级标题
23 | ---
24 |
25 | #### **扩展:如何换行?**
26 | 一般使用 **两个空格** 加 **回车** 换行,不过一些 IDE 也可以直接使用回车换行。
27 |
28 |
29 | ## 二、分割线
30 |
31 | 使用三个或以上的 `-` 或者 `*` 表示,且这一行只有符号,**注意不要被识别为二级标题即可**,例如中间或者前面可以加空格
32 |
33 | - - -
34 |
35 | * * *
36 |
37 |
38 | ## 三、斜体和粗体
39 |
40 | 使用 `*` 和 `**` 分别表示斜体和粗体,例如
41 |
42 | *斜体*
43 | **粗体**
44 | ***又斜又粗***
45 |
46 | #### **扩展:**删除线使用两个 `~` 表示,例如
47 |
48 | ~~我是要删掉的文字~~
49 |
50 | - - -
51 |
52 |
53 | ## 四、超链接和图片
54 |
55 | 超链接和图片的写法类似,图片仅在超链接前多了一个 `!` ,一般是 [文字描述] (链接)
56 | 两种写法,分别是: [第一种写法](https://www.baidu.com/) 和 [第二种写法][1]
57 | 图片的话就比如这样: ![Image][2]
58 |
59 | [1]: https://www.baidu.com/
60 | [2]: https://www.zybuluo.com/static/img/logo.png
61 |
62 | - - -
63 |
64 |
65 | ## 五、无序列表
66 |
67 | 使用 `-`、`+` 和 `*` 表示无序列表,前后留一行空白,可嵌套,例如
68 |
69 | + 一层
70 | - 二层
71 | - 二层
72 | * 三层
73 | + 四层
74 | + 一层
75 |
76 | - - -
77 |
78 |
79 | ## 六、有序列表
80 |
81 | 使用 `1. ` (点号后面有个空格)表示有序列表,可嵌套,例如
82 |
83 | 1. 一层
84 | 1. 二层
85 | 2. 二层
86 | 2. 一层
87 |
88 | - - -
89 |
90 |
91 | ## 七、文字引用
92 |
93 | 使用 `>` 表示,可以有多个 `>`,表示层级更深,例如
94 |
95 | > 第一层
96 | >>第二层
97 | >这样是跳不出去的
98 | >>> 还可以更深
99 |
100 | > 这样就跳出去了
101 |
102 | - - -
103 |
104 |
105 | ## 八、行内代码块
106 |
107 | 其实上面已经用过很多次了,即使用 \` 表示,例如
108 |
109 | `行内代码块`
110 |
111 | ### 扩展:很多字符是需要转义,使用反斜杠 `\` 进行转义
112 |
113 | - - -
114 |
115 |
116 | ## 九、代码块
117 |
118 | 使用四个空格缩进表示代码块,例如
119 |
120 | public class HelloWorld
121 | {
122 | public static void main(String[] args)
123 | {
124 | System.out.println( "Hello, World!" );
125 | }
126 | }
127 |
128 | 一些 IDE 支持行数提示和着色,一般使用三个 \` 表示,例如
129 |
130 | ```
131 | public class HelloWorld
132 | {
133 | public static void main(String[] args)
134 | {
135 | System.out.println( "Hello, World!" );
136 | }
137 | }
138 | ```
139 |
140 | - - -
141 |
142 |
143 | ## 十、表格
144 |
145 | 直接看例子吧,第二行的 `---:` 表示了对齐方式,默认**左对齐**,还有**右对齐**和**居中**
146 |
147 | |商品|数量|单价|
148 | |---|---:|:---:|
149 | |苹果苹果苹果|10|\$1|
150 | |电脑|1|\$1999|
151 |
152 | - - -
153 |
154 |
155 | ## 十一、数学公式
156 |
157 | 使用 `$` 表示,其中一个 \$ 表示在行内,两个 \$ 表示独占一行。
158 | 例如质量守恒公式:$$E=mc^2$$
159 | 支持 **LaTeX** 编辑显示支持,例如:$\sum_{i=1}^n a_i=0$, 访问 [MathJax][2] 参考更多使用方法。
160 |
161 | 推荐一个常用的数学公式在线编译网站: [https://www.codecogs.com/latex/eqneditor.php][3]
162 |
163 | [2]: http://meta.math.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference
164 |
165 | [3]: https://www.codecogs.com/latex/eqneditor.php
166 |
167 | - - -
168 |
169 |
170 | ## 十二、支持HTML标签
171 |
172 | ### 1. 例如想要段落的缩进,可以如下:
173 |
174 | 不断行的空白格 或
175 | 半方大的空白 或
176 | 全方大的空白 或
177 |
178 |
179 | - - -
180 |
181 | ## 十三、其它
182 | 1. markdown 各个 IDE 的使用可能存在大同小异,一般可以参考各个 IDE 的介绍文档
183 | 2. 本文档介绍的内容基本适用于大部分的 IDE
184 | 3. 其它一些类似 **流程图** 之类的功能,需要看 IDE 是否支持。
185 |
186 |
187 | 查看原始数据:[https://gitee.com/afei_/MarkdownDemo/raw/master/README.md](https://gitee.com/afei_/MarkdownDemo/raw/master/README.md)
188 |
189 | 博客:[https://blog.csdn.net/afei__/article/details/80717153](https://blog.csdn.net/afei__/article/details/80717153)
--------------------------------------------------------------------------------
/transform_test.go:
--------------------------------------------------------------------------------
1 | package gojsx
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestTransform(t *testing.T) {
8 | x := NewEsBuildTransform(EsBuildTransformOptions{})
9 | x.debug = true
10 |
11 | t.Run("json", func(t *testing.T) {
12 | b, err := x.Transform("1.json", []byte(`{"a":1}`), TransformerFormatCommonJS)
13 | if err != nil {
14 | t.Fatal(err)
15 | }
16 |
17 | t.Logf("%s", b)
18 |
19 | })
20 |
21 | t.Run("css", func(t *testing.T) {
22 | b, err := x.Transform("1.css", []byte(`.a{color: red}`), TransformerFormatCommonJS)
23 | if err != nil {
24 | t.Fatal(err)
25 | }
26 |
27 | t.Logf("%s", b)
28 | })
29 |
30 | t.Run("tsx", func(t *testing.T) {
31 | b, err := x.Transform("1.tsx", []byte(`import HelloJSX from './index.tsx'; module.exports = `), TransformerFormatIIFE)
32 | if err != nil {
33 | t.Fatal(err)
34 | }
35 |
36 | t.Logf("%s", b)
37 | })
38 |
39 | t.Run("md", func(t *testing.T) {
40 | b, err := x.Transform("1.md", []byte(`
41 | ---
42 | {a: 1}
43 | ---
44 | {ffff: ffdaf}
45 | <>
46 | dfafefdf: fwe :
47 |
48 | f{}fsdfsdfas d{}
49 |
50 | fsd<><@EOI3u4iuO#$U#($U#94u8u8
51 |
52 |
53 | "'""'""
54 | ""
55 | "
56 |
57 | ##¥77&¥&¥&7&&&&4uhefuhwf c$&&$
58 | ;;;
59 | <><<<>>
60 |
61 |
64 |
65 |
66 |
67 | `+"有不闭合的标签,如 ` `"+`
68 |
69 | `+"我们要渲染的模板是这个样子的\n```vue\n\n \n {{msg}} \n
\n \n```"+`
70 | ## h2`), TransformerFormatIIFE)
71 | if err != nil {
72 | t.Fatal(err)
73 | }
74 |
75 | t.Logf("%s", b)
76 | })
77 | t.Run("mdx", func(t *testing.T) {
78 | b, err := x.Transform("1.mdx", []byte(`
79 | ## h2 {1}
80 |
81 | <>
82 | {[].map(i=>(8))}
83 | >
84 | `), TransformerFormatIIFE)
85 | if err != nil {
86 | t.Fatal(err)
87 | }
88 |
89 | t.Logf("%s", b)
90 | })
91 | t.Run("mdx2", func(t *testing.T) {
92 | b, err := x.Transform("1.mdx", []byte(`---
93 | logo: Hollow
94 | ---
95 | import Logo from "./logo"
96 | import Footer from "./footer.md"
97 | const history = [
98 | {
99 | time: "2020.01",
100 | msgs: ["诞生", "hh"],
101 | }
102 | ]
103 |
104 |
105 | `), TransformerFormatIIFE)
106 | if err != nil {
107 | t.Fatal(err)
108 | }
109 |
110 | t.Logf("%s", b)
111 | })
112 | t.Run("json", func(t *testing.T) {
113 | b, err := x.Transform("1.json", []byte(`{"a":"1"}`), TransformerFormatCommonJS)
114 | if err != nil {
115 | t.Fatal(err)
116 | }
117 |
118 | t.Logf("%s", b)
119 | })
120 | t.Run("js", func(t *testing.T) {
121 | b, err := x.Transform("1.js", []byte(`modules.export= {a: 1}`), TransformerFormatCommonJS)
122 | if err != nil {
123 | t.Fatal(err)
124 | }
125 |
126 | t.Logf("%s", b)
127 | })
128 | t.Run("css", func(t *testing.T) {
129 | b, err := x.Transform("1.css", []byte(`body {color: red}`), TransformerFormatCommonJS)
130 | if err != nil {
131 | t.Fatal(err)
132 | }
133 |
134 | t.Logf("%s", b)
135 | })
136 | t.Run("import", func(t *testing.T) {
137 | b, err := x.Transform("1.tsx", []byte(`const Home = import("./page/Home"); export default `), TransformerFormatCommonJS)
138 | if err != nil {
139 | t.Fatal(err)
140 | }
141 |
142 | t.Logf("%s", b)
143 | })
144 | }
145 |
146 | func TestMermaid(t *testing.T) {
147 | x := NewEsBuildTransform(EsBuildTransformOptions{})
148 | x.debug = true
149 |
150 | b, err := x.Transform("1.md", []byte("```mermaid\ngraph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;\n```"), TransformerFormatIIFE)
151 | if err != nil {
152 | t.Fatal(err)
153 | }
154 |
155 | t.Logf("%s", b)
156 | }
157 |
--------------------------------------------------------------------------------
/pkg/html2jsx/html2jsx_test.go:
--------------------------------------------------------------------------------
1 | package html2jsx
2 |
3 | import (
4 | "bytes"
5 | "github.com/stretchr/testify/assert"
6 | "testing"
7 | )
8 |
9 | func TestParser(t *testing.T) {
10 | cases := []struct {
11 | In string
12 | Out string
13 | Name string
14 | }{
15 | {
16 | In: `
17 | {a: 1}
18 |
19 | {ffff: ffdaf}
20 | <>
21 | dfafefdf: fwe :
22 | f{}fsdfsdfas d{}
23 | fsd<><@EOI3u4iuO#$U#($U#94u8u8
24 |
25 |
26 | "'""'""
27 | ""
28 | "
29 |
30 | ##¥77&¥&¥&7&&&&4uhefuhwf c$&&$
31 | ;;;
32 | <><<<>>
33 |
34 | 有不闭合的标签,如 <meta charset="UTF-8">
35 | 我们要渲染的模板是这个样子的
36 | <template>
37 | <div>
38 | <span class="bg-gray" :class="cus_class" :style="{'font-size': fontSize+'px'}"> {{msg}} </span>
39 | </div>
40 | </template>
41 |
42 | h2 `,
43 | Out: `
44 | {a: 1}
45 |
46 | {ffff: ffdaf}
47 | <>
48 | dfafefdf: fwe :
49 | f{}fsdfsdfas d{}
50 | fsd<><@EOI3u4iuO#$U#($U#94u8u8
51 | <?fdf>
52 |
53 | "'""'""
54 | ""
55 | "
56 |
57 | ##¥77&¥&¥&7&&&&4uhefuhwf c$&&$
58 | ;;;
59 | <><<<><?>
60 |
61 | 有不闭合的标签,如 <meta charset="UTF-8">
62 | 我们要渲染的模板是这个样子的
63 | <template>\n <div>\n <span class="bg-gray" :class="cus_class" :style="{'font-size': fontSize+'px'}"> {{msg}} </span>\n </div>\n </template>\n " }}>
64 | h2 `,
65 | Name: "",
66 | },
67 | {
68 | In: `<template>
69 | <div>
70 | <span class="bg-gray" :class="cus_class" :style="{'font-size': fontSize+'px'}"> {{msg}} </span>
71 | </div>
72 | </template>
73 | `,
74 | Out: `<template>\n <div>\n <span class="bg-gray" :class="cus_class" :style="{'font-size': fontSize+'px'}"> {{msg}} </span>\n </div>\n </template>\n " }}> `,
75 | Name: "Code",
76 | },
77 | {
78 | In: ` `,
79 | Out: ` `,
80 | Name: "Jsx",
81 | },
82 | {
83 | In: ` `,
84 | Out: ` `,
85 | Name: "pre",
86 | },
87 | {
88 | In: `<>> >`,
89 | Out: `<></> </>`,
90 | Name: "empty",
91 | },
92 | }
93 |
94 | for _, c := range cases {
95 | t.Run(c.Name, func(t *testing.T) {
96 | cover := ctx{debug: true}
97 | var out bytes.Buffer
98 | err := cover.Covert(bytes.NewBufferString(c.In), &out, false)
99 | if err != nil {
100 | t.Fatal(err)
101 | }
102 |
103 | assert.Equal(t, c.Out, out.String())
104 | })
105 | }
106 |
107 | }
108 |
--------------------------------------------------------------------------------
/pkg/htmlparser/util.go:
--------------------------------------------------------------------------------
1 | package htmlparser
2 |
3 | var (
4 | singleQuoteEntityBytes = []byte("'")
5 | doubleQuoteEntityBytes = []byte(""")
6 | )
7 |
8 | // EscapeAttrVal returns the escaped attribute value bytes with quotes. Either single or double quotes are used, whichever is shorter. If there are no quotes present in the value and the value is in HTML (not XML), it will return the value without quotes.
9 | func EscapeAttrVal(buf *[]byte, b []byte, origQuote byte, mustQuote, isXML bool) []byte {
10 | singles := 0
11 | doubles := 0
12 | unquoted := true
13 | for _, c := range b {
14 | if charTable[c] {
15 | unquoted = false
16 | if c == '"' {
17 | doubles++
18 | } else if c == '\'' {
19 | singles++
20 | }
21 | }
22 | }
23 | if unquoted && (!mustQuote || origQuote == 0) && !isXML {
24 | return b
25 | } else if singles == 0 && origQuote == '\'' && !isXML || doubles == 0 && origQuote == '"' {
26 | if len(b)+2 > cap(*buf) {
27 | *buf = make([]byte, 0, len(b)+2)
28 | }
29 | t := (*buf)[:len(b)+2]
30 | t[0] = origQuote
31 | copy(t[1:], b)
32 | t[1+len(b)] = origQuote
33 | return t
34 | }
35 |
36 | n := len(b) + 2
37 | var quote byte
38 | var escapedQuote []byte
39 | if singles >= doubles || isXML {
40 | n += doubles * 4
41 | quote = '"'
42 | escapedQuote = doubleQuoteEntityBytes
43 | if singles == doubles && origQuote == '\'' && !isXML {
44 | quote = '\''
45 | escapedQuote = singleQuoteEntityBytes
46 | }
47 | } else {
48 | n += singles * 4
49 | quote = '\''
50 | escapedQuote = singleQuoteEntityBytes
51 | }
52 | if n > cap(*buf) {
53 | *buf = make([]byte, 0, n) // maximum size, not actual size
54 | }
55 | t := (*buf)[:n] // maximum size, not actual size
56 | t[0] = quote
57 | j := 1
58 | start := 0
59 | for i, c := range b {
60 | if c == quote {
61 | j += copy(t[j:], b[start:i])
62 | j += copy(t[j:], escapedQuote)
63 | start = i + 1
64 | }
65 | }
66 | j += copy(t[j:], b[start:])
67 | t[j] = quote
68 | return t[:j+1]
69 | }
70 |
71 | var charTable = [256]bool{
72 | // ASCII
73 | false, false, false, false, false, false, false, false,
74 | false, true, true, false, true, true, false, false, // tab, line feed, form feed, carriage return
75 | false, false, false, false, false, false, false, false,
76 | false, false, false, false, false, false, false, false,
77 |
78 | true, false, true, false, false, false, false, true, // space, "), '
79 | false, false, false, false, false, false, false, false,
80 | false, false, false, false, false, false, false, false,
81 | false, false, false, false, true, true, true, false, // <, =, >
82 |
83 | false, false, false, false, false, false, false, false,
84 | false, false, false, false, false, false, false, false,
85 | false, false, false, false, false, false, false, false,
86 | false, false, false, false, false, false, false, false,
87 |
88 | true, false, false, false, false, false, false, false, // `
89 | false, false, false, false, false, false, false, false,
90 | false, false, false, false, false, false, false, false,
91 | false, false, false, false, false, false, false, false,
92 |
93 | // non-ASCII
94 | false, false, false, false, false, false, false, false,
95 | false, false, false, false, false, false, false, false,
96 | false, false, false, false, false, false, false, false,
97 | false, false, false, false, false, false, false, false,
98 |
99 | false, false, false, false, false, false, false, false,
100 | false, false, false, false, false, false, false, false,
101 | false, false, false, false, false, false, false, false,
102 | false, false, false, false, false, false, false, false,
103 |
104 | false, false, false, false, false, false, false, false,
105 | false, false, false, false, false, false, false, false,
106 | false, false, false, false, false, false, false, false,
107 | false, false, false, false, false, false, false, false,
108 |
109 | false, false, false, false, false, false, false, false,
110 | false, false, false, false, false, false, false, false,
111 | false, false, false, false, false, false, false, false,
112 | false, false, false, false, false, false, false, false,
113 | }
114 |
--------------------------------------------------------------------------------
/pkg/mdx/testdata/fulldemo.md.out.txt:
--------------------------------------------------------------------------------
1 | Markdown Demo
2 |
3 | 一、标题
4 | 1. 使用 # 表示标题,其中 # 号必须在行首,例如:
5 | 一号标题
6 | 二号标题
7 | 三号标题
8 | 四号标题
9 | 五号标题
10 | 六号标题
11 | 2. 使用 === 或者 --- 表示,例如:
12 | 一级标题
13 | 二级标题
14 | 扩展:如何换行?
15 | 一般使用 两个空格 加 回车 换行,不过一些 IDE 也可以直接使用回车换行。
16 | 二、分割线
17 | 使用三个或以上的 - 或者 * 表示,且这一行只有符号,注意不要被识别为二级标题即可 ,例如中间或者前面可以加空格
18 |
19 |
20 | 三、斜体和粗体
21 | 使用 * 和 ** 分别表示斜体和粗体,例如
22 | 斜体
23 | 粗体
24 | 又斜又粗
25 | **扩展:**删除线使用两个 ~ 表示,例如
26 | 我是要删掉的文字
27 |
28 | 四、超链接和图片
29 | 超链接和图片的写法类似,图片仅在超链接前多了一个 ! ,一般是 [文字描述] (链接)
30 | 两种写法,分别是: 第一种写法 和 第二种写法
31 | 图片的话就比如这样:
32 |
33 | 五、无序列表
34 | 使用 -、+ 和 * 表示无序列表,前后留一行空白,可嵌套,例如
35 |
36 | 一层
37 |
38 | 二层
39 | 二层
40 |
47 |
48 |
49 |
50 | 一层
51 |
52 |
53 | 六、有序列表
54 | 使用 1. (点号后面有个空格)表示有序列表,可嵌套,例如
55 |
56 | 一层
57 |
58 | 二层
59 | 二层
60 |
61 |
62 | 一层
63 |
64 |
65 | 七、文字引用
66 | 使用 > 表示,可以有多个 >,表示层级更深,例如
67 |
68 | 第一层
69 |
70 | 第二层
71 | 这样是跳不出去的
72 |
73 | 还可以更深
74 |
75 |
76 |
77 |
78 | 这样就跳出去了
79 |
80 |
81 | 八、行内代码块
82 | 其实上面已经用过很多次了,即使用 ` 表示,例如
83 | 行内代码块
84 | 扩展:很多字符是需要转义,使用反斜杠 \ 进行转义
85 |
86 | 九、代码块
87 | 使用四个空格缩进表示代码块,例如
88 | public class HelloWorld\n{\n public static void main(String[] args)\n { \n System.out.println( "Hello, World!" );\n }\n}\n" }}>
89 | 一些 IDE 支持行数提示和着色,一般使用三个 ` 表示,例如
90 | public class HelloWorld\n{\n public static void main(String[] args)\n { \n System.out.println( "Hello, World!" );\n }\n}\n" }}>
91 |
92 | 十、表格
93 | 直接看例子吧,第二行的 ---: 表示了对齐方式,默认左对齐 ,还有右对齐 和居中
94 |
95 |
96 |
97 | 商品
98 | 数量
99 | 单价
100 |
101 |
102 |
103 |
104 | 苹果苹果苹果
105 | 10
106 | $1
107 |
108 |
109 | 电脑
110 | 1
111 | $1999
112 |
113 |
114 |
115 |
116 | 十一、数学公式
117 | 使用 $ 表示,其中一个 $ 表示在行内,两个 $ 表示独占一行。
118 | 例如质量守恒公式:$$E=mc^2$$
119 | 支持 LaTeX 编辑显示支持,例如:$\sum_{i=1}^n a_i=0$, 访问 MathJax 参考更多使用方法。
120 | 推荐一个常用的数学公式在线编译网站: https://www.codecogs.com/latex/eqneditor.php
121 |
122 | 十二、支持HTML标签
123 | 1. 例如想要段落的缩进,可以如下:
124 | 不断行的空白格 或
125 | 半方大的空白 或
126 | 全方大的空白 或
127 |
128 | 十三、其它
129 |
130 | markdown 各个 IDE 的使用可能存在大同小异,一般可以参考各个 IDE 的介绍文档
131 | 本文档介绍的内容基本适用于大部分的 IDE
132 | 其它一些类似 流程图 之类的功能,需要看 IDE 是否支持。
133 |
134 | 查看原始数据:https://gitee.com/afei_/MarkdownDemo/raw/master/README.md
135 | 博客:https://blog.csdn.net/afei__/article/details/80717153
136 |
--------------------------------------------------------------------------------
/pkg/html2jsx/html2jsx.go:
--------------------------------------------------------------------------------
1 | package html2jsx
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "github.com/tdewolff/parse/v2"
8 | "github.com/zbysir/gojsx/pkg/htmlparser"
9 | "io"
10 | "log"
11 | )
12 |
13 | // Convert
14 | //
15 | // enableJsx: 如果不开启,则会将 {} 处理成 html 编码
16 | func Convert(reader io.Reader, writer io.Writer, enableJsx bool) error {
17 | c := ctx{}
18 | return c.Covert(reader, writer, enableJsx)
19 | }
20 |
21 | type ctx struct {
22 | currStartTag []byte
23 | dangerouslySetInnerHTML bytes.Buffer
24 | dangerouslyHTMLStartTag []byte
25 | startDangerouslySetInnerHTML bool
26 | debug bool
27 | }
28 |
29 | func (c *ctx) Covert(src io.Reader, writer io.Writer, enableJsx bool) error {
30 | l := htmlparser.NewLexer(parse.NewInput(src))
31 | //pos := 0
32 |
33 | for l.Err() == nil {
34 | //start := pos
35 | tt, bs := l.Next()
36 |
37 | //pos += len(bs)
38 |
39 | if c.debug {
40 | log.Printf("debug token: %s %s", tt, bs)
41 | }
42 |
43 | writer.Write(c.toJsxToken(tt, bs, enableJsx))
44 | }
45 | if l.Err() != io.EOF {
46 | return l.Err()
47 | }
48 |
49 | return nil
50 | }
51 |
52 | func encodeJsxInsecure(s []byte) []byte {
53 | s = bytes.ReplaceAll(s, []byte("{"), []byte("{"))
54 | s = bytes.ReplaceAll(s, []byte("}"), []byte("}"))
55 | s = bytes.ReplaceAll(s, []byte("<"), []byte("<"))
56 | s = bytes.ReplaceAll(s, []byte(">"), []byte(">"))
57 | return s
58 | }
59 |
60 | func toStringCode(s []byte) []byte {
61 | var bf bytes.Buffer
62 | je := json.NewEncoder(&bf)
63 | je.SetEscapeHTML(false)
64 | _ = je.Encode(string(s))
65 |
66 | // 可以尝试更高效的 strconv.Quote 方法
67 | // strconv.Quote(string(s))
68 |
69 | return bytes.TrimSuffix(bf.Bytes(), []byte{'\n'})
70 | }
71 |
72 | // - script|pre|style|textarea 的子节点需要处理成纯文本
73 | func (c *ctx) toJsxToken(tt htmlparser.TokenType, src []byte, enableJsx bool) []byte {
74 | if c.startDangerouslySetInnerHTML {
75 | switch tt {
76 | case htmlparser.EndTagToken:
77 | var tag = src[2 : len(src)-1]
78 | if bytes.Equal(c.dangerouslyHTMLStartTag, tag) {
79 | innerBs := c.dangerouslySetInnerHTML.Bytes()
80 | var bs []byte
81 | if len(innerBs) != 0 {
82 | bs = []byte(fmt.Sprintf(` dangerouslySetInnerHTML={{ __html: %s }}>%s`, toStringCode(innerBs), src))
83 | } else {
84 | bs = []byte(fmt.Sprintf(`>%s`, src))
85 | }
86 | c.dangerouslySetInnerHTML.Reset()
87 | c.dangerouslyHTMLStartTag = nil
88 | c.startDangerouslySetInnerHTML = false
89 | return bs
90 | }
91 | }
92 | c.dangerouslySetInnerHTML.Write(src)
93 | return nil
94 | }
95 | switch tt {
96 | case htmlparser.StartTagToken:
97 | var tag = src[1:]
98 | if in(tag, []string{"script", "pre", "style", "textarea"}) {
99 | c.dangerouslyHTMLStartTag = tag
100 | } else if len(tag) == 0 {
101 | // for <
102 | src = encodeJsxInsecure(src)
103 | } else {
104 | // for
119 | if len(c.currStartTag) == 0 {
120 | src = encodeJsxInsecure(src)
121 | }
122 | case htmlparser.EndTagToken:
123 | // for >
124 | if len(c.currStartTag) == 0 {
125 | src = encodeJsxInsecure(src)
126 | } else {
127 | src = bytes.ToLower(src)
128 | }
129 | case htmlparser.TextToken:
130 | if !enableJsx {
131 | src = encodeJsxInsecure(src)
132 | }
133 | case htmlparser.CommentToken:
134 | src = encodeJsxInsecure(src)
135 | case htmlparser.AttributeToken:
136 | kvs := bytes.Split(src, []byte("="))
137 | if len(kvs) == 2 {
138 | v := bytes.Trim(kvs[1], " ")
139 | if !bytes.HasSuffix(v, []byte(`"`)) || !bytes.HasPrefix(v, []byte(`"`)) {
140 | v, _ = json.Marshal(string(v))
141 | }
142 | src = []byte(fmt.Sprintf("%s=%s", kvs[0], v))
143 | }
144 | }
145 |
146 | return src
147 | }
148 |
149 | func in(s []byte, i []string) bool {
150 | for _, item := range i {
151 | if item == string(s) {
152 | return true
153 | }
154 | }
155 |
156 | return false
157 | }
158 |
--------------------------------------------------------------------------------
/test/esbuild/esbuild_test.go:
--------------------------------------------------------------------------------
1 | package esbuild
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "fmt"
7 | "github.com/evanw/esbuild/pkg/api"
8 | sourcemap2 "github.com/go-sourcemap/sourcemap"
9 | "github.com/zbysir/gojsx"
10 | "github.com/zbysir/gojsx/test/sourcemap"
11 | "strings"
12 | "testing"
13 | )
14 |
15 | func mockSourcemap() []byte {
16 | m := sourcemap.Map{
17 | Version: 3,
18 | File: "a.md",
19 | SourceRoot: "",
20 | Sources: []string{"a.md"},
21 | SourcesContent: []string{`# h1
22 | hihi {a}
23 | `},
24 | Names: nil,
25 | Mappings: "",
26 | }
27 |
28 | // 0 开始计数, [0, 2] => [0, 6], [1, 0] => [0, 17], [1, 6] => [0, 22]
29 | // https://www.murzwin.com/base64vlq.html
30 | // MAAE,WACF,KAAM
31 | //
32 | // # h1
33 | // ^2
34 | // hihi {a}
35 | // ^0 ^6
36 | // <>h1 hih {a}
>
37 | // ^6 ^17 ^22
38 | //
39 |
40 | m.AddMapping(&sourcemap.Mapping{
41 | GeneratedLine: 0,
42 | GeneratedColumn: 6,
43 | OriginalFile: "a.md",
44 | OriginalLine: 0,
45 | OriginalColumn: 2,
46 | OriginalName: "",
47 | })
48 | m.AddMapping(&sourcemap.Mapping{
49 | GeneratedLine: 0,
50 | GeneratedColumn: 17,
51 | OriginalFile: "a.md",
52 | OriginalLine: 1,
53 | OriginalColumn: 0,
54 | OriginalName: "",
55 | })
56 | m.AddMapping(&sourcemap.Mapping{
57 | GeneratedLine: 0,
58 | GeneratedColumn: 22,
59 | OriginalFile: "a.md",
60 | OriginalLine: 1,
61 | OriginalColumn: 6,
62 | OriginalName: "",
63 | })
64 |
65 | m.EncodeMappings()
66 | sm := bytes.Buffer{}
67 | err := m.WriteTo(&sm)
68 | if err != nil {
69 | }
70 |
71 | return sm.Bytes()
72 | }
73 |
74 | func sourceLine(s string, i int) string {
75 | return strings.Split(s, "\n")[i-1]
76 | }
77 |
78 | func TestEsbuild(t *testing.T) {
79 | m := mockSourcemap()
80 | bm := "," + base64.URLEncoding.EncodeToString(m)
81 |
82 | _ = bm
83 | t.Logf("%s", m)
84 |
85 | // esbuild Transform 支持输入 sourcemap
86 | // 不过在报错的时候产生的 Location 没有通过 sourcemap 转换,需要手动转换。
87 | result := api.Transform(`<>h1 hih {a}
>;
88 | //# sourceMappingURL=data:application/json;base64`+bm, api.TransformOptions{
89 | Loader: api.LoaderJSX,
90 | Target: api.ESNext,
91 | JSXMode: api.JSXModeAutomatic,
92 | JSXDev: false,
93 | Banner: "",
94 | Platform: api.PlatformNode,
95 | Format: api.FormatIIFE,
96 | Sourcemap: api.SourceMapInline,
97 | Sourcefile: "root.js",
98 | MinifyIdentifiers: true,
99 | })
100 |
101 | if len(result.Errors) != 0 {
102 | var err error
103 | c, _ := sourcemap2.Parse("", m)
104 | e := result.Errors[0]
105 | if e.Location != nil {
106 | file := e.Location.File
107 | l := e.Location.Line
108 | i := e.Location.Column
109 | text := e.Location.LineText
110 | source, name, line, col, ok := c.Source(l, i)
111 | t.Logf("%v %v => %+v %v %v %v %v", l, i, source, name, line, col, ok)
112 |
113 | // 手动转换
114 | if ok {
115 | file = source
116 | l = line
117 | i = col
118 | text = sourceLine(c.SourceContent(source), line)
119 | }
120 |
121 | err = fmt.Errorf("\n%v: (%v:%v) \n%v\n%v^ %v\n", file, l, i, text, strings.Repeat(" ", i), e.Text)
122 | } else {
123 | err = fmt.Errorf("%v\n", e.Text)
124 | }
125 | t.Fatal(err)
126 | }
127 |
128 | t.Logf("%s", result.Code)
129 |
130 | code := string(result.Code)
131 |
132 | gx, _ := gojsx.NewJsx(gojsx.Option{})
133 | v, err := gx.ExecCode([]byte(code))
134 | if err != nil {
135 | t.Fatal(err)
136 | }
137 | t.Logf("%+v", v)
138 | // output :ReferenceError: a is not defined
139 | // at a.md:2:6
140 | }
141 |
142 | func TestLoad(t *testing.T) {
143 | result := api.Build(api.BuildOptions{
144 | EntryPoints: []string{"./test/Index.jsx"},
145 | Bundle: true,
146 | JSXMode: api.JSXModeAutomatic,
147 | JSXImportSource: "../internal/js",
148 | MinifyWhitespace: true,
149 | MinifyIdentifiers: true,
150 | MinifySyntax: true,
151 |
152 | Loader: map[string]api.Loader{
153 | ".png": api.LoaderDataURL,
154 | ".svg": api.LoaderText,
155 | },
156 | OutExtensions: map[string]string{},
157 | Target: api.ES2015,
158 | Write: true,
159 | Sourcemap: api.SourceMapInline,
160 | })
161 |
162 | if len(result.Errors) > 0 {
163 | for _, v := range result.Errors {
164 | t.Fatalf("%+v %+v", v.Text, v.Location)
165 | }
166 | }
167 |
168 | files := result.OutputFiles
169 | for _, f := range files {
170 | t.Logf("%+v %s", f.Path, f.Contents)
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/test/preact/preact_test.go:
--------------------------------------------------------------------------------
1 | package preact
2 |
3 | import (
4 | "fmt"
5 | "github.com/zbysir/gojsx"
6 | "io/fs"
7 | "log"
8 | "net/http"
9 | "os"
10 | "path/filepath"
11 | "strings"
12 | "testing"
13 | "time"
14 | )
15 |
16 | //var src embed.FS
17 |
18 | func TestPreactSSR(t *testing.T) {
19 |
20 | var src fs.FS
21 |
22 | src = os.DirFS(".")
23 |
24 | file := "./src/index.tsx"
25 | t.Run("tsx", func(t *testing.T) {
26 | x := gojsx.NewEsBuildTransform(gojsx.EsBuildTransformOptions{
27 | Minify: true,
28 | })
29 |
30 | rootBs, err := fs.ReadFile(src, "src/root.tsx")
31 | if err != nil {
32 | t.Fatal(err)
33 | }
34 |
35 | b, err := x.Transform("./src/root.tsx", rootBs, gojsx.TransformerFormatESModule)
36 | if err != nil {
37 | t.Fatal(err)
38 | }
39 |
40 | t.Logf("%s", b)
41 |
42 | j, err := gojsx.NewJsx(gojsx.Option{
43 | Fs: src,
44 | })
45 | if err != nil {
46 | t.Fatal(err)
47 | }
48 |
49 | j.RegisterModule("preact/hooks", map[string]interface{}{
50 | "useState": func(i any) interface{} {
51 | return []interface{}{i, func(i any) {}}
52 | },
53 | })
54 |
55 | v, err := j.Exec(file, gojsx.WithAutoExecJsx(nil))
56 | if err != nil {
57 | t.Fatal(err)
58 | }
59 |
60 | s := gojsx.Render(v.Default)
61 | t.Logf("%s", s)
62 | })
63 |
64 | t.Run("http", func(t *testing.T) {
65 | // listen 9091
66 | // http://localhost:9091
67 |
68 | m := http.NewServeMux()
69 | m.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
70 | j, err := gojsx.NewJsx(gojsx.Option{
71 | Fs: src,
72 | })
73 | if err != nil {
74 | t.Fatal(err)
75 | }
76 | _, fileName := filepath.Split(r.URL.Path)
77 | if fileName == "" {
78 | fileName = "index"
79 | }
80 |
81 | j.RegisterModule("preact/hooks", map[string]interface{}{
82 | "useState": func(i any) interface{} {
83 | return []interface{}{i, func(i any) {}}
84 | },
85 | })
86 |
87 | file := fmt.Sprintf("./src/page/%s.tsx", fileName)
88 |
89 | var code []byte
90 | if r.URL.Query().Has("ssr") {
91 | code = []byte(fmt.Sprintf(`import Layout from "./src/index.tsx"; import Page from "%v" ;
92 | export default function a ({js}){
93 | return }`, file))
94 | } else {
95 | code = []byte(fmt.Sprintf(`import Layout from "./src/index.tsx";
96 | export default function a ({js}){
97 | return }`))
98 | }
99 |
100 | log.Printf("code: %s", code)
101 |
102 | start := time.Now()
103 | v, err := j.ExecCode(code, gojsx.WithAutoExecJsx(map[string]interface{}{"js": fmt.Sprintf(`import {h, hydrate} from "https://cdn.skypack.dev/preact";
104 | import root from "./js/page/%s.tsx";
105 | hydrate(h(root), document.body);
106 | `, fileName)}), gojsx.WithFileName("root.tsx"))
107 | if err != nil {
108 | t.Fatal(err)
109 | }
110 |
111 | s := gojsx.Render(v.Default)
112 | s = strings.ReplaceAll(s, "", fmt.Sprintf("%s", time.Now().Sub(start).String()))
113 | w.Write([]byte(s))
114 | return
115 | }))
116 |
117 | m.Handle("/jslib/react/jsx-runtime", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
118 | bs, err := fs.ReadFile(src, "src/jslib/react/jsx-runtime.js")
119 | if err != nil {
120 | t.Fatal(err)
121 | }
122 | w.Header().Set("Content-Type", "application/javascript")
123 | w.Write(bs)
124 | }))
125 |
126 | m.Handle("/js/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
127 | x := gojsx.NewEsBuildTransform(gojsx.EsBuildTransformOptions{
128 | Minify: true,
129 | })
130 |
131 | fileName := strings.TrimPrefix(r.URL.Path, "/js/")
132 |
133 | page := filepath.Join("src", fileName)
134 |
135 | log.Printf("page: %s", fileName)
136 |
137 | rootBs, err := fs.ReadFile(src, page)
138 | if err != nil {
139 | w.Write([]byte(err.Error()))
140 | w.WriteHeader(400)
141 | return
142 | }
143 | pageJs, err := x.Transform(page, rootBs, gojsx.TransformerFormatESModule)
144 | if err != nil {
145 | w.Write([]byte(err.Error()))
146 | w.WriteHeader(400)
147 | return
148 | }
149 |
150 | // import{jsx as t,jsxs as i}from"react/jsx-runtime";import{useState as c}from"preact/hooks"
151 |
152 | sJs := string(pageJs)
153 | //实现在浏览器中import内联JS模块
154 | // https://juejin.cn/post/7070339012933713956
155 |
156 | // replace used module
157 | // 这个后期可以做成由用户配置,和 ImportMap 类似
158 | //sJs = strings.ReplaceAll(sJs, "react/jsx-runtime", "/jslib/react/jsx-runtime")
159 | sJs = strings.ReplaceAll(sJs, "react/jsx-runtime", "https://cdn.skypack.dev/preact/compat/jsx-runtime")
160 | //sJs = strings.ReplaceAll(sJs, "react/jsx-runtime", "https://cdn.skypack.dev/react/jsx-runtime")
161 | sJs = strings.ReplaceAll(sJs, `"preact/hooks"`, `"https://cdn.skypack.dev/preact/hooks"`)
162 | //sJs = strings.ReplaceAll(sJs, `"preact/hooks"`, `"https://cdn.skypack.dev/react"`)
163 | sJs = strings.ReplaceAll(sJs, `"preact"`, `"https://cdn.skypack.dev/preact"`)
164 |
165 | w.Header().Set("Content-Type", "application/javascript")
166 |
167 | w.Write([]byte(sJs))
168 | }))
169 |
170 | http.ListenAndServe(":9091", m)
171 |
172 | })
173 |
174 | }
175 |
--------------------------------------------------------------------------------
/test/sourcemap/sourcemap.go:
--------------------------------------------------------------------------------
1 | package sourcemap
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "io"
7 | "sort"
8 | "strings"
9 | )
10 |
11 | type Map struct {
12 | Version int `json:"version"`
13 | File string `json:"file,omitempty"`
14 | SourceRoot string `json:"sourceRoot,omitempty"`
15 | Sources []string `json:"sources"`
16 | SourcesContent []string `json:"sourcesContent,omitempty"`
17 | Names []string `json:"names,omitempty"`
18 | Mappings string `json:"mappings"`
19 | decodedMappings []*Mapping
20 | }
21 |
22 | type Mapping struct {
23 | GeneratedLine int
24 | GeneratedColumn int
25 | OriginalFile string
26 | OriginalLine int
27 | OriginalColumn int
28 | OriginalName string
29 | }
30 |
31 | func ReadFrom(r io.Reader) (*Map, error) {
32 | d := json.NewDecoder(r)
33 | var m Map
34 | if err := d.Decode(&m); err != nil {
35 | return nil, err
36 | }
37 | return &m, nil
38 | }
39 |
40 | const base64encode = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
41 |
42 | var base64decode [256]int
43 |
44 | func init() {
45 | for i := 0; i < len(base64decode); i++ {
46 | base64decode[i] = 0xff
47 | }
48 | for i := 0; i < len(base64encode); i++ {
49 | base64decode[base64encode[i]] = i
50 | }
51 | }
52 |
53 | func (m *Map) decodeMappings() {
54 | if m.decodedMappings != nil {
55 | return
56 | }
57 |
58 | r := strings.NewReader(m.Mappings)
59 | var generatedLine = 1
60 | var generatedColumn = 0
61 | var originalFile = 0
62 | var originalLine = 1
63 | var originalColumn = 0
64 | var originalName = 0
65 | for r.Len() != 0 {
66 | b, _ := r.ReadByte()
67 | if b == ',' {
68 | continue
69 | }
70 | if b == ';' {
71 | generatedLine++
72 | generatedColumn = 0
73 | continue
74 | }
75 | r.UnreadByte()
76 |
77 | count := 0
78 | readVLQ := func() int {
79 | v := 0
80 | s := uint(0)
81 | for {
82 | b, _ := r.ReadByte()
83 | o := base64decode[b]
84 | if o == 0xff {
85 | r.UnreadByte()
86 | return 0
87 | }
88 | v += (o &^ 32) << s
89 | if o&32 == 0 {
90 | break
91 | }
92 | s += 5
93 | }
94 | count++
95 | if v&1 != 0 {
96 | return -(v >> 1)
97 | }
98 | return v >> 1
99 | }
100 | generatedColumn += readVLQ()
101 | originalFile += readVLQ()
102 | originalLine += readVLQ()
103 | originalColumn += readVLQ()
104 | originalName += readVLQ()
105 |
106 | switch count {
107 | case 1:
108 | m.decodedMappings = append(m.decodedMappings, &Mapping{generatedLine, generatedColumn, "", 0, 0, ""})
109 | case 4:
110 | m.decodedMappings = append(m.decodedMappings, &Mapping{generatedLine, generatedColumn, m.Sources[originalFile], originalLine, originalColumn, ""})
111 | case 5:
112 | m.decodedMappings = append(m.decodedMappings, &Mapping{generatedLine, generatedColumn, m.Sources[originalFile], originalLine, originalColumn, m.Names[originalName]})
113 | }
114 | }
115 | }
116 |
117 | func (m *Map) DecodedMappings() []*Mapping {
118 | m.decodeMappings()
119 | return m.decodedMappings
120 | }
121 |
122 | func (m *Map) ClearMappings() {
123 | m.Mappings = ""
124 | m.decodedMappings = nil
125 | }
126 |
127 | func (m *Map) AddMapping(mapping *Mapping) {
128 | m.decodedMappings = append(m.decodedMappings, mapping)
129 | }
130 |
131 | func (m *Map) Len() int {
132 | m.decodeMappings()
133 | return len(m.DecodedMappings())
134 | }
135 |
136 | func (m *Map) Less(i, j int) bool {
137 | a := m.decodedMappings[i]
138 | b := m.decodedMappings[j]
139 | return a.GeneratedLine < b.GeneratedLine || (a.GeneratedLine == b.GeneratedLine && a.GeneratedColumn < b.GeneratedColumn)
140 | }
141 |
142 | func (m *Map) Swap(i, j int) {
143 | m.decodedMappings[i], m.decodedMappings[j] = m.decodedMappings[j], m.decodedMappings[i]
144 | }
145 |
146 | func (m *Map) EncodeMappings() {
147 | sort.Sort(m)
148 | m.Sources = nil
149 | fileIndexMap := make(map[string]int)
150 | m.Names = nil
151 | nameIndexMap := make(map[string]int)
152 | var generatedLine = 0
153 | var generatedColumn = 0
154 | var originalFile = 0
155 | var originalLine = 0
156 | var originalColumn = 0
157 | var originalName = 0
158 | buf := bytes.NewBuffer(nil)
159 | comma := false
160 | for _, mapping := range m.decodedMappings {
161 | for mapping.GeneratedLine > generatedLine {
162 | buf.WriteByte(';')
163 | generatedLine++
164 | generatedColumn = 0
165 | comma = false
166 | }
167 | if comma {
168 | buf.WriteByte(',')
169 | }
170 |
171 | writeVLQ := func(v int) {
172 | v <<= 1
173 | if v < 0 {
174 | v = -v
175 | v |= 1
176 | }
177 | for v >= 32 {
178 | buf.WriteByte(base64encode[32|(v&31)])
179 | v >>= 5
180 | }
181 | buf.WriteByte(base64encode[v])
182 | }
183 |
184 | writeVLQ(mapping.GeneratedColumn - generatedColumn)
185 | generatedColumn = mapping.GeneratedColumn
186 |
187 | if mapping.OriginalFile != "" {
188 | fileIndex, ok := fileIndexMap[mapping.OriginalFile]
189 | if !ok {
190 | fileIndex = len(m.Sources)
191 | fileIndexMap[mapping.OriginalFile] = fileIndex
192 | m.Sources = append(m.Sources, mapping.OriginalFile)
193 | }
194 | writeVLQ(fileIndex - originalFile)
195 | originalFile = fileIndex
196 |
197 | writeVLQ(mapping.OriginalLine - originalLine)
198 | originalLine = mapping.OriginalLine
199 |
200 | writeVLQ(mapping.OriginalColumn - originalColumn)
201 | originalColumn = mapping.OriginalColumn
202 |
203 | if mapping.OriginalName != "" {
204 | nameIndex, ok := nameIndexMap[mapping.OriginalName]
205 | if !ok {
206 | nameIndex = len(m.Names)
207 | nameIndexMap[mapping.OriginalName] = nameIndex
208 | m.Names = append(m.Names, mapping.OriginalName)
209 | }
210 | writeVLQ(nameIndex - originalName)
211 | originalName = nameIndex
212 | }
213 | }
214 |
215 | comma = true
216 | }
217 | m.Mappings = buf.String()
218 | }
219 |
220 | func (m *Map) WriteTo(w io.Writer) error {
221 | if m.Version == 0 {
222 | m.Version = 3
223 | }
224 | if m.decodedMappings != nil {
225 | m.EncodeMappings()
226 | }
227 | if m.Names == nil {
228 | m.Names = make([]string, 0)
229 | }
230 | if m.Sources == nil {
231 | m.Sources = make([]string, 0)
232 | }
233 | enc := json.NewEncoder(w)
234 | return enc.Encode(m)
235 | }
236 |
--------------------------------------------------------------------------------
/internal/pkg/goja_nodejs/require/resolve.go:
--------------------------------------------------------------------------------
1 | package require
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | js "github.com/dop251/goja"
7 | "path"
8 | "strings"
9 | )
10 |
11 | // NodeJS module search algorithm described by
12 | // https://nodejs.org/api/modules.html#modules_all_together
13 | func (r *RequireModule) resolve(modpath string) (module *js.Object, err error) {
14 | origPath, modpath := modpath, filepathClean(modpath)
15 | if modpath == "" {
16 | return nil, IllegalModuleNameError
17 | }
18 |
19 | module, err = r.loadNative(modpath)
20 | if err == nil {
21 | return
22 | }
23 |
24 | var start string
25 | err = nil
26 | if path.IsAbs(origPath) {
27 | start = "/"
28 | } else {
29 | start = r.getCurrentModulePath()
30 | }
31 |
32 | p := path.Join(start, modpath)
33 | if strings.HasPrefix(origPath, "./") ||
34 | strings.HasPrefix(origPath, "/") || strings.HasPrefix(origPath, "../") ||
35 | origPath == "." || origPath == ".." {
36 | if module, _ = r.modulesCache.Get(p); module != nil {
37 | return
38 | }
39 | module, err = r.loadAsFileOrDirectory(p)
40 | if err == nil && module != nil {
41 | r.modulesCache.Add(p, module)
42 | }
43 | } else {
44 | if module = r.nodeModules[p]; module != nil {
45 | return
46 | }
47 | module, err = r.loadNodeModules(modpath, start)
48 | if err == nil && module != nil {
49 | r.nodeModules[p] = module
50 | }
51 | }
52 |
53 | if module == nil && err == nil {
54 | err = &ErrorInvalidModule{Name: p}
55 | }
56 | return
57 | }
58 |
59 | func (r *RequireModule) loadNative(path string) (*js.Object, error) {
60 | module, ok := r.modulesCache.Get(path)
61 | if ok {
62 | return module, nil
63 | }
64 |
65 | var ldr ModuleLoader
66 | if ldr = r.r.native[path]; ldr == nil {
67 | ldr = native[path]
68 | }
69 |
70 | if ldr != nil {
71 | module = r.createModuleObject()
72 | r.modulesCache.Add(path, module)
73 | ldr(r.runtime, module)
74 | return module, nil
75 | }
76 |
77 | return nil, &ErrorInvalidModule{Name: path}
78 | }
79 |
80 | func (r *RequireModule) loadAsFileOrDirectory(path string) (module *js.Object, err error) {
81 | if module, err = r.loadAsFile(path); module != nil || err != nil {
82 | return
83 | }
84 |
85 | return r.loadAsDirectory(path)
86 | }
87 |
88 | func (r *RequireModule) loadAsFile(path string) (module *js.Object, err error) {
89 | if module, err = r.loadModule(path); module != nil || err != nil {
90 | return
91 | }
92 |
93 | p := path + ".js"
94 | if module, err = r.loadModule(p); module != nil || err != nil {
95 | return
96 | }
97 |
98 | p = path + ".json"
99 | return r.loadModule(p)
100 | }
101 |
102 | func (r *RequireModule) loadIndex(modpath string) (module *js.Object, err error) {
103 | p := path.Join(modpath, "index.js")
104 | if module, err = r.loadModule(p); module != nil || err != nil {
105 | return
106 | }
107 |
108 | p = path.Join(modpath, "index.json")
109 | return r.loadModule(p)
110 | }
111 |
112 | func (r *RequireModule) loadAsDirectory(modpath string) (module *js.Object, err error) {
113 | p := path.Join(modpath, "package.json")
114 | buf, err := r.r.getSource(p)
115 | if err != nil {
116 | return r.loadIndex(modpath)
117 | }
118 | var pkg struct {
119 | Main string
120 | }
121 | err = json.Unmarshal(buf, &pkg)
122 | if err != nil || len(pkg.Main) == 0 {
123 | return r.loadIndex(modpath)
124 | }
125 |
126 | m := path.Join(modpath, pkg.Main)
127 | if module, err = r.loadAsFile(m); module != nil || err != nil {
128 | return
129 | }
130 |
131 | return r.loadIndex(m)
132 | }
133 |
134 | func (r *RequireModule) loadNodeModule(modpath, start string) (*js.Object, error) {
135 | return r.loadAsFileOrDirectory(path.Join(start, modpath))
136 | }
137 |
138 | func (r *RequireModule) loadNodeModules(modpath, start string) (module *js.Object, err error) {
139 | for _, dir := range r.r.globalFolders {
140 | if module, err = r.loadNodeModule(modpath, dir); module != nil || err != nil {
141 | return
142 | }
143 | }
144 | for {
145 | var p string
146 | if path.Base(start) != "node_modules" {
147 | p = path.Join(start, "node_modules")
148 | } else {
149 | p = start
150 | }
151 | if module, err = r.loadNodeModule(modpath, p); module != nil || err != nil {
152 | return
153 | }
154 | if start == ".." { // Dir('..') is '.'
155 | break
156 | }
157 | parent := path.Dir(start)
158 | if parent == start {
159 | break
160 | }
161 | start = parent
162 | }
163 |
164 | return
165 | }
166 |
167 | func (r *RequireModule) getCurrentModulePath() string {
168 | var buf [2]js.StackFrame
169 | frames := r.runtime.CaptureCallStack(2, buf[:0])
170 | if len(frames) < 2 {
171 | return "."
172 | }
173 | return path.Dir(frames[1].SrcName())
174 | }
175 |
176 | func (r *RequireModule) createModuleObject() *js.Object {
177 | module := r.runtime.NewObject()
178 | module.Set("exports", r.runtime.NewObject())
179 | return module
180 | }
181 |
182 | func (r *RequireModule) loadModule(path string) (*js.Object, error) {
183 | module, ok := r.modulesCache.Get(path)
184 | if !ok {
185 | module = r.createModuleObject()
186 | // 解决循环引用
187 | r.modulesCache.Add(path, module)
188 | end := r.r.timeTracker.Start("loadModuleFile")
189 | err := r.loadModuleFile(path, module)
190 | end()
191 | if err != nil {
192 | module = nil
193 | r.modulesCache.Remove(path)
194 | if errors.Is(err, ModuleFileDoesNotExistError) {
195 | err = nil
196 | }
197 | }
198 | return module, err
199 | }
200 | return module, nil
201 | }
202 |
203 | func (r *RequireModule) loadModuleFile(path string, jsModule *js.Object) error {
204 | end := r.r.timeTracker.Start("getCompiledSource")
205 | prg, err := r.r.getCompiledSource(path)
206 | end()
207 |
208 | if err != nil {
209 | return err
210 | }
211 |
212 | f, err := r.runtime.RunProgram(prg)
213 | if err != nil {
214 | return err
215 | }
216 |
217 | if call, ok := js.AssertFunction(f); ok {
218 | jsExports := jsModule.Get("exports")
219 | jsRequire := r.runtime.Get("require")
220 |
221 | // Run the module source, with "jsExports" as "this",
222 | // "jsExports" as the "exports" variable, "jsRequire"
223 | // as the "require" variable and "jsModule" as the
224 | // "module" variable (Nodejs capable).
225 | _, err = call(jsExports, jsExports, jsRequire, jsModule)
226 | if err != nil {
227 | return err
228 | }
229 | } else {
230 | return &ErrorInvalidModule{Name: path}
231 | }
232 |
233 | return nil
234 | }
235 |
--------------------------------------------------------------------------------
/internal/pkg/goja_nodejs/require/module.go:
--------------------------------------------------------------------------------
1 | package require
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/hex"
6 | "errors"
7 | "fmt"
8 | js "github.com/dop251/goja"
9 | "github.com/dop251/goja/parser"
10 | lru "github.com/hashicorp/golang-lru/v2"
11 | "github.com/zbysir/gojsx/pkg/timetrack"
12 | "os"
13 | "path"
14 | "path/filepath"
15 | "runtime"
16 | "sync"
17 | "syscall"
18 | )
19 |
20 | type ModuleLoader func(*js.Runtime, *js.Object)
21 |
22 | // SourceLoader represents a function that returns a file data at a given path.
23 | // The function should return ModuleFileDoesNotExistError if the file either doesn't exist or is a directory.
24 | // This error will be ignored by the resolver and the search will continue. Any other errors will be propagated.
25 | type SourceLoader func(path string) ([]byte, error)
26 |
27 | type ErrorInvalidModule struct {
28 | Name string
29 | }
30 |
31 | func (e ErrorInvalidModule) Error() string {
32 | return fmt.Sprintf("Invalid module: '%v'", e.Name)
33 | }
34 |
35 | var (
36 | InvalidModuleError = errors.New("Invalid module")
37 | IllegalModuleNameError = errors.New("Illegal module name")
38 |
39 | ModuleFileDoesNotExistError = errors.New("module file does not exist")
40 | )
41 |
42 | var native map[string]ModuleLoader
43 |
44 | // Registry contains a cache of compiled modules which can be used by multiple Runtimes
45 | type Registry struct {
46 | sync.Mutex
47 | native map[string]ModuleLoader
48 | compliedCache *lru.Cache[string, *js.Program]
49 | SrcLoader SourceLoader
50 | globalFolders []string
51 | timeTracker *timetrack.TimeTracker
52 | }
53 |
54 | type RequireModule struct {
55 | r *Registry
56 | runtime *js.Runtime
57 | modulesCache *lru.Cache[string, *js.Object]
58 | nodeModules map[string]*js.Object
59 | }
60 |
61 | func NewRegistry(opts ...Option) *Registry {
62 | c, err := lru.New[string, *js.Program](100)
63 | if err != nil {
64 | panic(err)
65 | }
66 | r := &Registry{
67 | compliedCache: c,
68 | }
69 |
70 | for _, opt := range opts {
71 | opt(r)
72 | }
73 |
74 | return r
75 | }
76 |
77 | func NewRegistryWithLoader(srcLoader SourceLoader) *Registry {
78 | return NewRegistry(WithLoader(srcLoader))
79 | }
80 |
81 | type Option func(*Registry)
82 |
83 | // WithLoader sets a function which will be called by the require() function in order to get a source code for a
84 | // module at the given path. The same function will be used to get external source maps.
85 | // Note, this only affects the modules loaded by the require() function. If you need to use it as a source map
86 | // loader for code parsed in a different way (such as runtime.RunString() or eval()), use (*Runtime).SetParserOptions()
87 | func WithLoader(srcLoader SourceLoader) Option {
88 | return func(r *Registry) {
89 | r.SrcLoader = srcLoader
90 | }
91 | }
92 |
93 | // WithGlobalFolders appends the given paths to the registry's list of
94 | // global folders to search if the requested module is not found
95 | // elsewhere. By default, a registry's global folders list is empty.
96 | // In the reference Node.js implementation, the default global folders
97 | // list is $NODE_PATH, $HOME/.node_modules, $HOME/.node_libraries and
98 | // $PREFIX/lib/node, see
99 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders.
100 | func WithGlobalFolders(globalFolders ...string) Option {
101 | return func(r *Registry) {
102 | r.globalFolders = globalFolders
103 | }
104 | }
105 |
106 | // Enable adds the require() function to the specified runtime.
107 | func (r *Registry) Enable(runtime *js.Runtime) *RequireModule {
108 | c, _ := lru.New[string, *js.Object](100)
109 | rrt := &RequireModule{
110 | r: r,
111 | runtime: runtime,
112 | modulesCache: c,
113 | nodeModules: make(map[string]*js.Object),
114 | }
115 |
116 | runtime.Set("require", rrt.require)
117 | return rrt
118 | }
119 |
120 | func (r *Registry) RegisterNativeModule(name string, loader ModuleLoader) {
121 | r.Lock()
122 | defer r.Unlock()
123 |
124 | if r.native == nil {
125 | r.native = make(map[string]ModuleLoader)
126 | }
127 | name = filepathClean(name)
128 | r.native[name] = loader
129 | }
130 |
131 | // DefaultSourceLoader is used if none was set (see WithLoader()). It simply loads files from the host's filesystem.
132 | func DefaultSourceLoader(filename string) ([]byte, error) {
133 | fp := filepath.FromSlash(filename)
134 | data, err := os.ReadFile(fp)
135 | if err != nil {
136 | if os.IsNotExist(err) || errors.Is(err, syscall.EISDIR) {
137 | err = ModuleFileDoesNotExistError
138 | } else if runtime.GOOS == "windows" {
139 | if errors.Is(err, syscall.Errno(0x7b)) { // ERROR_INVALID_NAME, The filename, directory name, or volume label syntax is incorrect.
140 | err = ModuleFileDoesNotExistError
141 | } else {
142 | // temporary workaround for https://github.com/dop251/goja_nodejs/issues/21
143 | fi, err1 := os.Stat(fp)
144 | if err1 == nil && fi.IsDir() {
145 | err = ModuleFileDoesNotExistError
146 | }
147 | }
148 | }
149 | }
150 | return data, err
151 | }
152 |
153 | func (r *Registry) getSource(p string) ([]byte, error) {
154 | srcLoader := r.SrcLoader
155 | if srcLoader == nil {
156 | srcLoader = DefaultSourceLoader
157 | }
158 | return srcLoader(p)
159 | }
160 |
161 | func (r *Registry) ClearCompliedCache() {
162 | r.compliedCache.Purge()
163 | }
164 |
165 | func mD5(v []byte) string {
166 | m := md5.New()
167 | m.Write(v)
168 | return hex.EncodeToString(m.Sum(nil))
169 | }
170 |
171 | func (r *Registry) getCompiledSource(p string) (*js.Program, error) {
172 | r.Lock()
173 | defer r.Unlock()
174 |
175 | end := r.timeTracker.Start("getSource")
176 | buf, err := r.getSource(p)
177 | end()
178 | if err != nil {
179 | return nil, err
180 | }
181 |
182 | cacheKey := p + mD5(buf)
183 | prg, ok := r.compliedCache.Get(cacheKey)
184 | if ok {
185 | return prg, nil
186 | }
187 |
188 | source := "(function(exports, require, module) {" + string(buf) + "\n})"
189 | parsed, err := js.Parse(p, source, parser.WithSourceMapLoader(r.SrcLoader))
190 | if err != nil {
191 | return nil, err
192 | }
193 | prg, err = js.CompileAST(parsed, false)
194 | if err == nil {
195 | r.compliedCache.Add(cacheKey, prg)
196 | }
197 | return prg, err
198 | }
199 |
200 | func (r *RequireModule) require(call js.FunctionCall) js.Value {
201 | ret, err := r.Require(call.Argument(0).String())
202 | if err != nil {
203 | if _, ok := err.(*js.Exception); !ok {
204 | panic(r.runtime.NewGoError(err))
205 | }
206 | panic(err)
207 | }
208 | return ret
209 | }
210 |
211 | func filepathClean(p string) string {
212 | return path.Clean(p)
213 | }
214 |
215 | // Require can be used to import modules from Go source (similar to JS require() function).
216 | func (r *RequireModule) Require(p string) (ret js.Value, err error) {
217 | module, err := r.resolve(p)
218 | if err != nil {
219 | return
220 | }
221 | ret = module.Get("exports")
222 | return
223 | }
224 |
225 | func (r *RequireModule) Clean() {
226 | r.modulesCache.Purge()
227 | return
228 | }
229 |
230 | func Require(runtime *js.Runtime, name string) js.Value {
231 | if r, ok := js.AssertFunction(runtime.Get("require")); ok {
232 | mod, err := r(js.Undefined(), runtime.ToValue(name))
233 | if err != nil {
234 | panic(err)
235 | }
236 | return mod
237 | }
238 | panic(runtime.NewTypeError("Please enable require for this runtime using new(require.Registry).Enable(runtime)"))
239 | }
240 |
241 | func RegisterNativeModule(name string, loader ModuleLoader) {
242 | if native == nil {
243 | native = make(map[string]ModuleLoader)
244 | }
245 | name = filepathClean(name)
246 | native[name] = loader
247 | }
248 |
--------------------------------------------------------------------------------
/test/preact/src/page/component.tsx:
--------------------------------------------------------------------------------
1 | // import {Fragment, jsx} from "react/jsx-runtime";
2 |
3 | import {ComponentType} from "preact";
4 | import {useEffect, useState} from "preact/hooks";
5 |
6 | let Text = (props) => {
7 | return
8 | {props.text}
9 |
10 | }
11 |
12 | let Col = (props) => {
13 | return
14 | {props.children}
15 |
16 | }
17 |
18 | let Row = (props) => {
19 | return
20 | {props.children}
21 |
22 | }
23 |
24 | let Button = (props) => {
25 | props = {...props}
26 | switch (props.variant) {
27 | case "ghost":
28 | props.style = {
29 | ...props.style,
30 | background: "transparent",
31 | }
32 | break
33 | default:
34 | props.style = {
35 | ...props.style,
36 | }
37 | }
38 | return
39 | {props.children ? props.children : props.text}
40 |
41 | }
42 |
43 | let ComponentX = ({variant = "primary", variants, children}) => {
44 | const [isHovered, setIsHovered] = useState(false);
45 | const [count, setCount] = useState(1);
46 | let main = {...children}
47 | let hover = variants[variant + "_hover"];
48 | // main.props = {
49 | // ...main.props,
50 | // style: {
51 | // ...main.props?.style,
52 | // // color: "red"
53 | // }
54 | // }
55 | // useEffect(()=>{
56 | // setCount(count+1)
57 | // },[isHovered])
58 | if (hover) {
59 | main.props = {
60 | ...main.props,
61 | onMouseEnter: () => {
62 | console.log('onMouseEnter')
63 | setIsHovered(true)
64 | },
65 | onMouseLeave: () => {
66 | setIsHovered(false)
67 | },
68 | style: {
69 | ...main.props?.style,
70 | // color: "red"
71 | }
72 | }
73 |
74 | if (isHovered) {
75 | // 递归修改子组件属性
76 | console.log('xxxx', main)
77 |
78 | if (hover[main.props?.id]) {
79 | main = {
80 | ...main,
81 | props: {
82 | ...main.props,
83 | ...hover[main.props?.id].props,
84 | id: Math.random()
85 | },
86 | }
87 |
88 | // console.log('after', main)
89 | // setCount(count)
90 | }
91 | }
92 | }
93 |
94 |
95 | return main
96 | }
97 |
98 | function withComponent(Component: ComponentType, {
99 | type
100 | }) {
101 | return function (props) {
102 | return
103 | }
104 | }
105 |
106 | const Components = {
107 | "row": withComponent(Row, {type: "row"}),
108 | "col": withComponent(Col, {type: "col"}),
109 | "text": withComponent(Text, {type: "text"}),
110 | "button": withComponent(Button, {type: "button"}),
111 | "component": withComponent(ComponentX, {type: "component"}),
112 | }
113 |
114 | // 以后用这个数据来生成 jsx 代码。
115 | //
116 | // nodeTree
117 | const nodeTree = [
118 | {
119 | type: "_root",
120 | props: {
121 | children: [
122 | {
123 | type: "row",
124 | props: {
125 | style: {background: "#d7ffbf"}, children: [
126 | {type: "text", id: "2334", props: {style: {}, text: "line 1"}}]
127 | }
128 | },
129 | {
130 | type: "col",
131 | props: {
132 | style: {background: "#ffa09c"},
133 | children:
134 | [
135 | {type: "text", id: "2336", props: {style: {flex: "1 1 0%"}, text: "line 2 left"}},
136 | {type: "text", id: "2337", props: {style: {flex: "1 1 0%"}, text: "line 2 right"}}
137 | ]
138 | }
139 | },
140 | {
141 | type: "text", props: {style: {background: "#ccdcff", flex: "1 1 0%"}, text: "line 3"},
142 | },
143 | {
144 | type: "button",
145 | props: {style: {background: "#6d8eff", padding: "8px 16px"}, text: "btn", variant: ""}
146 | },
147 | {
148 | type: "button",
149 | props: {style: {background: "#6d8eff", padding: "8px 16px"}, text: "ghost", variant: "ghost"}
150 | },
151 | {
152 | type: "component",
153 | props: {
154 | // style: {
155 | // background: "#94ff8f", padding: "8px 16px"
156 | // },
157 | children: {
158 | type: "col",
159 | id: "col-1",
160 | props: {
161 | style: {background: "#ffa09c", transition: "background 1s"},
162 | children:
163 | [
164 | {
165 | type: "text",
166 | id: "text-1",
167 | props: {style: {flex: "1 1 0%"}, text: "line 2 left"}
168 | },
169 | {type: "text", props: {style: {flex: "1 1 0%"}, text: "line 2 right"}}
170 | ]
171 | }
172 | }
173 | ,
174 | variants: {
175 | primary_hover: {
176 | "col-1": {
177 | props: {
178 | style: {background: "#84b1ff"}
179 | }
180 | }
181 | },
182 | },
183 | variant: "primary"
184 | }
185 | },
186 | ]
187 | }
188 | },
189 | ]
190 |
191 | //
192 |
193 | class NodeTree {
194 | private nodeIdMap: {};
195 |
196 | constructor(nodeTree) {
197 | const nodeIdMap = {}
198 | nodeTree.forEach((node) => {
199 | nodeIdMap[node.id] = node
200 | })
201 |
202 | this.nodeIdMap = nodeIdMap
203 | }
204 |
205 | public nodeToJsx(node) {
206 | const {type, id, props} = node
207 |
208 | let children = null
209 | if (props.children) {
210 | if (Array.isArray(props.children)) {
211 | children = props.children.map((node) => {
212 | return this.nodeToJsx(node)
213 | })
214 | } else {
215 | children = this.nodeToJsx(props.children)
216 | }
217 | }
218 | if (type === "_root") {
219 | return <>{children}>
220 | }
221 |
222 | // if (props.variants) {
223 | // Object.keys(props.variants).forEach((key) => {
224 | // props.variants[key] = this.nodeToJsx(props.variants[key])
225 | // })
226 | // }
227 |
228 | let C = Components[type];
229 | return
230 | }
231 | }
232 |
233 | // interface
234 | function Pages() {
235 | let nodeTree1 = new NodeTree(nodeTree);
236 | return nodeTree1.nodeToJsx(nodeTree[0])
237 | }
238 |
239 | export default function Component() {
240 | return
243 | }
244 |
245 |
--------------------------------------------------------------------------------
/pkg/mdx/mdx.go:
--------------------------------------------------------------------------------
1 | package mdx
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "fmt"
7 | "github.com/yuin/goldmark"
8 | "github.com/yuin/goldmark/ast"
9 | "github.com/yuin/goldmark/parser"
10 | "github.com/yuin/goldmark/renderer"
11 | "github.com/yuin/goldmark/renderer/html"
12 | "github.com/yuin/goldmark/text"
13 | "github.com/yuin/goldmark/util"
14 | html2jsx2 "github.com/zbysir/gojsx/pkg/html2jsx"
15 | "io"
16 | "regexp"
17 | )
18 |
19 | type MdFormat string
20 |
21 | const (
22 | Mdx MdFormat = "mdx"
23 | Md MdFormat = "md"
24 | )
25 |
26 | type mdJsx struct {
27 | format MdFormat
28 | }
29 |
30 | func NewMdJsx(format MdFormat) goldmark.Extender {
31 | return &mdJsx{format: format}
32 | }
33 |
34 | var jsxCodeKey = parser.NewContextKey()
35 |
36 | func (e *mdJsx) Extend(m goldmark.Markdown) {
37 | switch e.format {
38 | case Mdx:
39 | m.Parser().AddOptions(
40 | parser.WithBlockParsers(
41 | util.Prioritized(NewJsCodeParser(), 0),
42 | util.Prioritized(NewJsxParser(), 10),
43 | ),
44 | parser.WithInlineParsers(util.Prioritized(NewJsxParser(), 0)),
45 | )
46 | m.Renderer().AddOptions(renderer.WithNodeRenderers(
47 | util.Prioritized(&JsxRender{}, 10),
48 | ))
49 | }
50 |
51 | m.Renderer().AddOptions(
52 | html.WithUnsafe(),
53 | html.WithXHTML(),
54 | html.WithWriter(&wrapWriter{Writer: html.DefaultWriter, enableJsx: e.format == Mdx}),
55 | )
56 |
57 | parse := &WrapParser{Parser: m.Parser()}
58 | m.SetParser(parse)
59 | m.SetRenderer(WrapRender{Renderer: m.Renderer(), enableJsx: e.format == Mdx, parser: parse})
60 | }
61 |
62 | type wrapWriter struct {
63 | html.Writer
64 | enableJsx bool
65 | }
66 |
67 | func encodeJsxInsecure(s []byte) []byte {
68 | s = bytes.ReplaceAll(s, []byte("{"), []byte("{"))
69 | s = bytes.ReplaceAll(s, []byte("}"), []byte("}"))
70 | return s
71 | }
72 |
73 | func (w *wrapWriter) Write(writer util.BufWriter, source []byte) {
74 | if !w.enableJsx {
75 | // 解决纯文本中 {} 符号引起的语法错误
76 | buffer := bytes.NewBuffer(nil)
77 | wr := bufio.NewWriter(buffer)
78 | w.Writer.Write(wr, source)
79 | wr.Flush()
80 | writer.Write(encodeJsxInsecure(buffer.Bytes()))
81 | } else {
82 | w.Writer.Write(writer, source)
83 | }
84 | }
85 |
86 | type WrapParser struct {
87 | parser.Parser
88 | ctx parser.Context
89 | }
90 |
91 | func (p *WrapParser) GetContext() parser.Context {
92 | return p.ctx
93 | }
94 |
95 | func (p *WrapParser) Parse(reader text.Reader, opts ...parser.ParseOption) ast.Node {
96 | var c parser.ParseConfig
97 | for _, o := range opts {
98 | o(&c)
99 | }
100 | if c.Context == nil {
101 | c.Context = parser.NewContext()
102 | opts = append(opts, parser.WithContext(c.Context))
103 | }
104 | p.ctx = c.Context
105 |
106 | return p.Parser.Parse(reader, opts...)
107 | }
108 |
109 | type WrapRender struct {
110 | renderer.Renderer
111 | enableJsx bool
112 | parser *WrapParser
113 | }
114 |
115 | func (r WrapRender) Render(w io.Writer, source []byte, n ast.Node) error {
116 | var buf bytes.Buffer
117 | err := r.Renderer.Render(&buf, source, n)
118 | if err != nil {
119 | return err
120 | }
121 |
122 | var out bytes.Buffer
123 |
124 | err = html2jsx2.Convert(&buf, &out, r.enableJsx)
125 | if err != nil {
126 | return err
127 | }
128 |
129 | outbs := out.Bytes()
130 |
131 | ctx := r.parser.GetContext()
132 | if ctx != nil {
133 | ts := GetJsxCode(ctx)
134 | if ts != nil {
135 | for i := 0; i < ts.Len(); i++ {
136 | s := ts.At(i)
137 | outbs = bytes.ReplaceAll(outbs, jsxNodePlaceholder(i), s.Value(source))
138 | }
139 | }
140 | }
141 |
142 | w.Write(outbs)
143 | return nil
144 | }
145 |
146 | type JsxRender struct {
147 | }
148 |
149 | func (j *JsxRender) renderJsxBlock(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
150 | if !entering {
151 | return ast.WalkContinue, nil
152 | }
153 | jsxNode := node.(*JsxNode)
154 | w.Write(jsxNodePlaceholder(jsxNode.index))
155 | return ast.WalkContinue, nil
156 | }
157 |
158 | func jsxNodePlaceholder(index int) []byte {
159 | return []byte(fmt.Sprintf("JSXNODE_%d_EDONXSJ", index))
160 | }
161 |
162 | func (j *JsxRender) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
163 | reg.Register(jsxKind, j.renderJsxBlock)
164 | }
165 |
166 | var jsxKind = ast.NewNodeKind("Jsx")
167 |
168 | type JsxNode struct {
169 | ast.BaseBlock
170 | tag string
171 | index int // 几个
172 | }
173 |
174 | func (j *JsxNode) Kind() ast.NodeKind {
175 | return jsxKind
176 | }
177 |
178 | // IsRaw return true 不解析 block 中的内容
179 | func (j *JsxNode) IsRaw() bool {
180 | return true
181 | }
182 |
183 | func (j *JsxNode) Dump(source []byte, level int) {
184 | ast.DumpHelper(j, source, level, nil, nil)
185 | }
186 |
187 | func (j *JsxNode) HasBlankPreviousLines() bool {
188 | return true
189 | }
190 |
191 | func (j *JsxNode) SetBlankPreviousLines(v bool) {
192 | return
193 | }
194 |
195 | type jsxParser struct {
196 | }
197 |
198 | func NewJsxParser() parser.BlockParser {
199 | return &jsxParser{}
200 | }
201 | func (j *jsxParser) saveJsxNode(node *JsxNode, segment text.Segment, pc parser.Context) {
202 | jsxs := GetJsxCode(pc)
203 | if jsxs == nil {
204 | jsxs = text.NewSegments()
205 | }
206 | node.index = jsxs.Len()
207 | jsxs.Append(segment)
208 | pc.Set(jsxCodeKey, jsxs)
209 | }
210 |
211 | func (j *jsxParser) Parse(parent ast.Node, reader text.Reader, pc parser.Context) ast.Node {
212 | line, segment := reader.PeekLine()
213 |
214 | match := jsxTagStartReg.FindAllSubmatch(line, -1)
215 | if match == nil {
216 | return nil
217 | }
218 | bs := reader.Value(segment)
219 |
220 | start, end, ok, err := parseTagToClose(bytes.NewBuffer(bs))
221 | if err != nil {
222 | return nil
223 | }
224 | if !ok {
225 | return nil
226 | }
227 |
228 | node := &JsxNode{
229 | BaseBlock: ast.BaseBlock{},
230 | }
231 |
232 | segment = text.NewSegment(start+segment.Start, end+segment.Start)
233 |
234 | node.Lines().Append(segment)
235 | reader.Advance(segment.Len())
236 |
237 | j.saveJsxNode(node, segment, pc)
238 | return node
239 | }
240 |
241 | var _ parser.BlockParser = (*jsxParser)(nil)
242 | var _ parser.InlineParser = (*jsxParser)(nil)
243 |
244 | func (j *jsxParser) Trigger() []byte {
245 | return []byte{'<'}
246 | }
247 |
248 | // 匹配 or <>
249 | var jsxTagStartReg = regexp.MustCompile(`^ {0,3}<(([A-Z]+[a-zA-Z0-9\-]*)|>)`)
250 |
251 | func (j *jsxParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) {
252 | node := &JsxNode{
253 | BaseBlock: ast.BaseBlock{},
254 | }
255 | line, segment := reader.PeekLine()
256 | if pos := pc.BlockOffset(); pos < 0 || line[pos] != '<' {
257 | return nil, parser.NoChildren
258 | }
259 | match := jsxTagStartReg.FindAllSubmatch(line, -1)
260 | if match == nil {
261 | return nil, parser.NoChildren
262 | }
263 |
264 | _, s := reader.Position()
265 | offset := s.Start
266 | bs := reader.Source()[offset:]
267 |
268 | start, end, ok, err := parseTagToClose(bytes.NewBuffer(bs))
269 | if err != nil {
270 | return nil, parser.NoChildren
271 | }
272 | if !ok {
273 | return nil, parser.NoChildren
274 | }
275 |
276 | tagName := string(match[0][2])
277 | node.tag = tagName
278 | code := GetJsCode(pc)
279 |
280 | if tagName != "" {
281 | // 简单判断 变量是否存在于 code,如果存在则说明是 JsxElement
282 | tr := regexp.MustCompile(fmt.Sprintf(`\b%s\b`, tagName))
283 | if !tr.MatchString(code) {
284 | return nil, parser.NoChildren
285 | }
286 | }
287 |
288 | segment = text.NewSegment(start+offset, end+offset)
289 |
290 | jsxs := GetJsxCode(pc)
291 | if jsxs == nil {
292 | jsxs = text.NewSegments()
293 | }
294 | node.index = jsxs.Len()
295 | jsxs.Append(segment)
296 | pc.Set(jsxCodeKey, jsxs)
297 |
298 | node.Lines().Append(segment)
299 | reader.Advance(segment.Len() - 1)
300 | return node, parser.Close
301 |
302 | }
303 |
304 | func (j *jsxParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State {
305 | return parser.Close
306 | }
307 |
308 | func (j *jsxParser) Close(node ast.Node, reader text.Reader, pc parser.Context) {
309 | }
310 |
311 | func GetJsxCode(pc parser.Context) *text.Segments {
312 | i := pc.Get(jsxCodeKey)
313 | if i != nil {
314 | jsxs := i.(*text.Segments)
315 | return jsxs
316 | }
317 |
318 | return nil
319 | }
320 |
321 | func (j *jsxParser) CanInterruptParagraph() bool {
322 | return true
323 | }
324 |
325 | func (j *jsxParser) CanAcceptIndentedLine() bool {
326 | return true
327 | }
328 |
--------------------------------------------------------------------------------
/transform.go:
--------------------------------------------------------------------------------
1 | package gojsx
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "encoding/json"
7 | "fmt"
8 | "github.com/evanw/esbuild/pkg/api"
9 | "github.com/go-sourcemap/sourcemap"
10 | "github.com/yuin/goldmark"
11 | "github.com/yuin/goldmark-meta"
12 | "github.com/yuin/goldmark/ast"
13 | "github.com/yuin/goldmark/extension"
14 | "github.com/yuin/goldmark/parser"
15 | "github.com/yuin/goldmark/text"
16 | "github.com/zbysir/gojsx/pkg/mdx"
17 | "go.abhg.dev/goldmark/mermaid"
18 | "log"
19 | "path/filepath"
20 | "strings"
21 | )
22 |
23 | type Transformer interface {
24 | Transform(filePath string, src []byte, format TransformerFormat) (out []byte, err error)
25 | }
26 |
27 | type TransformerFormat uint8
28 |
29 | const (
30 | TransformerNone TransformerFormat = 0
31 | TransformerFormatDefault TransformerFormat = 1
32 | TransformerFormatIIFE TransformerFormat = 2
33 | TransformerFormatCommonJS TransformerFormat = 3
34 | TransformerFormatESModule TransformerFormat = 4
35 | )
36 |
37 | type EsBuildTransform struct {
38 | debug bool
39 | minify bool
40 | markdownOptions []goldmark.Option
41 | markdownExport func(ctx parser.Context, n ast.Node, src []byte) map[string]interface{}
42 | }
43 |
44 | type EsBuildTransformOptions struct {
45 | Minify bool
46 | MarkdownOptions []goldmark.Option
47 | }
48 |
49 | func NewEsBuildTransform(o EsBuildTransformOptions) *EsBuildTransform {
50 | return &EsBuildTransform{
51 | minify: o.Minify,
52 | markdownOptions: o.MarkdownOptions,
53 | }
54 | }
55 |
56 | var defaultExtensionToLoaderMap = map[string]api.Loader{
57 | "": api.LoaderJS, // default
58 | ".js": api.LoaderJS,
59 | ".mjs": api.LoaderJS,
60 | ".cjs": api.LoaderJS,
61 | ".jsx": api.LoaderJSX,
62 | ".ts": api.LoaderTS,
63 | ".tsx": api.LoaderTSX,
64 | ".css": api.LoaderCSS,
65 | ".json": api.LoaderJSON,
66 | ".txt": api.LoaderText,
67 | }
68 |
69 | func trapBOM(fileBytes []byte) []byte {
70 | trimmedBytes := bytes.Trim(fileBytes, "\xef\xbb\xbf")
71 | return trimmedBytes
72 | }
73 |
74 | // TODO SourceMap
75 | // 将 md 转换成 jsx 语法
76 | func (e *EsBuildTransform) transformMarkdown(ext string, src []byte) (out []byte, err error) {
77 | // 将 md 处理成 xhtml
78 | var mdHtml bytes.Buffer
79 | ctx := parser.NewContext()
80 | opts := []goldmark.Option{
81 | goldmark.WithExtensions(
82 | meta.Meta,
83 | extension.GFM,
84 | &mermaid.Extender{
85 | RenderMode: mermaid.RenderModeClient,
86 | MermaidJS: "https://unpkg.com/mermaid@9/dist/mermaid.min.js",
87 | NoScript: false,
88 | MMDC: nil,
89 | Theme: "",
90 | },
91 | ),
92 | goldmark.WithParserOptions(
93 | parser.WithAutoHeadingID(),
94 | // https://github.com/mdx-js/mdx/issues/1279
95 | parser.WithHeadingAttribute(), // handles special case like ### heading ### {#id}
96 | ),
97 | }
98 | switch ext {
99 | case ".mdx":
100 | opts = append(opts, goldmark.WithExtensions(
101 | mdx.NewMdJsx("mdx"),
102 | ))
103 | case ".md":
104 | opts = append(opts, goldmark.WithExtensions(
105 | mdx.NewMdJsx("md"),
106 | ))
107 | }
108 |
109 | opts = append(opts, e.markdownOptions...)
110 | md := goldmark.New(opts...)
111 |
112 | doc := md.Parser().Parse(text.NewReader(src), parser.WithContext(ctx))
113 |
114 | if e.debug {
115 | doc.Dump(src, 1)
116 | }
117 |
118 | err = md.Renderer().Render(&mdHtml, src, doc)
119 | if err != nil {
120 | return
121 | }
122 |
123 | m := meta.Get(ctx)
124 | jsCode := mdx.GetJsCode(ctx)
125 |
126 | var code bytes.Buffer
127 | code.WriteString(jsCode)
128 | code.WriteString(";\n")
129 |
130 | exportObj := map[string]interface{}{
131 | "meta": toStrMap(m),
132 | }
133 |
134 | if e.markdownExport != nil {
135 | export := e.markdownExport(ctx, doc, src)
136 | for k, v := range export {
137 | exportObj[k] = v
138 | }
139 | }
140 |
141 | for k, v := range exportObj {
142 | code.WriteString(fmt.Sprintf("export let %s = ", k))
143 | bs, _ := json.Marshal(v)
144 | code.Write(bs)
145 | code.WriteString(";\n")
146 | }
147 |
148 | // write jsx
149 | code.WriteString("export default (props)=> <>")
150 | mdHtml.WriteTo(&code)
151 | code.WriteString(">")
152 |
153 | return code.Bytes(), nil
154 | }
155 |
156 | // toStrMap gopkg.in/yaml.v2 会解析出 map[interface{}]interface{} 这样的结构,不支持 json 序列化。需要手动转一次
157 | func toStrMap(i interface{}) interface{} {
158 | switch t := i.(type) {
159 | case map[string]interface{}:
160 | m := map[string]interface{}{}
161 | for k, v := range t {
162 | m[k] = toStrMap(v)
163 | }
164 | return m
165 | case map[interface{}]interface{}:
166 | m := map[string]interface{}{}
167 | for k, v := range t {
168 | m[k.(string)] = toStrMap(v)
169 | }
170 | return m
171 | case []interface{}:
172 | m := make([]interface{}, len(t))
173 | for i, v := range t {
174 | m[i] = toStrMap(v)
175 | }
176 | return m
177 | default:
178 | return i
179 | }
180 | }
181 |
182 | func (e *EsBuildTransform) Transform(filePath string, code []byte, format TransformerFormat) (out []byte, err error) {
183 | code = trapBOM(code)
184 |
185 | var esFormat api.Format
186 | var globalName string
187 | switch format {
188 | case TransformerNone:
189 | return code, nil
190 | case TransformerFormatDefault:
191 | esFormat = api.FormatDefault
192 | case TransformerFormatIIFE:
193 | // 如果是 IIFE 格式,则始终将结果导出
194 | esFormat = api.FormatIIFE
195 | globalName = "__export__"
196 | case TransformerFormatCommonJS:
197 | esFormat = api.FormatCommonJS
198 | case TransformerFormatESModule:
199 | esFormat = api.FormatESModule
200 | default:
201 | return code, nil
202 | }
203 |
204 | _, file := filepath.Split(filePath)
205 | ext := filepath.Ext(filePath)
206 |
207 | var loader api.Loader
208 | switch ext {
209 | case ".md", ".mdx":
210 | code, err = e.transformMarkdown(ext, code)
211 | if err != nil {
212 | return
213 | }
214 | loader = api.LoaderTSX
215 | if e.debug {
216 | log.Printf("transformMarkdown code: %s", code)
217 | }
218 |
219 | default:
220 | var ok bool
221 | loader, ok = defaultExtensionToLoaderMap[ext]
222 | if !ok {
223 | return nil, fmt.Errorf("unsupport file extension(%s) for transform", ext)
224 | }
225 | }
226 |
227 | var sourcemapx api.SourceMap
228 | switch ext {
229 | case ".jsx", ".tsx", ".mdx", ".md", ".js", ".ts", ".mjs", ".cjs":
230 | sourcemapx = api.SourceMapInline
231 | default:
232 | // .json 不生成 sourcemap,因为会 esbuild 生成空 sourcemap,但 goja 执行空 sourcemap 会报错。
233 | sourcemapx = api.SourceMapNone
234 | }
235 |
236 | result := api.Transform(string(code), api.TransformOptions{
237 | Color: 0,
238 | LogLevel: 0,
239 | LogLimit: 0,
240 | LogOverride: nil,
241 | Sourcemap: sourcemapx,
242 | SourceRoot: "",
243 | SourcesContent: 0,
244 | Target: api.ESNext,
245 | Engines: nil,
246 | Supported: nil,
247 | Platform: api.PlatformNode,
248 | Format: esFormat,
249 | GlobalName: globalName,
250 | MangleProps: "",
251 | ReserveProps: "",
252 | MangleQuoted: 0,
253 | MangleCache: nil,
254 | Drop: 0,
255 | DropLabels: nil,
256 | MinifyWhitespace: e.minify,
257 | MinifyIdentifiers: e.minify,
258 | MinifySyntax: e.minify,
259 | LineLimit: 0,
260 | Charset: 0,
261 | TreeShaking: 0,
262 | IgnoreAnnotations: false,
263 | LegalComments: 0,
264 | JSX: api.JSXAutomatic,
265 | JSXFactory: "h",
266 | JSXFragment: "b",
267 | JSXImportSource: "react",
268 | JSXDev: false,
269 | JSXSideEffects: false,
270 | TsconfigRaw: "",
271 | Banner: "",
272 | Footer: globalName,
273 | Define: nil,
274 | Pure: nil,
275 | KeepNames: false,
276 | Sourcefile: file,
277 | Loader: loader,
278 | })
279 |
280 | if len(result.Errors) != 0 {
281 | er := result.Errors[0]
282 | if er.Location != nil {
283 | location := e.trySourcemapLocation(er.Location, code)
284 | err = fmt.Errorf("%v: (%v:%v) \n%v\n%v^ %v\n", filePath, location.Line, location.Column, location.LineText, strings.Repeat(" ", location.Column), er.Text)
285 | } else {
286 | err = fmt.Errorf("%v\n", er.Text)
287 | }
288 | return
289 | }
290 |
291 | code = result.Code
292 | return code, nil
293 | }
294 |
295 | // 将 esbuild 报错位置信息通过 sourcemap 转换
296 | func (e *EsBuildTransform) trySourcemapLocation(l *api.Location, source []byte) *api.Location {
297 | sms := bytes.Split(source, []byte(`sourceMappingURL=data:application/json;base64,`))
298 | if len(sms) != 2 {
299 | return l
300 | }
301 |
302 | sourcemapJson, _ := base64.URLEncoding.DecodeString(string(sms[1]))
303 | if sourcemapJson == nil {
304 | return l
305 | }
306 |
307 | c, err := sourcemap.Parse("./", sourcemapJson)
308 | if err != nil {
309 | return l
310 | }
311 |
312 | file, _, line, column, ok := c.Source(l.Line, l.Column)
313 | if !ok {
314 | return l
315 | }
316 |
317 | return &api.Location{
318 | File: file,
319 | Namespace: l.Namespace,
320 | Line: line,
321 | Column: column,
322 | Length: l.Length,
323 | LineText: sourceLine(c.SourceContent(file), line),
324 | Suggestion: l.Suggestion,
325 | }
326 | }
327 |
328 | func sourceLine(s string, i int) string {
329 | return strings.SplitN(s, "\n", i)[i-1]
330 | }
331 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
2 | github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
3 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
4 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
9 | github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
10 | github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
11 | github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
12 | github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 h1:O7I1iuzEA7SG+dK8ocOBSlYAA9jBUmCYl/Qa7ey7JAM=
13 | github.com/dop251/goja v0.0.0-20240220182346-e401ed450204/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
14 | github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
15 | github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
16 | github.com/evanw/esbuild v0.20.2 h1:E4Y0iJsothpUCq7y0D+ERfqpJmPWrZpNybJA3x3I4p8=
17 | github.com/evanw/esbuild v0.20.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
18 | github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
19 | github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
20 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
21 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
22 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
23 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
24 | github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4=
25 | github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
26 | github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
27 | github.com/jolestar/go-commons-pool/v2 v2.1.2 h1:E+XGo58F23t7HtZiC/W6jzO2Ux2IccSH/yx4nD+J1CM=
28 | github.com/jolestar/go-commons-pool/v2 v2.1.2/go.mod h1:r4NYccrkS5UqP1YQI1COyTZ9UjPJAAGTUxzcsK1kqhY=
29 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
30 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
31 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
32 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
33 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
34 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
35 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
36 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
37 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
39 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
40 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
41 | github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
42 | github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
43 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
44 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
45 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
46 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
47 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
48 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
49 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
50 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
51 | github.com/tdewolff/parse/v2 v2.6.5 h1:lYvWBk55GkqKl0JJenGpmrgu/cPHQQ6/Mm1hBGswoGQ=
52 | github.com/tdewolff/parse/v2 v2.6.5/go.mod h1:woz0cgbLwFdtbjJu8PIKxhW05KplTFQkOdX78o+Jgrs=
53 | github.com/tdewolff/test v1.0.7 h1:8Vs0142DmPFW/bQeHRP3MV19m1gvndjUb1sn8yy74LM=
54 | github.com/tdewolff/test v1.0.7/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
55 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
56 | github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M=
57 | github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
58 | github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
59 | github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
60 | go.abhg.dev/goldmark/mermaid v0.4.0 h1:yqwaYvNFbKwh9JByKzN0eDpxrUoYe8NUJvtSKY94S5M=
61 | go.abhg.dev/goldmark/mermaid v0.4.0/go.mod h1:L5SiQ7PedPuZY0+zaPoJ5ZnDitIYS0Obi5w7Pf02tPY=
62 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
63 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
64 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
65 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
66 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
67 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
68 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
69 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
70 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
71 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
72 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
73 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
74 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
75 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
76 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
77 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
78 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
79 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
80 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
81 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
82 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
83 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
84 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
85 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
86 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
87 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
88 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
89 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
90 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
91 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
92 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
93 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
94 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
95 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
96 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
97 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
98 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
99 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
100 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
101 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
102 | rogchap.com/v8go v0.9.0 h1:wYbUCO4h6fjTamziHrzyrPnpFNuzPpjZY+nfmZjNaew=
103 | rogchap.com/v8go v0.9.0/go.mod h1:MxgP3pL2MW4dpme/72QRs8sgNMmM0pRc8DPhcuLWPAs=
104 |
--------------------------------------------------------------------------------
/jsx_test.go:
--------------------------------------------------------------------------------
1 | package gojsx
2 |
3 | import (
4 | "embed"
5 | "fmt"
6 | "net/http"
7 | "strings"
8 | "sync"
9 | "testing"
10 | "time"
11 |
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | //go:embed test
16 | var srcfs embed.FS
17 |
18 | func TestJsx(t *testing.T) {
19 | j, err := NewJsx(Option{
20 | SourceCache: nil,
21 | Fs: srcfs,
22 | Debug: false,
23 | })
24 | if err != nil {
25 | t.Fatal(err)
26 | }
27 |
28 | j.RegisterModule("react", map[string]interface{}{
29 | "useEffect": func() {},
30 | })
31 |
32 | s, err := j.Render("./test/Index", map[string]interface{}{"li": []int64{1, 2, 3, 4}, "html": `dangerouslySetInnerHTML `})
33 | if err != nil {
34 | t.Fatal(err)
35 | }
36 |
37 | assert.Equal(t, "UnTitled
a /2
<h1>dangerouslySetInnerHTML</h1>
dangerouslySetInnerHTML ", s)
38 | }
39 |
40 | //go:embed test/blog/tailwind.css
41 | var tailwind []byte
42 |
43 | func TestHttp(t *testing.T) {
44 | j, err := NewJsx(Option{
45 | SourceCache: NewMemSourceCache(),
46 | Debug: true,
47 | VmMaxTotal: 10,
48 | })
49 | if err != nil {
50 | t.Fatal(err)
51 | }
52 |
53 | err = http.ListenAndServe(":8082", http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
54 | //j.RefreshRegistry()
55 |
56 | pageData := map[string]interface{}{}
57 | page := ""
58 | switch request.URL.Path {
59 | case "/tailwind.css":
60 | writer.Header().Set("Content-Type", "text/css")
61 | writer.Write(tailwind)
62 | return
63 | case "/", "":
64 | page = "home"
65 | pageData = map[string]interface{}{
66 | "blogs": []interface{}{
67 | map[string]interface{}{
68 | "name": "如何渲染 jsx",
69 | },
70 | map[string]interface{}{
71 | "name": "关于我",
72 | },
73 | },
74 | }
75 | case "/detail":
76 | page = "blog-detail"
77 | pageData = map[string]interface{}{
78 | "title": "如何渲染 jsx",
79 | "html": "html",
80 | }
81 | default:
82 | page = request.URL.Path
83 | }
84 | ti := time.Now()
85 | s, err := j.Render("./test/blog/Index",
86 | map[string]interface{}{
87 | "a": 1,
88 | "title": "bysir' blog",
89 | "me": "bysir",
90 | "page": page,
91 | "pageData": pageData,
92 | "time": "",
93 | })
94 | if err != nil {
95 | t.Fatal(err)
96 | }
97 | t.Logf("t: %v", time.Now().Sub(ti))
98 | s += time.Now().Sub(ti).String()
99 | writer.Write([]byte(s))
100 | }))
101 | if err != nil {
102 | t.Fatal(err)
103 | }
104 | }
105 |
106 | // cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
107 | // 71415 ns/op
108 | func BenchmarkJsx(b *testing.B) {
109 | j, err := NewJsx(Option{})
110 | if err != nil {
111 | b.Fatal(err)
112 | }
113 |
114 | // render first to enable cache
115 | _, err = j.Render("./test/Index", map[string]interface{}{"a": 1})
116 |
117 | b.ResetTimer()
118 | for i := 0; i < b.N; i++ {
119 | _, err := j.Render("./test/Index", map[string]interface{}{"a": 1}, WithCache(true))
120 | if err != nil {
121 | b.Fatal(err)
122 | }
123 | }
124 | }
125 |
126 | func TestP(t *testing.T) {
127 | j, err := NewJsx(Option{})
128 | if err != nil {
129 | t.Fatal(err)
130 | }
131 | var wg sync.WaitGroup
132 | for i := 0; i < 1000; i++ {
133 | wg.Add(1)
134 | go func() {
135 | defer wg.Done()
136 |
137 | _, err := j.Render("./test/Index", map[string]interface{}{"a": 1})
138 | if err != nil {
139 | t.Fatal(err)
140 | }
141 | }()
142 | }
143 |
144 | wg.Wait()
145 | }
146 |
147 | func TestRunJs(t *testing.T) {
148 | j, err := NewJsx(Option{})
149 | if err != nil {
150 | t.Fatal(err)
151 | }
152 | v, err := j.ExecCode([]byte(`function HelloJSX(){return 123
}; export default `), WithFileName("1.tsx"))
153 | if err != nil {
154 | t.Fatal(err)
155 | }
156 |
157 | t.Logf("%+v", v)
158 | }
159 |
160 | func TestRunMd(t *testing.T) {
161 | j, err := NewJsx(Option{})
162 | if err != nil {
163 | t.Fatal(err)
164 | }
165 | v, err := j.ExecCode([]byte(`## h2`), WithFileName("1.md"))
166 | if err != nil {
167 | t.Fatal(err)
168 | }
169 |
170 | t.Logf("%+v", v)
171 | }
172 |
173 | func TestRenderMd(t *testing.T) {
174 | j, err := NewJsx(Option{})
175 | if err != nil {
176 | t.Fatal(err)
177 | }
178 | n, err := j.Render("./test/md.md", map[string]interface{}{"a": 1})
179 | if err != nil {
180 | t.Fatal(err)
181 | }
182 |
183 | t.Logf("%v", n)
184 | }
185 |
186 | func TestRenderMdx(t *testing.T) {
187 | j, err := NewJsx(Option{})
188 | if err != nil {
189 | t.Fatal(err)
190 | }
191 | n, err := j.Render("./test/mdx.mdx", map[string]interface{}{"a": 1})
192 | if err != nil {
193 | t.Fatal(err)
194 | }
195 |
196 | t.Logf("%v", n)
197 | }
198 |
199 | func TestExec(t *testing.T) {
200 | j, err := NewJsx(Option{})
201 | if err != nil {
202 | t.Fatal(err)
203 | }
204 | n, err := j.Exec("./test/md.md")
205 | if err != nil {
206 | t.Fatal(err)
207 | }
208 |
209 | v, _ := n.Default.(Callable)(nil, nil)
210 | vd, _ := tryToVDom(v.Export())
211 | t.Logf("%+v", vd)
212 | t.Logf("%+v", n.Exports)
213 |
214 | n, err = j.Exec("./test/md.md", WithAutoExecJsx(nil))
215 | if err != nil {
216 | t.Fatal(err)
217 | }
218 |
219 | t.Logf("%+v", n.Default.(VDom))
220 | }
221 |
222 | func TestExecJson(t *testing.T) {
223 | j, err := NewJsx(Option{})
224 | if err != nil {
225 | t.Fatal(err)
226 | }
227 | n, err := j.Exec("./test/a.json")
228 | if err != nil {
229 | t.Fatal(err)
230 | }
231 | t.Logf("%+v", n.Exports)
232 | }
233 |
234 | type Model struct {
235 | ID uint
236 | }
237 |
238 | func TestEmbeddingStruct(t *testing.T) {
239 | j, err := NewJsx(Option{})
240 | if err != nil {
241 | t.Fatal(err)
242 | }
243 |
244 | props := struct {
245 | Model
246 | Name string `json:"name"`
247 | Age int
248 | FullName string
249 | }{
250 | Model{ID: 233},
251 | "abc",
252 | 23,
253 | "bysir",
254 | }
255 |
256 | v, _, err := j.RenderCode([]byte(`export default (props)=>{props.iD +' ' + props.name + ' '+ props.fullName + ' ' + props.age}
`), props, WithFileName("1.tsx"))
257 | if err != nil {
258 | t.Fatal(err)
259 | }
260 |
261 | assert.Equal(t, `233 abc bysir 23
`, v)
262 | }
263 |
264 | func TestHydrate(t *testing.T) {
265 | j, err := NewJsx(Option{})
266 | if err != nil {
267 | t.Fatal(err)
268 | }
269 |
270 | v, ctx, err := j.RenderCode([]byte(`function HelloJSX(props){return alert(props.a)} hydrate-a={JSON.stringify(props.a)}>
}; export default (props)=> `), map[string]interface{}{
271 | "a": map[string]interface{}{"name": "1"},
272 | }, WithFileName("1.tsx"))
273 | if err != nil {
274 | t.Fatal(err)
275 | }
276 |
277 | assert.Equal(t, `
`, v)
278 | assert.Equal(t, map[string]map[string]string{
279 | "0": {
280 | "hydrate-a": `{"name":"1"}`,
281 | },
282 | }, ctx.Hydrate)
283 | }
284 |
285 | func TestRenderAttributes(t *testing.T) {
286 | var s strings.Builder
287 | renderAttributes(&s, &RenderCtx{}, map[string]interface{}{
288 | "tabIndex": 1,
289 | "autoFocus": "true",
290 | "default": true,
291 | "disabled": 1,
292 | "async": false,
293 | "data-abc": "abc",
294 | "data-empty": "",
295 | "data-bool": false,
296 | "muted": false,
297 | "checked": true,
298 | "selected": true,
299 | "multiple": true,
300 | })
301 |
302 | assert.Equal(t, ` autofocus checked data-abc="abc" data-bool="false" default disabled multiple selected tabIndex="1"`, s.String())
303 | }
304 |
305 | func TestRenderStyle(t *testing.T) {
306 | var s strings.Builder
307 | renderStyle(&s, map[string]interface{}{
308 | "color": "#fff",
309 | "--color": "#EEE",
310 | "---color": "#EEE",
311 | "a-color": "#EEE",
312 | "fontWidth": "100",
313 | })
314 |
315 | assert.Equal(t, "---color: #EEE; --color: #EEE; a-color: #EEE; color: #fff; font-width: 100;", s.String())
316 | }
317 |
318 | func TestHyphenateStyleName(t *testing.T) {
319 | cases := map[string]string{
320 | "fontWidth": "font-width",
321 | "FontWidth": "font-width",
322 | "color": "color",
323 | "Color": "color",
324 | "--color": "--color",
325 | "MozTransition": "-moz-transition",
326 | "msTransition": "-ms-transition",
327 | }
328 |
329 | for in, out := range cases {
330 | assert.Equal(t, out, hyphenateStyleName(in))
331 | }
332 | }
333 |
334 | func TestRenderDangerouslyHtml(t *testing.T) {
335 | j, err := NewJsx(Option{
336 | SourceCache: nil,
337 | Fs: srcfs,
338 | Debug: false,
339 | })
340 | if err != nil {
341 | t.Fatal(err)
342 | }
343 |
344 | j.RegisterModule("react", map[string]interface{}{
345 | "useEffect": func() {},
346 | })
347 |
348 | s, _, err := j.RenderCode([]byte(`
349 | export default function Index(props) {
350 | return <>
351 | {props.html}
352 |
353 | {{"__dangerousHTML": props.html}}
354 | >
355 | }
356 | `), map[string]interface{}{"html": `dangerouslySetInnerHTML `, "object": map[string]any{"_": "test"}})
357 | if err != nil {
358 | t.Fatal(err)
359 | }
360 |
361 | assert.Equal(t, "<h1>dangerouslySetInnerHTML</h1>dangerouslySetInnerHTML ", s)
362 | }
363 |
364 | // TestRenderCondition 测试 || && 这样的断路语法
365 | // 注意,在 react 中,null, undefined, false, true 都不会显示渲染出 dom,0 会渲染出 0
366 | func TestRenderCondition(t *testing.T) {
367 | j, err := NewJsx(Option{
368 | SourceCache: nil,
369 | Fs: srcfs,
370 | Debug: false,
371 | })
372 | if err != nil {
373 | t.Fatal(err)
374 | }
375 |
376 | j.RegisterModule("react", map[string]interface{}{
377 | "useEffect": func() {},
378 | })
379 |
380 | cases := []struct {
381 | name string
382 | want string
383 | code string
384 | }{
385 | {
386 | name: "first name",
387 | want: "first name",
388 | code: "{props.first || props.second}",
389 | },
390 | {
391 | name: "has second",
392 | want: " has second ",
393 | code: "{props.second && has second }",
394 | },
395 | {
396 | name: "nothing false &&",
397 | want: "",
398 | code: "{props.ffalse && has ffalse }",
399 | },
400 | {
401 | name: "nothing false",
402 | want: "",
403 | code: "{props.ffalse}",
404 | },
405 | {
406 | name: "nothing true",
407 | want: "",
408 | code: "{props.tture}",
409 | },
410 | {
411 | name: "zeroFloat",
412 | want: "0",
413 | code: "{props.zeroFloat}",
414 | },
415 | }
416 | props := map[string]interface{}{"first": `first name`, "second": true, "ffalse": false, "tture": true, "zeroFloat": 0.0}
417 |
418 | for _, c := range cases {
419 | t.Run(c.name, func(t *testing.T) {
420 | s, _, err := j.RenderCode([]byte(fmt.Sprintf(`
421 | export default function Index(props) {
422 | return <>%s>
423 | }
424 | `, c.code)), props)
425 | if err != nil {
426 | t.Fatal(err)
427 | }
428 |
429 | assert.Equal(t, c.want, s)
430 | })
431 | }
432 |
433 | }
434 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2016 Paulo Pires
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/internal/pkg/goja_nodejs/require/module_test.go:
--------------------------------------------------------------------------------
1 | package require
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "os"
9 | "path"
10 | "testing"
11 |
12 | js "github.com/dop251/goja"
13 | )
14 |
15 | func mapFileSystemSourceLoader(files map[string]string) SourceLoader {
16 | return func(path string) ([]byte, error) {
17 | s, ok := files[path]
18 | if !ok {
19 | return nil, ModuleFileDoesNotExistError
20 | }
21 | return []byte(s), nil
22 | }
23 | }
24 |
25 | func TestRequireNativeModule(t *testing.T) {
26 | const SCRIPT = `
27 | var m = require("test/m");
28 | m.test();
29 | `
30 |
31 | vm := js.New()
32 |
33 | registry := new(Registry)
34 | registry.Enable(vm)
35 |
36 | RegisterNativeModule("test/m", func(runtime *js.Runtime, module *js.Object) {
37 | o := module.Get("exports").(*js.Object)
38 | o.Set("test", func(call js.FunctionCall) js.Value {
39 | return runtime.ToValue("passed")
40 | })
41 | })
42 |
43 | v, err := vm.RunString(SCRIPT)
44 | if err != nil {
45 | t.Fatal(err)
46 | }
47 |
48 | if !v.StrictEquals(vm.ToValue("passed")) {
49 | t.Fatalf("Unexpected result: %v", v)
50 | }
51 | }
52 |
53 | func TestRequireRegistryNativeModule(t *testing.T) {
54 | const SCRIPT = `
55 | var log = require("test/log");
56 | log.print('passed');
57 | `
58 |
59 | logWithOutput := func(w io.Writer, prefix string) ModuleLoader {
60 | return func(vm *js.Runtime, module *js.Object) {
61 | o := module.Get("exports").(*js.Object)
62 | o.Set("print", func(call js.FunctionCall) js.Value {
63 | fmt.Fprint(w, prefix, call.Argument(0).String())
64 | return js.Undefined()
65 | })
66 | }
67 | }
68 |
69 | vm1 := js.New()
70 | buf1 := &bytes.Buffer{}
71 |
72 | registry1 := new(Registry)
73 | registry1.Enable(vm1)
74 |
75 | registry1.RegisterNativeModule("test/log", logWithOutput(buf1, "vm1 "))
76 |
77 | vm2 := js.New()
78 | buf2 := &bytes.Buffer{}
79 |
80 | registry2 := new(Registry)
81 | registry2.Enable(vm2)
82 |
83 | registry2.RegisterNativeModule("test/log", logWithOutput(buf2, "vm2 "))
84 |
85 | _, err := vm1.RunString(SCRIPT)
86 | if err != nil {
87 | t.Fatal(err)
88 | }
89 |
90 | s := buf1.String()
91 | if s != "vm1 passed" {
92 | t.Fatalf("vm1: Unexpected result: %q", s)
93 | }
94 |
95 | _, err = vm2.RunString(SCRIPT)
96 | if err != nil {
97 | t.Fatal(err)
98 | }
99 |
100 | s = buf2.String()
101 | if s != "vm2 passed" {
102 | t.Fatalf("vm2: Unexpected result: %q", s)
103 | }
104 | }
105 |
106 | func TestRequire(t *testing.T) {
107 | const SCRIPT = `
108 | var m = require("./testdata/m.js");
109 | m.test();
110 | `
111 |
112 | vm := js.New()
113 |
114 | registry := new(Registry)
115 | registry.Enable(vm)
116 |
117 | v, err := vm.RunString(SCRIPT)
118 | if err != nil {
119 | t.Fatal(err)
120 | }
121 |
122 | if !v.StrictEquals(vm.ToValue("passed")) {
123 | t.Fatalf("Unexpected result: %v", v)
124 | }
125 | }
126 |
127 | func TestSourceLoader(t *testing.T) {
128 | const SCRIPT = `
129 | var m = require("m.js");
130 | m.test();
131 | `
132 |
133 | const MODULE = `
134 | function test() {
135 | return "passed1";
136 | }
137 |
138 | exports.test = test;
139 | `
140 |
141 | vm := js.New()
142 |
143 | registry := NewRegistry(WithGlobalFolders("."), WithLoader(func(name string) ([]byte, error) {
144 | if name == "m.js" {
145 | return []byte(MODULE), nil
146 | }
147 | return nil, errors.New("Module does not exist")
148 | }))
149 | registry.Enable(vm)
150 |
151 | v, err := vm.RunString(SCRIPT)
152 | if err != nil {
153 | t.Fatal(err)
154 | }
155 |
156 | if !v.StrictEquals(vm.ToValue("passed1")) {
157 | t.Fatalf("Unexpected result: %v", v)
158 | }
159 | }
160 |
161 | func TestStrictModule(t *testing.T) {
162 | const SCRIPT = `
163 | var m = require("m.js");
164 | m.test();
165 | `
166 |
167 | const MODULE = `
168 | "use strict";
169 |
170 | function test() {
171 | var a = "passed1";
172 | eval("var a = 'not passed'");
173 | return a;
174 | }
175 |
176 | exports.test = test;
177 | `
178 |
179 | vm := js.New()
180 |
181 | registry := NewRegistry(WithGlobalFolders("."), WithLoader(func(name string) ([]byte, error) {
182 | if name == "m.js" {
183 | return []byte(MODULE), nil
184 | }
185 | return nil, errors.New("Module does not exist")
186 | }))
187 | registry.Enable(vm)
188 |
189 | v, err := vm.RunString(SCRIPT)
190 | if err != nil {
191 | t.Fatal(err)
192 | }
193 |
194 | if !v.StrictEquals(vm.ToValue("passed1")) {
195 | t.Fatalf("Unexpected result: %v", v)
196 | }
197 | }
198 |
199 | func TestResolve(t *testing.T) {
200 | testRequire := func(src, fpath string, globalFolders []string, fs map[string]string) (*js.Runtime, js.Value, error) {
201 | vm := js.New()
202 | r := NewRegistry(WithGlobalFolders(globalFolders...), WithLoader(mapFileSystemSourceLoader(fs)))
203 | r.Enable(vm)
204 | t.Logf("Require(%s)", fpath)
205 | ret, err := vm.RunScript(path.Join(src, "test.js"), fmt.Sprintf("require('%s')", fpath))
206 | if err != nil {
207 | return nil, nil, err
208 | }
209 | return vm, ret, nil
210 | }
211 |
212 | globalFolders := []string{
213 | "/usr/lib/node_modules",
214 | "/home/src/.node_modules",
215 | }
216 |
217 | fs := map[string]string{
218 | "/home/src/app/app.js": `exports.name = "app"`,
219 | "/home/src/app2/app2.json": `{"name": "app2"}`,
220 | "/home/src/app3/index.js": `exports.name = "app3"`,
221 | "/home/src/app4/index.json": `{"name": "app4"}`,
222 | "/home/src/app5/package.json": `{"main": "app5.js"}`,
223 | "/home/src/app5/app5.js": `exports.name = "app5"`,
224 | "/home/src/app6/package.json": `{"main": "."}`,
225 | "/home/src/app6/index.js": `exports.name = "app6"`,
226 | "/home/src/app7/package.json": `{"main": "./a/b/c/file.js"}`,
227 | "/home/src/app7/a/b/c/file.js": `exports.name = "app7"`,
228 | "/usr/lib/node_modules/app8": `exports.name = "app8"`,
229 | "/home/src/app9/app9.js": `exports.name = require('./a/file.js').name`,
230 | "/home/src/app9/a/file.js": `exports.name = require('./b/file.js').name`,
231 | "/home/src/app9/a/b/file.js": `exports.name = require('./c/file.js').name`,
232 | "/home/src/app9/a/b/c/file.js": `exports.name = "app9"`,
233 | "/home/src/.node_modules/app10": `exports.name = "app10"`,
234 | "/home/src/app11/app11.js": `exports.name = require('d/file.js').name`,
235 | "/home/src/app11/a/b/c/app11.js": `exports.name = require('d/file.js').name`,
236 | "/home/src/app11/node_modules/d/file.js": `exports.name = "app11"`,
237 | "/app12.js": `exports.name = require('a/file.js').name`,
238 | "/node_modules/a/file.js": `exports.name = "app12"`,
239 | "/app13/app13.js": `exports.name = require('b/file.js').name`,
240 | "/node_modules/b/file.js": `exports.name = "app13"`,
241 | "node_modules/app14/index.js": `exports.name = "app14"`,
242 | "../node_modules/app15/index.js": `exports.name = "app15"`,
243 | }
244 |
245 | for i, tc := range []struct {
246 | src string
247 | path string
248 | ok bool
249 | field string
250 | value string
251 | }{
252 | {"/home/src", "./app/app", true, "name", "app"},
253 | {"/home/src", "./app/app.js", true, "name", "app"},
254 | {"/home/src", "./app/bad.js", false, "", ""},
255 | {"/home/src", "./app2/app2", true, "name", "app2"},
256 | {"/home/src", "./app2/app2.json", true, "name", "app2"},
257 | {"/home/src", "./app/bad.json", false, "", ""},
258 | {"/home/src", "./app3", true, "name", "app3"},
259 | {"/home/src", "./appbad", false, "", ""},
260 | {"/home/src", "./app4", true, "name", "app4"},
261 | {"/home/src", "./appbad", false, "", ""},
262 | {"/home/src", "./app5", true, "name", "app5"},
263 | {"/home/src", "./app6", true, "name", "app6"},
264 | {"/home/src", "./app7", true, "name", "app7"},
265 | {"/home/src", "app8", true, "name", "app8"},
266 | {"/home/src", "./app9/app9", true, "name", "app9"},
267 | {"/home/src", "app10", true, "name", "app10"},
268 | {"/home/src", "./app11/app11.js", true, "name", "app11"},
269 | {"/home/src", "./app11/a/b/c/app11.js", true, "name", "app11"},
270 | {"/", "./app12", true, "name", "app12"},
271 | {"/", "./app13/app13", true, "name", "app13"},
272 | {".", "app14", true, "name", "app14"},
273 | {"..", "nonexistent", false, "", ""},
274 | } {
275 | vm, mod, err := testRequire(tc.src, tc.path, globalFolders, fs)
276 | if err != nil {
277 | if tc.ok {
278 | t.Errorf("%d: require() failed: %v", i, err)
279 | }
280 | continue
281 | } else {
282 | if !tc.ok {
283 | t.Errorf("%d: expected to fail, but did not", i)
284 | continue
285 | }
286 | }
287 | f := mod.ToObject(vm).Get(tc.field)
288 | if f == nil {
289 | t.Errorf("%v: field %q not found", i, tc.field)
290 | continue
291 | }
292 | value := f.String()
293 | if value != tc.value {
294 | t.Errorf("%v: got %q expected %q", i, value, tc.value)
295 | }
296 | }
297 | }
298 |
299 | func TestRequireCycle(t *testing.T) {
300 | vm := js.New()
301 | r := NewRegistry(WithLoader(mapFileSystemSourceLoader(map[string]string{
302 | "a.js": `var b = require('./b.js'); exports.done = true;`,
303 | "b.js": `var a = require('./a.js'); exports.done = true;`,
304 | })))
305 | r.Enable(vm)
306 | res, err := vm.RunString(`
307 | var a = require('./a.js');
308 | var b = require('./b.js');
309 | a.done && b.done;
310 | `)
311 | if err != nil {
312 | t.Fatal(err)
313 | }
314 | if v := res.Export(); v != true {
315 | t.Fatalf("Unexpected result: %v", v)
316 | }
317 | }
318 |
319 | func TestErrorPropagation(t *testing.T) {
320 | vm := js.New()
321 | r := NewRegistry(WithLoader(mapFileSystemSourceLoader(map[string]string{
322 | "m.js": `throw 'test passed';`,
323 | })))
324 | rr := r.Enable(vm)
325 | _, err := rr.Require("./m")
326 | if err == nil {
327 | t.Fatal("Expected an error")
328 | }
329 | if ex, ok := err.(*js.Exception); ok {
330 | if !ex.Value().StrictEquals(vm.ToValue("test passed")) {
331 | t.Fatalf("Unexpected Exception: %v", ex)
332 | }
333 | } else {
334 | t.Fatal(err)
335 | }
336 | }
337 |
338 | func TestSourceMapLoader(t *testing.T) {
339 | vm := js.New()
340 | r := NewRegistry(WithLoader(func(p string) ([]byte, error) {
341 | switch p {
342 | case "dir/m.js":
343 | return []byte(`throw 'test passed';
344 | //# sourceMappingURL=m.js.map`), nil
345 | case "dir/m.js.map":
346 | return []byte(`{"version":3,"file":"m.js","sourceRoot":"","sources":["m.ts"],"names":[],"mappings":";AAAA"}
347 | `), nil
348 | }
349 | return nil, ModuleFileDoesNotExistError
350 | }))
351 |
352 | rr := r.Enable(vm)
353 | _, err := rr.Require("./dir/m")
354 | if err == nil {
355 | t.Fatal("Expected an error")
356 | }
357 | if ex, ok := err.(*js.Exception); ok {
358 | if !ex.Value().StrictEquals(vm.ToValue("test passed")) {
359 | t.Fatalf("Unexpected Exception: %v", ex)
360 | }
361 | } else {
362 | t.Fatal(err)
363 | }
364 | }
365 |
366 | func testsetup() (string, func(), error) {
367 | name, err := os.MkdirTemp("", "goja-nodejs-require-test")
368 | if err != nil {
369 | return "", nil, err
370 | }
371 | return name, func() {
372 | os.RemoveAll(name)
373 | }, nil
374 | }
375 |
376 | func TestDefaultModuleLoader(t *testing.T) {
377 | workdir, teardown, err := testsetup()
378 | if err != nil {
379 | t.Fatal(err)
380 | }
381 | defer teardown()
382 |
383 | err = os.Chdir(workdir)
384 | if err != nil {
385 | t.Fatal(err)
386 | }
387 | err = os.Mkdir("module", 0755)
388 | if err != nil {
389 | t.Fatal(err)
390 | }
391 | err = os.WriteFile("module/index.js", []byte(`throw 'test passed';`), 0644)
392 | if err != nil {
393 | t.Fatal(err)
394 | }
395 | vm := js.New()
396 | r := NewRegistry()
397 | rr := r.Enable(vm)
398 | _, err = rr.Require("./module")
399 | if err == nil {
400 | t.Fatal("Expected an error")
401 | }
402 | if ex, ok := err.(*js.Exception); ok {
403 | if !ex.Value().StrictEquals(vm.ToValue("test passed")) {
404 | t.Fatalf("Unexpected Exception: %v", ex)
405 | }
406 | } else {
407 | t.Fatal(err)
408 | }
409 | }
410 |
--------------------------------------------------------------------------------
/pkg/htmlparser/lex.go:
--------------------------------------------------------------------------------
1 | // Package html is an HTML5 lexer following the specifications at http://www.w3.org/TR/html5/syntax.html.
2 | package htmlparser
3 |
4 | import (
5 | "strconv"
6 |
7 | "github.com/tdewolff/parse/v2"
8 | )
9 |
10 | // TokenType determines the type of token, eg. a number or a semicolon.
11 | type TokenType uint32
12 |
13 | // TokenType values.
14 | const (
15 | ErrorToken TokenType = iota // extra token when errors occur
16 | CommentToken
17 | DoctypeToken
18 | StartTagToken
19 | StartTagCloseToken
20 | StartTagVoidToken
21 | EndTagToken
22 | AttributeToken
23 | TextToken
24 | SvgToken
25 | MathToken
26 | )
27 |
28 | // String returns the string representation of a TokenType.
29 | func (tt TokenType) String() string {
30 | switch tt {
31 | case ErrorToken:
32 | return "Error"
33 | case CommentToken:
34 | return "Comment"
35 | case DoctypeToken:
36 | return "Doctype"
37 | case StartTagToken:
38 | return "StartTag"
39 | case StartTagCloseToken:
40 | return "StartTagClose"
41 | case StartTagVoidToken:
42 | return "StartTagVoid"
43 | case EndTagToken:
44 | return "EndTag"
45 | case AttributeToken:
46 | return "Attribute"
47 | case TextToken:
48 | return "Text"
49 | case SvgToken:
50 | return "Svg"
51 | case MathToken:
52 | return "Math"
53 | }
54 | return "Invalid(" + strconv.Itoa(int(tt)) + ")"
55 | }
56 |
57 | ////////////////////////////////////////////////////////////////
58 |
59 | // Lexer is the state for the lexer.
60 | type Lexer struct {
61 | r *parse.Input
62 | err error
63 |
64 | rawTag Hash
65 | inTag bool
66 |
67 | text []byte
68 | attrVal []byte
69 | }
70 |
71 | // NewLexer returns a new Lexer for a given io.Reader.
72 | func NewLexer(r *parse.Input) *Lexer {
73 | return &Lexer{
74 | r: r,
75 | }
76 | }
77 |
78 | // Err returns the error encountered during lexing, this is often io.EOF but also other errors can be returned.
79 | func (l *Lexer) Err() error {
80 | if l.err != nil {
81 | return l.err
82 | }
83 | return l.r.Err()
84 | }
85 |
86 | // Text returns the textual representation of a token. This excludes delimiters and additional leading/trailing characters.
87 | func (l *Lexer) Text() []byte {
88 | return l.text
89 | }
90 |
91 | // AttrVal returns the attribute value when an AttributeToken was returned from Next.
92 | func (l *Lexer) AttrVal() []byte {
93 | return l.attrVal
94 | }
95 |
96 | // Next returns the next Token. It returns ErrorToken when an error was encountered. Using Err() one can retrieve the error message.
97 | func (l *Lexer) Next() (TokenType, []byte) {
98 | l.text = nil
99 | var c byte
100 | if l.inTag {
101 | l.attrVal = nil
102 | for { // before attribute name state
103 | if c = l.r.Peek(0); c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' {
104 | l.r.Move(1)
105 | continue
106 | }
107 | break
108 | }
109 | if c == 0 && l.r.Err() != nil {
110 | return ErrorToken, nil
111 | } else if c != '>' && (c != '/' || l.r.Peek(1) != '>') {
112 | return AttributeToken, l.shiftAttribute()
113 | }
114 |
115 | // modify: remove l.r.Skip()
116 | //l.r.Skip()
117 | l.inTag = false
118 | if c == '/' {
119 | l.r.Move(2)
120 | return StartTagVoidToken, l.r.Shift()
121 | }
122 | l.r.Move(1)
123 | return StartTagCloseToken, l.r.Shift()
124 | }
125 |
126 | if l.rawTag != 0 {
127 | if rawText := l.shiftRawText(); len(rawText) > 0 {
128 | l.text = rawText
129 | l.rawTag = 0
130 | return TextToken, rawText
131 | }
132 | l.rawTag = 0
133 | }
134 |
135 | for {
136 | c = l.r.Peek(0)
137 | if c == '<' {
138 | c = l.r.Peek(1)
139 | // modeify: delete l.r.Peek(2) != '>'
140 | isEndTag := c == '/' && (l.r.Peek(2) != 0 || l.r.PeekErr(2) == nil)
141 | if l.r.Pos() > 0 {
142 | // modify: add || c == '>'
143 | if isEndTag || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '!' || c == '?' || c == '>' {
144 | // return currently buffered texttoken so that we can return tag next iteration
145 | l.text = l.r.Shift()
146 | return TextToken, l.text
147 | }
148 | } else if isEndTag {
149 | l.r.Move(2)
150 | // only endtags that are not followed by > or EOF arrive here
151 | // modify: add && c != '>'
152 | if c = l.r.Peek(0); !('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') && c != '>' {
153 | return CommentToken, l.shiftBogusComment()
154 | }
155 | return EndTagToken, l.shiftEndTag()
156 | // modify: add || c == '>'
157 | } else if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '>' {
158 | l.r.Move(1)
159 | l.inTag = true
160 | return l.shiftStartTag()
161 | } else if c == '!' {
162 | l.r.Move(2)
163 | return l.readMarkup()
164 | } else if c == '?' {
165 | l.r.Move(1)
166 | return CommentToken, l.shiftBogusComment()
167 | }
168 | } else if c == 0 && l.r.Err() != nil {
169 | if l.r.Pos() > 0 {
170 | l.text = l.r.Shift()
171 | return TextToken, l.text
172 | }
173 | return ErrorToken, nil
174 | }
175 | l.r.Move(1)
176 | }
177 | }
178 |
179 | ////////////////////////////////////////////////////////////////
180 |
181 | // The following functions follow the specifications at https://html.spec.whatwg.org/multipage/parsing.html
182 |
183 | func (l *Lexer) shiftRawText() []byte {
184 | if l.rawTag == Plaintext {
185 | for {
186 | if l.r.Peek(0) == 0 && l.r.Err() != nil {
187 | return l.r.Shift()
188 | }
189 | l.r.Move(1)
190 | }
191 | } else { // RCDATA, RAWTEXT and SCRIPT
192 | for {
193 | c := l.r.Peek(0)
194 | if c == '<' {
195 | if l.r.Peek(1) == '/' {
196 | mark := l.r.Pos()
197 | l.r.Move(2)
198 | for {
199 | if c = l.r.Peek(0); !('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
200 | break
201 | }
202 | l.r.Move(1)
203 | }
204 | if h := ToHash(parse.ToLower(parse.Copy(l.r.Lexeme()[mark+2:]))); h == l.rawTag { // copy so that ToLower doesn't change the case of the underlying slice
205 | l.r.Rewind(mark)
206 | return l.r.Shift()
207 | }
208 | } else if l.rawTag == Script && l.r.Peek(1) == '!' && l.r.Peek(2) == '-' && l.r.Peek(3) == '-' {
209 | l.r.Move(4)
210 | inScript := false
211 | for {
212 | c := l.r.Peek(0)
213 | if c == '-' && l.r.Peek(1) == '-' && l.r.Peek(2) == '>' {
214 | l.r.Move(3)
215 | break
216 | } else if c == '<' {
217 | isEnd := l.r.Peek(1) == '/'
218 | if isEnd {
219 | l.r.Move(2)
220 | } else {
221 | l.r.Move(1)
222 | }
223 | mark := l.r.Pos()
224 | for {
225 | if c = l.r.Peek(0); !('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
226 | break
227 | }
228 | l.r.Move(1)
229 | }
230 | if h := ToHash(parse.ToLower(parse.Copy(l.r.Lexeme()[mark:]))); h == Script { // copy so that ToLower doesn't change the case of the underlying slice
231 | if !isEnd {
232 | inScript = true
233 | } else {
234 | if !inScript {
235 | l.r.Rewind(mark - 2)
236 | return l.r.Shift()
237 | }
238 | inScript = false
239 | }
240 | }
241 | } else if c == 0 && l.r.Err() != nil {
242 | return l.r.Shift()
243 | } else {
244 | l.r.Move(1)
245 | }
246 | }
247 | } else {
248 | l.r.Move(1)
249 | }
250 | } else if c == 0 && l.r.Err() != nil {
251 | return l.r.Shift()
252 | } else {
253 | l.r.Move(1)
254 | }
255 | }
256 | }
257 | }
258 |
259 | func (l *Lexer) readMarkup() (TokenType, []byte) {
260 | if l.at('-', '-') {
261 | l.r.Move(2)
262 | for {
263 | if l.r.Peek(0) == 0 && l.r.Err() != nil {
264 | l.text = l.r.Lexeme()[4:]
265 | return CommentToken, l.r.Shift()
266 | } else if l.at('-', '-', '>') {
267 | l.text = l.r.Lexeme()[4:]
268 | l.r.Move(3)
269 | return CommentToken, l.r.Shift()
270 | } else if l.at('-', '-', '!', '>') {
271 | l.text = l.r.Lexeme()[4:]
272 | l.r.Move(4)
273 | return CommentToken, l.r.Shift()
274 | }
275 | l.r.Move(1)
276 | }
277 | } else if l.at('[', 'C', 'D', 'A', 'T', 'A', '[') {
278 | l.r.Move(7)
279 | for {
280 | if l.r.Peek(0) == 0 && l.r.Err() != nil {
281 | l.text = l.r.Lexeme()[9:]
282 | return TextToken, l.r.Shift()
283 | } else if l.at(']', ']', '>') {
284 | l.text = l.r.Lexeme()[9:]
285 | l.r.Move(3)
286 | return TextToken, l.r.Shift()
287 | }
288 | l.r.Move(1)
289 | }
290 | } else {
291 | if l.atCaseInsensitive('d', 'o', 'c', 't', 'y', 'p', 'e') {
292 | l.r.Move(7)
293 | if l.r.Peek(0) == ' ' {
294 | l.r.Move(1)
295 | }
296 | for {
297 | if c := l.r.Peek(0); c == '>' || c == 0 && l.r.Err() != nil {
298 | l.text = l.r.Lexeme()[9:]
299 | if c == '>' {
300 | l.r.Move(1)
301 | }
302 | return DoctypeToken, l.r.Shift()
303 | }
304 | l.r.Move(1)
305 | }
306 | }
307 | }
308 | return CommentToken, l.shiftBogusComment()
309 | }
310 |
311 | func (l *Lexer) shiftBogusComment() []byte {
312 | for {
313 | c := l.r.Peek(0)
314 | if c == '>' {
315 | l.text = l.r.Lexeme()[2:]
316 | l.r.Move(1)
317 | return l.r.Shift()
318 | } else if c == 0 && l.r.Err() != nil {
319 | l.text = l.r.Lexeme()[2:]
320 | return l.r.Shift()
321 | }
322 | l.r.Move(1)
323 | }
324 | }
325 |
326 | func (l *Lexer) shiftStartTag() (TokenType, []byte) {
327 | for {
328 | if c := l.r.Peek(0); c == ' ' || c == '>' || c == '/' && l.r.Peek(1) == '>' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == 0 && l.r.Err() != nil {
329 | break
330 | }
331 | l.r.Move(1)
332 | }
333 | l.text = parse.ToLower(parse.Copy(l.r.Lexeme()[1:]))
334 | if h := ToHash(l.text); h == Textarea || h == Title || h == Style || h == Xmp || h == Iframe || h == Script || h == Plaintext || h == Svg || h == Math {
335 | if h == Svg || h == Math {
336 | data := l.shiftXML(h)
337 | if l.err != nil {
338 | return ErrorToken, nil
339 | }
340 |
341 | l.inTag = false
342 | if h == Svg {
343 | return SvgToken, data
344 | }
345 | return MathToken, data
346 | }
347 | l.rawTag = h
348 | }
349 | return StartTagToken, l.r.Shift()
350 | }
351 |
352 | func (l *Lexer) shiftAttribute() []byte {
353 | nameStart := l.r.Pos()
354 | var c byte
355 | for { // attribute name state
356 | if c = l.r.Peek(0); c == ' ' || c == '=' || c == '>' || c == '/' && l.r.Peek(1) == '>' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == 0 && l.r.Err() != nil {
357 | break
358 | }
359 | l.r.Move(1)
360 | }
361 | nameEnd := l.r.Pos()
362 | for { // after attribute name state
363 | if c = l.r.Peek(0); c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' {
364 | l.r.Move(1)
365 | continue
366 | }
367 | break
368 | }
369 | if c == '=' {
370 | l.r.Move(1)
371 | for { // before attribute value state
372 | if c = l.r.Peek(0); c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' {
373 | l.r.Move(1)
374 | continue
375 | }
376 | break
377 | }
378 | attrPos := l.r.Pos()
379 | delim := c
380 | if delim == '"' || delim == '\'' { // attribute value single- and double-quoted state
381 | l.r.Move(1)
382 | for {
383 | c := l.r.Peek(0)
384 | if c == delim {
385 | l.r.Move(1)
386 | break
387 | } else if c == 0 && l.r.Err() != nil {
388 | break
389 | }
390 | l.r.Move(1)
391 | }
392 | } else { // attribute value unquoted state
393 | for {
394 | // modify: add || c == '/'
395 | if c := l.r.Peek(0); c == ' ' || c == '/' || c == '>' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == 0 && l.r.Err() != nil {
396 | break
397 | }
398 | l.r.Move(1)
399 | }
400 | }
401 | l.attrVal = l.r.Lexeme()[attrPos:]
402 | } else {
403 | l.r.Rewind(nameEnd)
404 | l.attrVal = nil
405 | }
406 | l.text = parse.ToLower(parse.Copy(l.r.Lexeme()[nameStart:nameEnd]))
407 | return l.r.Shift()
408 | }
409 |
410 | func (l *Lexer) shiftEndTag() []byte {
411 | for {
412 | c := l.r.Peek(0)
413 | if c == '>' {
414 | l.text = l.r.Lexeme()[2:]
415 | l.r.Move(1)
416 | break
417 | } else if c == 0 && l.r.Err() != nil {
418 | l.text = l.r.Lexeme()[2:]
419 | break
420 | }
421 | l.r.Move(1)
422 | }
423 |
424 | end := len(l.text)
425 | for end > 0 {
426 | if c := l.text[end-1]; c == ' ' || c == '\t' || c == '\n' || c == '\r' {
427 | end--
428 | continue
429 | }
430 | break
431 | }
432 | l.text = l.text[:end]
433 | return l.r.Shift()
434 | }
435 |
436 | // shiftXML parses the content of a svg or math tag according to the XML 1.1 specifications, including the tag itself.
437 | // So far we have already parsed `' {
470 | l.r.Move(1)
471 | break
472 | } else if c == 0 {
473 | if l.r.Err() == nil {
474 | l.err = parse.NewErrorLexer(l.r, "HTML parse error: unexpected NULL character")
475 | }
476 | return l.r.Shift()
477 | }
478 | l.r.Move(1)
479 | }
480 | return l.r.Shift()
481 | }
482 |
483 | ////////////////////////////////////////////////////////////////
484 |
485 | func (l *Lexer) at(b ...byte) bool {
486 | for i, c := range b {
487 | if l.r.Peek(i) != c {
488 | return false
489 | }
490 | }
491 | return true
492 | }
493 |
494 | func (l *Lexer) atCaseInsensitive(b ...byte) bool {
495 | for i, c := range b {
496 | if l.r.Peek(i) != c && (l.r.Peek(i)+('a'-'A')) != c {
497 | return false
498 | }
499 | }
500 | return true
501 | }
502 |
--------------------------------------------------------------------------------
/test/blog/tailwind.css:
--------------------------------------------------------------------------------
1 | /*
2 | ! tailwindcss v3.1.6 | MIT License | https://tailwindcss.com
3 | */
4 |
5 | /*
6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
8 | */
9 |
10 | *,
11 | ::before,
12 | ::after {
13 | box-sizing: border-box;
14 | /* 1 */
15 | border-width: 0;
16 | /* 2 */
17 | border-style: solid;
18 | /* 2 */
19 | border-color: #e5e7eb;
20 | /* 2 */
21 | }
22 |
23 | ::before,
24 | ::after {
25 | --tw-content: '';
26 | }
27 |
28 | /*
29 | 1. Use a consistent sensible line-height in all browsers.
30 | 2. Prevent adjustments of font size after orientation changes in iOS.
31 | 3. Use a more readable tab size.
32 | 4. Use the user's configured `sans` font-family by default.
33 | */
34 |
35 | html {
36 | line-height: 1.5;
37 | /* 1 */
38 | -webkit-text-size-adjust: 100%;
39 | /* 2 */
40 | -moz-tab-size: 4;
41 | /* 3 */
42 | -o-tab-size: 4;
43 | tab-size: 4;
44 | /* 3 */
45 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
46 | /* 4 */
47 | }
48 |
49 | /*
50 | 1. Remove the margin in all browsers.
51 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
52 | */
53 |
54 | body {
55 | margin: 0;
56 | /* 1 */
57 | line-height: inherit;
58 | /* 2 */
59 | }
60 |
61 | /*
62 | 1. Add the correct height in Firefox.
63 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
64 | 3. Ensure horizontal rules are visible by default.
65 | */
66 |
67 | hr {
68 | height: 0;
69 | /* 1 */
70 | color: inherit;
71 | /* 2 */
72 | border-top-width: 1px;
73 | /* 3 */
74 | }
75 |
76 | /*
77 | Add the correct text decoration in Chrome, Edge, and Safari.
78 | */
79 |
80 | abbr:where([title]) {
81 | -webkit-text-decoration: underline dotted;
82 | text-decoration: underline dotted;
83 | }
84 |
85 | /*
86 | Remove the default font size and weight for headings.
87 | */
88 |
89 | h1,
90 | h2,
91 | h3,
92 | h4,
93 | h5,
94 | h6 {
95 | font-size: inherit;
96 | font-weight: inherit;
97 | }
98 |
99 | /*
100 | Reset links to optimize for opt-in styling instead of opt-out.
101 | */
102 |
103 | a {
104 | color: inherit;
105 | text-decoration: inherit;
106 | }
107 |
108 | /*
109 | Add the correct font weight in Edge and Safari.
110 | */
111 |
112 | b,
113 | strong {
114 | font-weight: bolder;
115 | }
116 |
117 | /*
118 | 1. Use the user's configured `mono` font family by default.
119 | 2. Correct the odd `em` font sizing in all browsers.
120 | */
121 |
122 | code,
123 | kbd,
124 | samp,
125 | pre {
126 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
127 | /* 1 */
128 | font-size: 1em;
129 | /* 2 */
130 | }
131 |
132 | /*
133 | Add the correct font size in all browsers.
134 | */
135 |
136 | small {
137 | font-size: 80%;
138 | }
139 |
140 | /*
141 | Prevent `sub` and `sup` elements from affecting the line height in all browsers.
142 | */
143 |
144 | sub,
145 | sup {
146 | font-size: 75%;
147 | line-height: 0;
148 | position: relative;
149 | vertical-align: baseline;
150 | }
151 |
152 | sub {
153 | bottom: -0.25em;
154 | }
155 |
156 | sup {
157 | top: -0.5em;
158 | }
159 |
160 | /*
161 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
162 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
163 | 3. Remove gaps between table borders by default.
164 | */
165 |
166 | table {
167 | text-indent: 0;
168 | /* 1 */
169 | border-color: inherit;
170 | /* 2 */
171 | border-collapse: collapse;
172 | /* 3 */
173 | }
174 |
175 | /*
176 | 1. Change the font styles in all browsers.
177 | 2. Remove the margin in Firefox and Safari.
178 | 3. Remove default padding in all browsers.
179 | */
180 |
181 | button,
182 | input,
183 | optgroup,
184 | select,
185 | textarea {
186 | font-family: inherit;
187 | /* 1 */
188 | font-size: 100%;
189 | /* 1 */
190 | font-weight: inherit;
191 | /* 1 */
192 | line-height: inherit;
193 | /* 1 */
194 | color: inherit;
195 | /* 1 */
196 | margin: 0;
197 | /* 2 */
198 | padding: 0;
199 | /* 3 */
200 | }
201 |
202 | /*
203 | Remove the inheritance of text transform in Edge and Firefox.
204 | */
205 |
206 | button,
207 | select {
208 | text-transform: none;
209 | }
210 |
211 | /*
212 | 1. Correct the inability to style clickable types in iOS and Safari.
213 | 2. Remove default button styles.
214 | */
215 |
216 | button,
217 | [type='button'],
218 | [type='reset'],
219 | [type='submit'] {
220 | -webkit-appearance: button;
221 | /* 1 */
222 | background-color: transparent;
223 | /* 2 */
224 | background-image: none;
225 | /* 2 */
226 | }
227 |
228 | /*
229 | Use the modern Firefox focus style for all focusable elements.
230 | */
231 |
232 | :-moz-focusring {
233 | outline: auto;
234 | }
235 |
236 | /*
237 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
238 | */
239 |
240 | :-moz-ui-invalid {
241 | box-shadow: none;
242 | }
243 |
244 | /*
245 | Add the correct vertical alignment in Chrome and Firefox.
246 | */
247 |
248 | progress {
249 | vertical-align: baseline;
250 | }
251 |
252 | /*
253 | Correct the cursor style of increment and decrement buttons in Safari.
254 | */
255 |
256 | ::-webkit-inner-spin-button,
257 | ::-webkit-outer-spin-button {
258 | height: auto;
259 | }
260 |
261 | /*
262 | 1. Correct the odd appearance in Chrome and Safari.
263 | 2. Correct the outline style in Safari.
264 | */
265 |
266 | [type='search'] {
267 | -webkit-appearance: textfield;
268 | /* 1 */
269 | outline-offset: -2px;
270 | /* 2 */
271 | }
272 |
273 | /*
274 | Remove the inner padding in Chrome and Safari on macOS.
275 | */
276 |
277 | ::-webkit-search-decoration {
278 | -webkit-appearance: none;
279 | }
280 |
281 | /*
282 | 1. Correct the inability to style clickable types in iOS and Safari.
283 | 2. Change font properties to `inherit` in Safari.
284 | */
285 |
286 | ::-webkit-file-upload-button {
287 | -webkit-appearance: button;
288 | /* 1 */
289 | font: inherit;
290 | /* 2 */
291 | }
292 |
293 | /*
294 | Add the correct display in Chrome and Safari.
295 | */
296 |
297 | summary {
298 | display: list-item;
299 | }
300 |
301 | /*
302 | Removes the default spacing and border for appropriate elements.
303 | */
304 |
305 | blockquote,
306 | dl,
307 | dd,
308 | h1,
309 | h2,
310 | h3,
311 | h4,
312 | h5,
313 | h6,
314 | hr,
315 | figure,
316 | p,
317 | pre {
318 | margin: 0;
319 | }
320 |
321 | fieldset {
322 | margin: 0;
323 | padding: 0;
324 | }
325 |
326 | legend {
327 | padding: 0;
328 | }
329 |
330 | ol,
331 | ul,
332 | menu {
333 | list-style: none;
334 | margin: 0;
335 | padding: 0;
336 | }
337 |
338 | /*
339 | Prevent resizing textareas horizontally by default.
340 | */
341 |
342 | textarea {
343 | resize: vertical;
344 | }
345 |
346 | /*
347 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
348 | 2. Set the default placeholder color to the user's configured gray 400 color.
349 | */
350 |
351 | input::-moz-placeholder, textarea::-moz-placeholder {
352 | opacity: 1;
353 | /* 1 */
354 | color: #9ca3af;
355 | /* 2 */
356 | }
357 |
358 | input:-ms-input-placeholder, textarea:-ms-input-placeholder {
359 | opacity: 1;
360 | /* 1 */
361 | color: #9ca3af;
362 | /* 2 */
363 | }
364 |
365 | input::placeholder,
366 | textarea::placeholder {
367 | opacity: 1;
368 | /* 1 */
369 | color: #9ca3af;
370 | /* 2 */
371 | }
372 |
373 | /*
374 | Set the default cursor for buttons.
375 | */
376 |
377 | button,
378 | [role="button"] {
379 | cursor: pointer;
380 | }
381 |
382 | /*
383 | Make sure disabled buttons don't get the pointer cursor.
384 | */
385 |
386 | :disabled {
387 | cursor: default;
388 | }
389 |
390 | /*
391 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
392 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
393 | This can trigger a poorly considered lint error in some tools but is included by design.
394 | */
395 |
396 | img,
397 | svg,
398 | video,
399 | canvas,
400 | audio,
401 | iframe,
402 | embed,
403 | object {
404 | display: block;
405 | /* 1 */
406 | vertical-align: middle;
407 | /* 2 */
408 | }
409 |
410 | /*
411 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
412 | */
413 |
414 | img,
415 | video {
416 | max-width: 100%;
417 | height: auto;
418 | }
419 |
420 | *, ::before, ::after {
421 | --tw-border-spacing-x: 0;
422 | --tw-border-spacing-y: 0;
423 | --tw-translate-x: 0;
424 | --tw-translate-y: 0;
425 | --tw-rotate: 0;
426 | --tw-skew-x: 0;
427 | --tw-skew-y: 0;
428 | --tw-scale-x: 1;
429 | --tw-scale-y: 1;
430 | --tw-pan-x: ;
431 | --tw-pan-y: ;
432 | --tw-pinch-zoom: ;
433 | --tw-scroll-snap-strictness: proximity;
434 | --tw-ordinal: ;
435 | --tw-slashed-zero: ;
436 | --tw-numeric-figure: ;
437 | --tw-numeric-spacing: ;
438 | --tw-numeric-fraction: ;
439 | --tw-ring-inset: ;
440 | --tw-ring-offset-width: 0px;
441 | --tw-ring-offset-color: #fff;
442 | --tw-ring-color: rgb(59 130 246 / 0.5);
443 | --tw-ring-offset-shadow: 0 0 #0000;
444 | --tw-ring-shadow: 0 0 #0000;
445 | --tw-shadow: 0 0 #0000;
446 | --tw-shadow-colored: 0 0 #0000;
447 | --tw-blur: ;
448 | --tw-brightness: ;
449 | --tw-contrast: ;
450 | --tw-grayscale: ;
451 | --tw-hue-rotate: ;
452 | --tw-invert: ;
453 | --tw-saturate: ;
454 | --tw-sepia: ;
455 | --tw-drop-shadow: ;
456 | --tw-backdrop-blur: ;
457 | --tw-backdrop-brightness: ;
458 | --tw-backdrop-contrast: ;
459 | --tw-backdrop-grayscale: ;
460 | --tw-backdrop-hue-rotate: ;
461 | --tw-backdrop-invert: ;
462 | --tw-backdrop-opacity: ;
463 | --tw-backdrop-saturate: ;
464 | --tw-backdrop-sepia: ;
465 | }
466 |
467 | ::-webkit-backdrop {
468 | --tw-border-spacing-x: 0;
469 | --tw-border-spacing-y: 0;
470 | --tw-translate-x: 0;
471 | --tw-translate-y: 0;
472 | --tw-rotate: 0;
473 | --tw-skew-x: 0;
474 | --tw-skew-y: 0;
475 | --tw-scale-x: 1;
476 | --tw-scale-y: 1;
477 | --tw-pan-x: ;
478 | --tw-pan-y: ;
479 | --tw-pinch-zoom: ;
480 | --tw-scroll-snap-strictness: proximity;
481 | --tw-ordinal: ;
482 | --tw-slashed-zero: ;
483 | --tw-numeric-figure: ;
484 | --tw-numeric-spacing: ;
485 | --tw-numeric-fraction: ;
486 | --tw-ring-inset: ;
487 | --tw-ring-offset-width: 0px;
488 | --tw-ring-offset-color: #fff;
489 | --tw-ring-color: rgb(59 130 246 / 0.5);
490 | --tw-ring-offset-shadow: 0 0 #0000;
491 | --tw-ring-shadow: 0 0 #0000;
492 | --tw-shadow: 0 0 #0000;
493 | --tw-shadow-colored: 0 0 #0000;
494 | --tw-blur: ;
495 | --tw-brightness: ;
496 | --tw-contrast: ;
497 | --tw-grayscale: ;
498 | --tw-hue-rotate: ;
499 | --tw-invert: ;
500 | --tw-saturate: ;
501 | --tw-sepia: ;
502 | --tw-drop-shadow: ;
503 | --tw-backdrop-blur: ;
504 | --tw-backdrop-brightness: ;
505 | --tw-backdrop-contrast: ;
506 | --tw-backdrop-grayscale: ;
507 | --tw-backdrop-hue-rotate: ;
508 | --tw-backdrop-invert: ;
509 | --tw-backdrop-opacity: ;
510 | --tw-backdrop-saturate: ;
511 | --tw-backdrop-sepia: ;
512 | }
513 |
514 | ::backdrop {
515 | --tw-border-spacing-x: 0;
516 | --tw-border-spacing-y: 0;
517 | --tw-translate-x: 0;
518 | --tw-translate-y: 0;
519 | --tw-rotate: 0;
520 | --tw-skew-x: 0;
521 | --tw-skew-y: 0;
522 | --tw-scale-x: 1;
523 | --tw-scale-y: 1;
524 | --tw-pan-x: ;
525 | --tw-pan-y: ;
526 | --tw-pinch-zoom: ;
527 | --tw-scroll-snap-strictness: proximity;
528 | --tw-ordinal: ;
529 | --tw-slashed-zero: ;
530 | --tw-numeric-figure: ;
531 | --tw-numeric-spacing: ;
532 | --tw-numeric-fraction: ;
533 | --tw-ring-inset: ;
534 | --tw-ring-offset-width: 0px;
535 | --tw-ring-offset-color: #fff;
536 | --tw-ring-color: rgb(59 130 246 / 0.5);
537 | --tw-ring-offset-shadow: 0 0 #0000;
538 | --tw-ring-shadow: 0 0 #0000;
539 | --tw-shadow: 0 0 #0000;
540 | --tw-shadow-colored: 0 0 #0000;
541 | --tw-blur: ;
542 | --tw-brightness: ;
543 | --tw-contrast: ;
544 | --tw-grayscale: ;
545 | --tw-hue-rotate: ;
546 | --tw-invert: ;
547 | --tw-saturate: ;
548 | --tw-sepia: ;
549 | --tw-drop-shadow: ;
550 | --tw-backdrop-blur: ;
551 | --tw-backdrop-brightness: ;
552 | --tw-backdrop-contrast: ;
553 | --tw-backdrop-grayscale: ;
554 | --tw-backdrop-hue-rotate: ;
555 | --tw-backdrop-invert: ;
556 | --tw-backdrop-opacity: ;
557 | --tw-backdrop-saturate: ;
558 | --tw-backdrop-sepia: ;
559 | }
560 |
561 | .container {
562 | width: 100%;
563 | }
564 |
565 | @media (min-width: 640px) {
566 | .container {
567 | max-width: 640px;
568 | }
569 | }
570 |
571 | @media (min-width: 768px) {
572 | .container {
573 | max-width: 768px;
574 | }
575 | }
576 |
577 | @media (min-width: 1024px) {
578 | .container {
579 | max-width: 1024px;
580 | }
581 | }
582 |
583 | @media (min-width: 1280px) {
584 | .container {
585 | max-width: 1280px;
586 | }
587 | }
588 |
589 | @media (min-width: 1536px) {
590 | .container {
591 | max-width: 1536px;
592 | }
593 | }
594 |
595 | .mx-auto {
596 | margin-left: auto;
597 | margin-right: auto;
598 | }
599 |
600 | .flex {
601 | display: flex;
602 | }
603 |
604 | .list-inside {
605 | list-style-position: inside;
606 | }
607 |
608 | .list-disc {
609 | list-style-type: disc;
610 | }
611 |
612 | .items-center {
613 | align-items: center;
614 | }
615 |
616 | .space-x-4 > :not([hidden]) ~ :not([hidden]) {
617 | --tw-space-x-reverse: 0;
618 | margin-right: calc(1rem * var(--tw-space-x-reverse));
619 | margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
620 | }
621 |
622 | .rounded-xl {
623 | border-radius: 0.75rem;
624 | }
625 |
626 | .bg-white {
627 | --tw-bg-opacity: 1;
628 | background-color: rgb(255 255 255 / var(--tw-bg-opacity));
629 | }
630 |
631 | .p-6 {
632 | padding: 1.5rem;
633 | }
634 |
635 | .shadow-md {
636 | --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
637 | --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
638 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
639 | }
--------------------------------------------------------------------------------