penguin/AstroJS

Javascript framework for my blog

botanres-go/internal/classmap/scan.go

raw ยท 4703 bytes

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
}