forked from Botanical/BotanJS
Deprecating old approach
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/tgckpg/botanres-go/internal/classmap"
|
||||
)
|
||||
|
||||
type OutputMode string
|
||||
|
||||
const (
|
||||
ModeJS OutputMode = "js"
|
||||
ModeCSS OutputMode = "css"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Mode OutputMode
|
||||
Request []string
|
||||
JSFiles []classmap.Resource
|
||||
CSSFiles []classmap.Resource
|
||||
Hash string
|
||||
Content []byte
|
||||
ContentType string
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
Root string
|
||||
Map *classmap.Map
|
||||
}
|
||||
|
||||
func New(root string, m *classmap.Map) (*Resolver, error) {
|
||||
root, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Resolver{Root: root, Map: m}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) ResolveRequest(code string, mode OutputMode, excludes []string) (Result, error) {
|
||||
apis, err := DecodeRequest(code)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
var includeSyms []classmap.Symbol
|
||||
var excludeSyms []classmap.Symbol
|
||||
for _, api := range apis {
|
||||
if api == "" {
|
||||
continue
|
||||
}
|
||||
neg := strings.HasPrefix(api, "-")
|
||||
api = strings.TrimPrefix(api, "-")
|
||||
syms, err := r.ResolveSymbol(api)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if neg {
|
||||
excludeSyms = append(excludeSyms, syms...)
|
||||
} else {
|
||||
includeSyms = append(includeSyms, syms...)
|
||||
}
|
||||
}
|
||||
for _, ex := range excludes {
|
||||
syms, err := r.ResolveSymbol(ex)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
excludeSyms = append(excludeSyms, syms...)
|
||||
}
|
||||
|
||||
jsFiles := r.resourcesFor(includeSyms)
|
||||
jsExcludes := r.resourcesFor(excludeSyms)
|
||||
jsFiles = subtractResources(jsFiles, jsExcludes)
|
||||
|
||||
res := Result{Mode: mode, Request: apis, JSFiles: jsFiles}
|
||||
switch mode {
|
||||
case ModeJS:
|
||||
content, hash, err := r.MergeJS(jsFiles)
|
||||
res.Hash, res.Content, res.ContentType = hash, content, "application/javascript"
|
||||
return res, err
|
||||
case ModeCSS:
|
||||
cssIn := r.cssResources(jsFiles)
|
||||
cssEx := r.cssResources(jsExcludes)
|
||||
cssFiles := subtractResources(cssIn, cssEx)
|
||||
content, hash, err := r.MergeCSS(cssFiles)
|
||||
res.CSSFiles, res.Hash, res.Content, res.ContentType = cssFiles, hash, content, "text/css"
|
||||
return res, err
|
||||
default:
|
||||
return Result{}, fmt.Errorf("invalid mode: %s", mode)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) ResolveSymbol(name string) ([]classmap.Symbol, error) {
|
||||
seen := map[string]bool{}
|
||||
var out []classmap.Symbol
|
||||
if strings.HasSuffix(name, ".*") {
|
||||
prefix := strings.TrimSuffix(name, "*")
|
||||
var keys []string
|
||||
for k, s := range r.Map.Symbols {
|
||||
if strings.HasPrefix(k, prefix) && s.Kind == classmap.KindClass && k != strings.TrimSuffix(prefix, ".") {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, fmt.Errorf("namespace does not exist or contains no classes: %s", name)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
if err := r.resolveOne(k, seen, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
if err := r.resolveOne(name, seen, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) resolveOne(name string, seen map[string]bool, out *[]classmap.Symbol) error {
|
||||
if seen[name] {
|
||||
return nil
|
||||
}
|
||||
sym, ok := r.Map.Symbols[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("no such class: %s", name)
|
||||
}
|
||||
seen[name] = true
|
||||
imports := sym.Imports
|
||||
// Old resolver uses parent imports when the found node is prop/method.
|
||||
if sym.Kind != classmap.KindClass && sym.Parent != "" {
|
||||
if p, ok := r.Map.Symbols[sym.Parent]; ok {
|
||||
imports = p.Imports
|
||||
}
|
||||
}
|
||||
for _, imp := range imports {
|
||||
if err := r.resolveOne(imp, seen, out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
*out = append(*out, sym)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resolver) resourcesFor(syms []classmap.Symbol) []classmap.Resource {
|
||||
var out []classmap.Resource
|
||||
for _, s := range syms {
|
||||
res, ok := r.resourceFor(s)
|
||||
if ok {
|
||||
out = appendResource(out, res)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Resolver) resourceFor(s classmap.Symbol) (classmap.Resource, bool) {
|
||||
if s.Resource.Src != "" {
|
||||
return s.Resource, true
|
||||
}
|
||||
for p := s.Parent; p != ""; {
|
||||
ps, ok := r.Map.Symbols[p]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if ps.Resource.Src != "" {
|
||||
return ps.Resource, true
|
||||
}
|
||||
p = ps.Parent
|
||||
}
|
||||
return classmap.Resource{}, false
|
||||
}
|
||||
|
||||
func (r *Resolver) cssResources(jsFiles []classmap.Resource) []classmap.Resource {
|
||||
var out []classmap.Resource
|
||||
for _, res := range jsFiles {
|
||||
if res.CSSHash != "1" {
|
||||
out = appendResource(out, res)
|
||||
}
|
||||
parts := strings.Split(res.Src, "/")
|
||||
for i := 1; i < len(parts); i++ {
|
||||
key := strings.Join(parts[:len(parts)-i], "/") + "/_this.js"
|
||||
def, ok := r.Map.Files[key]
|
||||
if ok && def.CSSHash != "1" {
|
||||
out = appendResource(out, def)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool { return out[i].Src < out[j].Src })
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Resolver) MergeJS(files []classmap.Resource) ([]byte, string, error) {
|
||||
var b bytes.Buffer
|
||||
head, _ := os.ReadFile(filepath.Join(r.Root, "_this.js"))
|
||||
b.Write(head)
|
||||
for _, f := range files {
|
||||
pathName := strings.TrimSuffix(f.Src, ".js")
|
||||
pathName = strings.ReplaceAll(pathName, "/", ".")
|
||||
pathName = strings.ReplaceAll(pathName, "._this", "")
|
||||
b.WriteString(";BotanJS.define(\"")
|
||||
b.WriteString(pathName)
|
||||
b.WriteString("\");")
|
||||
src, err := os.ReadFile(filepath.Join(r.Root, filepath.FromSlash(f.Src)))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
b.Write(src)
|
||||
}
|
||||
wrapped := append([]byte("(function(){"), b.Bytes()...)
|
||||
wrapped = append(wrapped, []byte("})();")...)
|
||||
return wrapped, hashList(files, "js"), nil
|
||||
}
|
||||
|
||||
func (r *Resolver) MergeCSS(files []classmap.Resource) ([]byte, string, error) {
|
||||
var b bytes.Buffer
|
||||
b.WriteString("/* @ */")
|
||||
head, _ := os.ReadFile(filepath.Join(r.Root, "_this.css"))
|
||||
b.Write(head)
|
||||
for _, f := range files {
|
||||
cssPath := strings.TrimSuffix(f.Src, ".js") + ".css"
|
||||
src, err := os.ReadFile(filepath.Join(r.Root, filepath.FromSlash(cssPath)))
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
return nil, "", err
|
||||
}
|
||||
b.Write(src)
|
||||
}
|
||||
return b.Bytes(), hashList(files, "css"), nil
|
||||
}
|
||||
|
||||
func DecodeRequest(code string) ([]string, error) {
|
||||
sep := "/"
|
||||
decoded := code
|
||||
if raw, err := base64.StdEncoding.DecodeString(code); err == nil {
|
||||
zr, err := zlib.NewReader(bytes.NewReader(raw))
|
||||
if err == nil {
|
||||
buf, readErr := io.ReadAll(zr)
|
||||
_ = zr.Close()
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
decoded = string(buf)
|
||||
sep = ","
|
||||
}
|
||||
}
|
||||
cleaner := regexp.MustCompile(`[^A-Za-z.\*\-_/ ,]`)
|
||||
decoded = cleaner.ReplaceAllString(decoded, "")
|
||||
var fields []string
|
||||
for _, p := range strings.Split(decoded, sep) {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
fields = append(fields, p)
|
||||
}
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func appendResource(out []classmap.Resource, res classmap.Resource) []classmap.Resource {
|
||||
for _, x := range out {
|
||||
if x.Src == res.Src {
|
||||
return out
|
||||
}
|
||||
}
|
||||
return append(out, res)
|
||||
}
|
||||
|
||||
func subtractResources(in, ex []classmap.Resource) []classmap.Resource {
|
||||
drop := map[string]bool{}
|
||||
for _, x := range ex {
|
||||
drop[x.Src] = true
|
||||
}
|
||||
var out []classmap.Resource
|
||||
for _, x := range in {
|
||||
if !drop[x.Src] {
|
||||
out = append(out, x)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func hashList(files []classmap.Resource, mode string) string {
|
||||
var parts []string
|
||||
for _, f := range files {
|
||||
if mode == "css" {
|
||||
parts = append(parts, f.CSSHash)
|
||||
} else {
|
||||
parts = append(parts, f.JSHash)
|
||||
}
|
||||
}
|
||||
sum := md5.Sum([]byte(strings.Join(parts, "|")))
|
||||
return hex.EncodeToString(sum[:]) + "." + mode
|
||||
}
|
||||
Reference in New Issue
Block a user