changeset 2:d19133be91ba

ndex and smarter parser
author Atarwn Gard <a@qwa.su>
date Mon, 09 Mar 2026 01:55:11 +0500
parents 3e7247db5c6e
children eb705d4cdcd7
files icf/icf.go main.go
diffstat 2 files changed, 152 insertions(+), 116 deletions(-) [+]
line wrap: on
line diff
diff -r 3e7247db5c6e -r d19133be91ba icf/icf.go
--- a/icf/icf.go	Mon Mar 09 01:04:16 2026 +0500
+++ b/icf/icf.go	Mon Mar 09 01:55:11 2026 +0500
@@ -2,16 +2,6 @@
 //
 // ICF is a rule-based configuration format with variables, abstract blocks
 // (mixins), pattern matching with named capture groups, and brace expansion.
-//
-// Syntax:
-//
-//	; comment
-//	KEY=value                       variable
-//	@name                           abstract block (mixin)
-//	|> directive arg {a,b}          directive with optional brace expansion
-//	block.id @mixin                 concrete block inheriting a mixin
-//	<cap>.example.com               block with named capture group
-//	<_>.example.com                 anonymous wildcard
 package icf
 
 import (
@@ -21,13 +11,11 @@
 	"strings"
 )
 
-// Directive is a single key + arguments line inside a block.
 type Directive struct {
 	Key  string
 	Args []string
 }
 
-// Config holds the parsed and fully resolved state of an ICF file.
 type Config struct {
 	vars     map[string]string
 	abstract map[string][]Directive
@@ -40,40 +28,54 @@
 	Directives []Directive
 }
 
-// Parse reads an ICF document and returns a ready Config.
+type rawBlock struct {
+	id         string
+	mixin      string
+	directives []rawDirective
+}
+
+type rawDirective struct {
+	key  string
+	args []string // after brace expansion, before var substitution
+}
+
 func Parse(r io.Reader) (*Config, error) {
-	c := &Config{
-		vars:     make(map[string]string),
-		abstract: make(map[string][]Directive),
+	lines, err := readLines(r)
+	if err != nil {
+		return nil, err
 	}
 
-	var raw []ParsedBlock
-	var cur *ParsedBlock
+	// --- Pass 1: collect variables ---
+	vars := make(map[string]string)
+	for _, line := range lines {
+		if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") {
+			key := line[:i]
+			if isVarName(key) {
+				vars[key] = strings.TrimSpace(line[i+1:])
+			}
+		}
+	}
+
+	subst := makeSubst(vars)
+
+	// --- Pass 2: parse blocks (raw, no capture substitution yet) ---
+	var raws []rawBlock
+	var cur *rawBlock
 
 	flush := func() {
 		if cur != nil {
-			raw = append(raw, *cur)
+			raws = append(raws, *cur)
 			cur = nil
 		}
 	}
 
-	scanner := bufio.NewScanner(r)
-	for scanner.Scan() {
-		line := stripComment(strings.TrimSpace(scanner.Text()))
-		if line == "" {
-			continue
-		}
-
-		// Variable assignment: KEY=value
+	for _, line := range lines {
 		if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") {
-			key := line[:i]
-			if isVarName(key) {
-				c.vars[key] = strings.TrimSpace(line[i+1:])
+			if isVarName(line[:i]) {
 				continue
 			}
 		}
 
-		// Directive: |> key args...
 		if strings.HasPrefix(line, "|>") {
 			if cur == nil {
 				return nil, fmt.Errorf("icf: directive outside block: %q", line)
@@ -82,53 +84,62 @@
 			if len(parts) == 0 {
 				continue
 			}
-			cur.Directives = append(cur.Directives, Directive{
-				Key:  parts[0],
-				Args: braceExpand(parts[1:]),
+			cur.directives = append(cur.directives, rawDirective{
+				key:  parts[0],
+				args: braceExpand(parts[1:]),
 			})
 			continue
 		}
 
-		// Block header: id [@mixin]
 		flush()
 		parts := strings.Fields(line)
-		pb := ParsedBlock{ID: parts[0]}
+		rb := rawBlock{id: subst(parts[0], nil)}
 		if len(parts) >= 2 && strings.HasPrefix(parts[1], "@") {
-			pb.Mixin = parts[1][1:]
+			rb.mixin = subst(parts[1][1:], nil)
 		}
-		cur = &pb
+		cur = &rb
 	}
 	flush()
 
-	if err := scanner.Err(); err != nil {
-		return nil, err
+	// --- Pass 3: separate abstract from concrete, apply var substitution ---
+	c := &Config{
+		vars:     vars,
+		abstract: make(map[string][]Directive),
 	}
 
-	// Separate abstract from concrete blocks
-	for _, b := range raw {
-		if strings.HasPrefix(b.ID, "@") {
-			c.abstract[b.ID[1:]] = b.Directives
+	for _, rb := range raws {
+		dirs := make([]Directive, len(rb.directives))
+		for i, rd := range rb.directives {
+			args := make([]string, len(rd.args))
+			for j, a := range rd.args {
+				args[j] = subst(a, nil)
+			}
+			dirs[i] = Directive{Key: rd.key, Args: args}
+		}
+
+		if strings.HasPrefix(rb.id, "@") {
+			c.abstract[rb.id[1:]] = dirs
 		} else {
-			c.Blocks = append(c.Blocks, b)
+			c.Blocks = append(c.Blocks, ParsedBlock{
+				ID:         rb.id,
+				Mixin:      rb.mixin,
+				Directives: dirs,
+			})
 		}
 	}
 
 	return c, nil
 }
 
-// Abstract returns the directives of a named abstract block with variables
-// substituted. Returns nil if not found.
+// Abstract returns the directives of a named abstract block.
+// Returns nil if not found.
 func (c *Config) Abstract(name string) []Directive {
-	dirs, ok := c.abstract[name]
-	if !ok {
-		return nil
-	}
-	return c.applyVars(dirs, nil)
+	return c.abstract[name]
 }
 
 // Match finds the most specific block matching input (e.g. "host/path") and
 // returns resolved directives plus named captures.
-// Domain part is matched exactly (with captures); path part uses prefix match.
+// Domain is matched exactly; path uses prefix match.
 func (c *Config) Match(input string) ([]Directive, map[string]string) {
 	inHost, inPath, _ := strings.Cut(input, "/")
 
@@ -141,7 +152,6 @@
 
 	for _, b := range c.Blocks {
 		patHost, patPath, hasPath := strings.Cut(b.ID, "/")
-
 		caps := make(map[string]string)
 
 		domScore, ok := matchExact(patHost, inHost, caps)
@@ -170,55 +180,80 @@
 	return c.ResolveBlock(best.block, best.captures), best.captures
 }
 
-// ResolveBlock merges mixin then block directives, substituting vars+captures.
+// ResolveBlock merges mixin directives (lower priority) with block directives,
+// then substitutes capture variables.
 func (c *Config) ResolveBlock(b ParsedBlock, caps map[string]string) []Directive {
 	var merged []Directive
 	if b.Mixin != "" {
 		merged = append(merged, c.abstract[b.Mixin]...)
 	}
 	merged = append(merged, b.Directives...)
-	return c.applyVars(merged, caps)
-}
+
+	if len(caps) == 0 {
+		return merged
+	}
 
-func (c *Config) applyVars(dirs []Directive, caps map[string]string) []Directive {
-	out := make([]Directive, len(dirs))
-	for i, d := range dirs {
-		out[i].Key = c.subst(d.Key, caps)
+	// Substitute capture variables into a copy
+	subst := makeSubst(c.vars)
+	out := make([]Directive, len(merged))
+	for i, d := range merged {
+		out[i].Key = d.Key
 		out[i].Args = make([]string, len(d.Args))
 		for j, a := range d.Args {
-			out[i].Args[j] = c.subst(a, caps)
+			out[i].Args[j] = subst(a, caps)
 		}
 	}
 	return out
 }
 
-func (c *Config) subst(s string, caps map[string]string) string {
-	if !strings.Contains(s, "$") {
-		return s
-	}
-	var b strings.Builder
-	i := 0
-	for i < len(s) {
-		if s[i] != '$' {
-			b.WriteByte(s[i])
-			i++
-			continue
+// makeSubst returns a function that substitutes $VAR in s,
+// checking caps first, then vars.
+func makeSubst(vars map[string]string) func(s string, caps map[string]string) string {
+	return func(s string, caps map[string]string) string {
+		if !strings.Contains(s, "$") {
+			return s
 		}
-		j := i + 1
-		for j < len(s) && isVarChar(s[j]) {
-			j++
+		var b strings.Builder
+		i := 0
+		for i < len(s) {
+			if s[i] != '$' {
+				b.WriteByte(s[i])
+				i++
+				continue
+			}
+			j := i + 1
+			for j < len(s) && isVarChar(s[j]) {
+				j++
+			}
+			name := s[i+1 : j]
+			if caps != nil {
+				if v, ok := caps[name]; ok {
+					b.WriteString(v)
+					i = j
+					continue
+				}
+			}
+			if v, ok := vars[name]; ok {
+				b.WriteString(v)
+			} else {
+				b.WriteString(s[i:j])
+			}
+			i = j
 		}
-		name := s[i+1 : j]
-		if v, ok := caps[name]; ok {
-			b.WriteString(v)
-		} else if v, ok := c.vars[name]; ok {
-			b.WriteString(v)
-		} else {
-			b.WriteString(s[i:j])
+		return b.String()
+	}
+}
+
+func readLines(r io.Reader) ([]string, error) {
+	var out []string
+	scanner := bufio.NewScanner(r)
+	for scanner.Scan() {
+		line := stripComment(strings.TrimSpace(scanner.Text()))
+		if line != "" {
+			out = append(out, line)
 		}
-		i = j
 	}
-	return b.String()
+	return out, scanner.Err()
 }
 
 func matchExact(pat, s string, caps map[string]string) (int, bool) {
@@ -249,12 +284,10 @@
 			rest := pat[end+1:]
 
 			for split := 1; split <= len(inp); split++ {
-				candidate := inp[:split]
-				remaining := inp[split:]
-				score, finalRem, ok := matchCaptures(rest, remaining, caps)
+				score, finalRem, ok := matchCaptures(rest, inp[split:], caps)
 				if ok {
 					if capName != "_" {
-						caps[capName] = candidate
+						caps[capName] = inp[:split]
 					}
 					return score, finalRem, true
 				}
@@ -318,4 +351,4 @@
 func isVarChar(c byte) bool {
 	return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
 		(c >= '0' && c <= '9') || c == '_'
-}
+}
\ No newline at end of file
diff -r 3e7247db5c6e -r d19133be91ba main.go
--- a/main.go	Mon Mar 09 01:04:16 2026 +0500
+++ b/main.go	Mon Mar 09 01:55:11 2026 +0500
@@ -160,29 +160,23 @@
 
 func (h *handler) serve(w http.ResponseWriter, r *http.Request, dirs []icf.Directive, _ map[string]string) {
 	var (
-		rootDir   string
-		rootIndex []string
-		fcgiAddr  string
-		fcgiPat   string
-		rprxAddr  string
+		rootDir  string
+		rootShow bool
+		ndex     []string
+		fcgiAddr string
+		fcgiPat  string
+		rprxAddr string
+		rdirCode int
+		rdirURL  string
 	)
 
 	for _, d := range dirs {
 		switch d.Key {
 		case "root":
 			rootDir = safeArg(d.Args, 0)
-			switch safeArg(d.Args, 1) {
-			case "show":
-				if len(d.Args) >= 3 {
-					rootIndex = d.Args[2:]
-				} else {
-					rootIndex = []string{"index.html"}
-				}
-			case "hide", "":
-				rootIndex = nil
-			default:
-				log.Printf("d2o: root: unknown mode %q (want show|hide)", safeArg(d.Args, 1))
-			}
+			rootShow = safeArg(d.Args, 1) == "show"
+		case "ndex":
+			ndex = d.Args
 		case "fcgi":
 			fcgiAddr = safeArg(d.Args, 0)
 			fcgiPat = safeArg(d.Args, 1)
@@ -191,9 +185,19 @@
 			}
 		case "rprx":
 			rprxAddr = safeArg(d.Args, 0)
+		case "rdir":
+			rdirCode, _ = strconv.Atoi(safeArg(d.Args, 0))
+			rdirURL = safeArg(d.Args, 1)
 		}
 	}
 
+	if rdirURL != "" {
+		if rdirCode == 0 {
+			rdirCode = http.StatusFound
+		}
+		http.Redirect(w, r, rdirURL, rdirCode)
+		return
+	}
 	if rprxAddr != "" {
 		serveReverseProxy(w, r, rprxAddr)
 		return
@@ -206,7 +210,7 @@
 		return
 	}
 	if rootDir != "" {
-		serveStatic(w, r, rootDir, rootIndex)
+		serveStatic(w, r, rootDir, rootShow, ndex)
 		return
 	}
 
@@ -214,8 +218,7 @@
 }
 
 // --- Static -----------------------------------------------------------------
-
-func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, rootIndex []string) {
+func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, show bool, ndex []string) {
 	fpath := filepath.Join(rootDir, filepath.FromSlash(path.Clean(r.URL.Path)))
 
 	info, err := os.Stat(fpath)
@@ -229,17 +232,17 @@
 	}
 
 	if info.IsDir() {
-		if rootIndex == nil {
-			http.Error(w, "forbidden", http.StatusForbidden)
-			return
-		}
-		for _, idx := range rootIndex {
+		for _, idx := range ndex {
 			idxPath := filepath.Join(fpath, idx)
 			if _, err := os.Stat(idxPath); err == nil {
 				http.ServeFile(w, r, idxPath)
 				return
 			}
 		}
+		if !show {
+			http.Error(w, "forbidden", http.StatusForbidden)
+			return
+		}
 		listDir(w, r, fpath, r.URL.Path)
 		return
 	}
@@ -362,4 +365,4 @@
 	regPat := "^" + strings.ReplaceAll(regexp.QuoteMeta(pattern), regexp.QuoteMeta("*"), ".*") + "$"
 	matched, err := regexp.MatchString(regPat, s)
 	return err == nil && matched
-}
+}
\ No newline at end of file