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 }