Skip to content

Commit

Permalink
feat: support parse as AST (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
JounQin committed Apr 3, 2022
1 parent 66b5efc commit c3c9aaa
Show file tree
Hide file tree
Showing 19 changed files with 2,506 additions and 202 deletions.
5 changes: 5 additions & 0 deletions .changeset/eighty-candles-own.md
@@ -0,0 +1,5 @@
---
"sh-syntax": minor
---

feat: support parse as AST
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Expand Up @@ -10,7 +10,6 @@ jobs:
strategy:
matrix:
node:
- 12
- 14
- 16
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Expand Up @@ -21,10 +21,10 @@ jobs:
with:
version: latest

- name: Setup Node.js 14.x
- name: Setup Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 14.x
node-version: 16.x
cache: pnpm

- name: Install Dependencies
Expand Down
25 changes: 19 additions & 6 deletions README.md
Expand Up @@ -43,19 +43,32 @@ npm i sh-syntax

```js
// node
import { print } from 'sh-syntax'

await print("echo 'Hello World!'")
import { parse, print } from 'sh-syntax'

const text = "echo 'Hello World!'"
const ast = await parse(text)
const newText = await print(ast, {
// `originalText` is required for now, hope we will find better solution later
originalText: text,
})
```

```js
// browser
import { getPrinter } from 'sh-syntax'
import { getProcessor } from 'sh-syntax'

const print = getPrinter(() =>
const processor = getProcessor(() =>
fetch('path/to/main.wasm').then(res => res.arrayBuffer()),
)
await print("echo 'Hello World!'")

const parse = (text, options) => processor(text, options)

const print = (ast, options) => processor(ast, options)

// just like node again
const text = "echo 'Hello World!'"
const ast = await parse(text)
const newText = await print(ast, { originalText: text })
```

## Changelog
Expand Down
6 changes: 0 additions & 6 deletions go.sum
@@ -1,26 +1,20 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/frankban/quicktest v1.13.1 h1:xVm/f9seEhZFL9+n5kv5XLrGwy6elc4V9v/XFY2vmd8=
github.com/frankban/quicktest v1.13.1/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 h1:d1PiN4RxzIFXCJTvRkvSkKqwtRAl5ZV4lATKtQI0B7I=
github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210925032602-92d5a993a665/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
Expand Down
198 changes: 166 additions & 32 deletions main.go
Expand Up @@ -7,6 +7,7 @@ import (
"bytes"
"flag"
"io"
"reflect"
"syscall/js"

"mvdan.cc/sh/v3/syntax"
Expand All @@ -18,9 +19,11 @@ var (
)

var (
uid = flag.String("uid", "", "unique ID")
text = flag.String("text", "", "processing text")
uid = flag.String("uid", "", "the unique ID")
text = flag.String("text", "", "the processing text")
filepath = flag.String("filepath", "", "file path of the processing text")
ast = flag.String("ast", "", "AST of the processing text")
originalText = flag.String("originalText", "", "original processing text for AST")
keepComments = flag.Bool("keepComments", false, "KeepComments makes the parser parse comments and attach them to nodes, as opposed to discarding them.")
stopAt = flag.String("stopAt", "", `StopAt configures the lexer to stop at an arbitrary word, treating it as if it were the end of the input. It can contain any characters except whitespace, and cannot be over four bytes in size.
This can be useful to embed shell code within another language, as one can use a special word to mark the delimiters between the two.
Expand All @@ -37,9 +40,129 @@ Note that this feature is best-effort and will only keep the alignment stable, s
functionNextLine = flag.Bool("functionNextLine", false, "FunctionNextLine will place a function's opening braces on the next line.")
)

func main() {
flag.Parse()
func mapParseError(err error) interface{} {
if err == nil {
return nil
}

parseError, ok := err.(syntax.ParseError)

if ok {
return map[string]interface{}{
"Filename": parseError.Filename,
"Text": parseError.Text,
"Incomplete": parseError.Incomplete,
"Pos": posToMap(parseError.Pos),
}
}

return err.Error()
}

func Map(in interface{}, fn func(interface{}) interface{}) interface{} {
val := reflect.ValueOf(in)
out := make([]interface{}, val.Len())

for i := 0; i < val.Len(); i++ {
out[i] = fn(val.Index(i).Interface())
}

return out
}

func posToMap(Pos syntax.Pos) map[string]interface{} {
return map[string]interface{}{
"Offset": Pos.Offset(),
"Line": Pos.Line(),
"Col": Pos.Col(),
}
}

func handleNode(node syntax.Node) interface{} {
if node == nil {
return nil
}
return map[string]interface{}{
"Pos": posToMap(node.Pos()),
"End": posToMap(node.End()),
}
}

func handleComments(comments []syntax.Comment) []interface{} {
return Map(comments, func(val interface{}) interface{} {
curr := val.(syntax.Comment)
return map[string]interface{}{
"Hash": posToMap(curr.Hash),
"Text": curr.Text,
"Pos": posToMap(curr.Pos()),
"End": posToMap(curr.End()),
}
}).([]interface{})
}

func handleWord(word *syntax.Word) interface{} {
if word == nil {
return nil
}
return map[string]interface{}{
"Parts": Map(word.Parts, func(val interface{}) interface{} {
curr := val.(syntax.WordPart)
return handleNode(curr)
}),
"Lit": word.Lit(),
"Pos": posToMap(word.Pos()),
"End": posToMap(word.End()),
}
}

func fileToMap(file syntax.File) map[string]interface{} {
return map[string]interface{}{
"Name": file.Name,
"Stmt": Map(file.Stmts, func(val interface{}) interface{} {
curr := val.(*syntax.Stmt)
return map[string]interface{}{
"Comments": handleComments(curr.Comments),
"Cmd": handleNode(curr.Cmd),
"Position": posToMap(curr.Position),
"Semicolon": posToMap(curr.Semicolon),
"Negated": curr.Negated,
"Background": curr.Background,
"Coprocess": curr.Coprocess,
"Redirs": Map(curr.Redirs, func(val interface{}) interface{} {
curr := val.(*syntax.Redirect)
var N interface{}
if curr.N != nil {
ValuePos := posToMap(curr.N.Pos())
ValueEnd := posToMap(curr.N.End())
N = map[string]interface{}{
"ValuePos": ValuePos,
"ValueEnd": ValueEnd,
"Value": curr.N.Value,
"Pos": ValuePos,
"End": ValueEnd,
}
}
return map[string]interface{}{
"OpPos": posToMap(curr.OpPos),
"Op": curr.Op.String(),
"N": N,
"Word": handleWord(curr.Word),
"Hdoc": handleWord(curr.Hdoc),
"Pos": posToMap(curr.Pos()),
"End": posToMap(curr.End()),
}
}),
"Pos": posToMap(curr.Pos()),
"End": posToMap(curr.End()),
}
}),
"Last": handleComments(file.Last),
"Pos": posToMap(file.Pos()),
"End": posToMap(file.End()),
}
}

func parse(text string, filepath string) (*syntax.File, error) {
var options []syntax.ParserOption

options = append(options, syntax.KeepComments(*keepComments), syntax.Variant(syntax.LangVariant(*variant)))
Expand All @@ -50,51 +173,62 @@ func main() {

parser = syntax.NewParser(options...)

return parser.Parse(bytes.NewReader([]byte(text)), filepath)
}

func print(originalText string, filepath string) (string, error) {
file, err := parse(originalText, filepath)

if err != nil {
return "", err
}

printer = syntax.NewPrinter(
syntax.Indent(*indent),
syntax.BinaryNextLine(*binaryNextLine),
syntax.SwitchCaseIndent(*switchCaseIndent),
syntax.SpaceRedirects(*spaceRedirects),
syntax.KeepPadding(*keepPadding),
syntax.Minify(*minify),
syntax.FunctionNextLine(*functionNextLine),
)

var buf bytes.Buffer
writer := io.Writer(&buf)

err = printer.Print(writer, file)

if err != nil {
return "", err
}

return buf.String(), err
}

func main() {
flag.Parse()

Go := js.Global().Get("Go")

if Go.Get("__shProcessing").IsUndefined() {
Go.Set("__shProcessing", js.ValueOf(map[string]interface{}{}))
}

result := map[string]interface{}{}
var Data interface{}
var Error interface{}

file, err := parser.Parse(bytes.NewReader([]byte(*text)), "path")
if err != nil {
error, ok := err.(syntax.ParseError)
if ok {
result["error"] = map[string]interface{}{
"filename": error.Filename,
"incomplete": error.Incomplete,
"text": error.Text,
"pos": map[string]interface{}{
"col": error.Col(),
"line": error.Line(),
"offset": error.Offset(),
},
"message": error.Error(),
}
} else {
result["error"] = err.Error()
}
if *ast == "" {
file, err := parse(*text, *filepath)
Data = fileToMap(*file)
Error = mapParseError(err)
} else {
var buf bytes.Buffer
writer := io.Writer(&buf)
err = printer.Print(writer, file)
if err != nil {
result["error"] = err.Error()
} else {
result["text"] = buf.String()
}
result, err := print(*originalText, *filepath)
Data = result
Error = mapParseError(err)
}

Go.Get("__shProcessing").Set(*uid, js.ValueOf(result))
Go.Get("__shProcessing").Set(*uid, js.ValueOf(map[string]interface{}{
"Data": Data,
"Error": Error,
}))
}
Binary file modified main.wasm
Binary file not shown.
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -7,7 +7,7 @@
"author": "JounQin <admin@1stg.me>",
"license": "MIT",
"engines": {
"node": ">=v12.20"
"node": ">=v14"
},
"exports": {
"import": "./lib/index.js",
Expand Down Expand Up @@ -40,7 +40,7 @@
"tslib": "^2.3.1"
},
"devDependencies": {
"@1stg/lib-config": "^5.3.0",
"@1stg/lib-config": "^5.4.0",
"@changesets/changelog-github": "^0.4.4",
"@changesets/cli": "^2.22.0",
"@types/jest": "^27.4.1",
Expand Down

0 comments on commit c3c9aaa

Please sign in to comment.