forked from Botanical/BotanJS
211 lines
4.6 KiB
Go
211 lines
4.6 KiB
Go
package classmap
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
reNamespace = regexp.MustCompile(`__namespace\s*\(\s*['\"]([^'\"]+)['\"]\s*\)`)
|
|
reImport = regexp.MustCompile(`__import\s*\(\s*['\"]([^'\"]+)['\"]\s*\)`)
|
|
reInvoke = regexp.MustCompile(`ns\s*\[\s*NS_INVOKE\s*\]\s*\(\s*['\"]([^'\"]+)['\"]\s*\)`)
|
|
reExport = regexp.MustCompile(`ns\s*\[\s*NS_EXPORT\s*\]\s*\(\s*EX_([A-Z_]+[A-Z])\s*,\s*['\"]([^'\"]+)['\"]\s*,`)
|
|
)
|
|
|
|
type Export struct {
|
|
Type string
|
|
Name string
|
|
}
|
|
|
|
type Meta struct {
|
|
Namespace string
|
|
Imports []string
|
|
Exports []Export
|
|
}
|
|
|
|
func ParseFile(path string) (Meta, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return Meta{}, err
|
|
}
|
|
defer f.Close()
|
|
|
|
var m Meta
|
|
s := bufio.NewScanner(f)
|
|
// Some old JS files can have long one-line wrappers.
|
|
s.Buffer(make([]byte, 0, 64*1024), 8*1024*1024)
|
|
for s.Scan() {
|
|
line := s.Text()
|
|
if x := reNamespace.FindStringSubmatch(line); x != nil {
|
|
m.Namespace = x[1]
|
|
continue
|
|
}
|
|
if x := reImport.FindStringSubmatch(line); x != nil {
|
|
m.Imports = append(m.Imports, x[1])
|
|
continue
|
|
}
|
|
if x := reInvoke.FindStringSubmatch(line); x != nil {
|
|
m.Imports = append(m.Imports, m.Namespace+"."+x[1])
|
|
continue
|
|
}
|
|
if x := reExport.FindStringSubmatch(line); x != nil {
|
|
m.Exports = append(m.Exports, Export{Type: x[1], Name: x[2]})
|
|
continue
|
|
}
|
|
}
|
|
return m, s.Err()
|
|
}
|
|
|
|
func Build(root string) (*Map, error) {
|
|
root, err := filepath.Abs(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m := &Map{Symbols: map[string]Symbol{}, Files: map[string]Resource{}}
|
|
head := filepath.Join(root, "_this.js")
|
|
|
|
err = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
if path == filepath.Join(root, "externs") {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if filepath.Ext(path) != ".js" || path == head {
|
|
return nil
|
|
}
|
|
meta, err := ParseFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if meta.Namespace == "" {
|
|
return fmt.Errorf("%s: missing __namespace", path)
|
|
}
|
|
rel, err := filepath.Rel(root, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel = filepath.ToSlash(rel)
|
|
res := Resource{Src: rel, JSHash: fileHash(path), CSSHash: fileHash(strings.TrimSuffix(path, ".js") + ".css")}
|
|
m.Files[rel] = res
|
|
srcEvery := meta.Namespace != className(rel)
|
|
|
|
ensureParents(m, meta.Namespace)
|
|
if !srcEvery {
|
|
sym := m.Symbols[meta.Namespace]
|
|
sym.Kind = KindClass
|
|
sym.Resource = res
|
|
sym.Imports = appendUnique(sym.Imports, meta.Imports...)
|
|
m.Symbols[meta.Namespace] = sym
|
|
}
|
|
|
|
for _, ex := range meta.Exports {
|
|
kind := exportKind(ex.Type)
|
|
name := meta.Namespace + "." + ex.Name
|
|
ensureParents(m, name)
|
|
sym := m.Symbols[name]
|
|
sym.Kind = kind
|
|
if srcEvery {
|
|
sym.Resource = res
|
|
if kind == KindClass {
|
|
sym.Imports = appendUnique(sym.Imports, meta.Imports...)
|
|
}
|
|
}
|
|
m.Symbols[name] = sym
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func ensureParents(m *Map, name string) {
|
|
parts := strings.Split(name, ".")
|
|
for i := range parts {
|
|
full := strings.Join(parts[:i+1], ".")
|
|
if _, ok := m.Symbols[full]; !ok {
|
|
parent := ""
|
|
if i > 0 {
|
|
parent = strings.Join(parts[:i], ".")
|
|
}
|
|
m.Symbols[full] = Symbol{Name: full, Kind: KindClass, Parent: parent}
|
|
}
|
|
}
|
|
}
|
|
|
|
func className(rel string) string {
|
|
name := strings.TrimSuffix(filepath.ToSlash(rel), ".js")
|
|
name = strings.ReplaceAll(name, "/", ".")
|
|
name = strings.ReplaceAll(name, "._this", "")
|
|
name = strings.ReplaceAll(name, "..BotanJS.", "")
|
|
return name
|
|
}
|
|
|
|
func exportKind(t string) Kind {
|
|
switch t {
|
|
case "CLASS":
|
|
return KindClass
|
|
case "FUNC":
|
|
return KindMethod
|
|
default:
|
|
return KindProp
|
|
}
|
|
}
|
|
|
|
func fileHash(path string) string {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return "1"
|
|
}
|
|
defer f.Close()
|
|
h := sha1.New()
|
|
_, _ = io.Copy(h, f)
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
func appendUnique(dst []string, vals ...string) []string {
|
|
seen := map[string]bool{}
|
|
for _, v := range dst {
|
|
seen[v] = true
|
|
}
|
|
for _, v := range vals {
|
|
if v == "" || seen[v] {
|
|
continue
|
|
}
|
|
dst = append(dst, v)
|
|
seen[v] = true
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func SortedSymbols(m *Map) []Symbol {
|
|
out := make([]Symbol, 0, len(m.Symbols))
|
|
for _, s := range m.Symbols {
|
|
out = append(out, s)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out
|
|
}
|
|
|
|
func SortedFiles(m *Map) []Resource {
|
|
out := make([]Resource, 0, len(m.Files))
|
|
for _, r := range m.Files {
|
|
out = append(out, r)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Src < out[j].Src })
|
|
return out
|
|
}
|