Mercurial Hosting > d2o
annotate icf/icf.go @ 14:a51dfcb0faeb default tip
forgot to remove bestPatPath variable that's not used anymore
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Mon, 23 Mar 2026 13:48:32 +0500 |
| parents | 9460f83a5664 |
| children |
| rev | line source |
|---|---|
| 0 | 1 // Package icf implements the Inherited Configuration Format parser. |
| 2 // | |
| 3 // ICF is a rule-based configuration format with variables, abstract blocks | |
| 4 // (mixins), pattern matching with named capture groups, and brace expansion. | |
| 5 package icf | |
| 6 | |
| 7 import ( | |
| 8 "bufio" | |
| 9 "fmt" | |
| 10 "io" | |
| 13 | 11 "path" |
| 0 | 12 "strings" |
| 13 ) | |
| 14 | |
| 15 type Directive struct { | |
| 16 Key string | |
| 17 Args []string | |
| 18 } | |
| 19 | |
| 20 type Config struct { | |
|
6
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
21 vars map[string]string |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
22 abstract map[string][]Directive |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
23 abstractMixin map[string]string // mixin name for each abstract block |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
24 Blocks []ParsedBlock |
| 0 | 25 } |
| 26 | |
| 27 type ParsedBlock struct { | |
| 28 ID string | |
| 29 Mixin string | |
| 30 Directives []Directive | |
| 31 } | |
| 32 | |
| 2 | 33 type rawBlock struct { |
| 34 id string | |
| 35 mixin string | |
| 36 directives []rawDirective | |
| 37 } | |
| 38 | |
| 39 type rawDirective struct { | |
| 40 key string | |
| 41 args []string // after brace expansion, before var substitution | |
| 42 } | |
| 43 | |
| 0 | 44 func Parse(r io.Reader) (*Config, error) { |
| 2 | 45 lines, err := readLines(r) |
| 46 if err != nil { | |
| 47 return nil, err | |
| 0 | 48 } |
| 49 | |
| 2 | 50 // --- Pass 1: collect variables --- |
| 51 vars := make(map[string]string) | |
| 4 | 52 subst := makeSubst(vars) |
| 53 | |
| 2 | 54 for _, line := range lines { |
| 55 if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") { | |
| 56 key := line[:i] | |
| 57 if isVarName(key) { | |
| 4 | 58 vars[key] = subst(strings.TrimSpace(line[i+1:]), nil) |
| 2 | 59 } |
| 60 } | |
| 61 } | |
| 62 | |
| 63 // --- Pass 2: parse blocks (raw, no capture substitution yet) --- | |
| 64 var raws []rawBlock | |
| 65 var cur *rawBlock | |
| 0 | 66 |
| 67 flush := func() { | |
| 68 if cur != nil { | |
| 2 | 69 raws = append(raws, *cur) |
| 0 | 70 cur = nil |
| 71 } | |
| 72 } | |
| 73 | |
| 2 | 74 for _, line := range lines { |
| 0 | 75 if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") { |
| 2 | 76 if isVarName(line[:i]) { |
| 0 | 77 continue |
| 78 } | |
| 79 } | |
| 80 | |
| 81 if strings.HasPrefix(line, "|>") { | |
| 82 if cur == nil { | |
| 83 return nil, fmt.Errorf("icf: directive outside block: %q", line) | |
| 84 } | |
| 85 parts := strings.Fields(strings.TrimSpace(line[2:])) | |
| 86 if len(parts) == 0 { | |
| 87 continue | |
| 88 } | |
| 2 | 89 cur.directives = append(cur.directives, rawDirective{ |
| 90 key: parts[0], | |
| 91 args: braceExpand(parts[1:]), | |
| 0 | 92 }) |
| 93 continue | |
| 94 } | |
| 95 | |
| 96 flush() | |
| 97 parts := strings.Fields(line) | |
| 2 | 98 rb := rawBlock{id: subst(parts[0], nil)} |
| 0 | 99 if len(parts) >= 2 && strings.HasPrefix(parts[1], "@") { |
| 2 | 100 rb.mixin = subst(parts[1][1:], nil) |
| 0 | 101 } |
| 2 | 102 cur = &rb |
| 0 | 103 } |
| 104 flush() | |
| 105 | |
| 2 | 106 // --- Pass 3: separate abstract from concrete, apply var substitution --- |
| 107 c := &Config{ | |
|
6
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
108 vars: vars, |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
109 abstract: make(map[string][]Directive), |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
110 abstractMixin: make(map[string]string), |
| 0 | 111 } |
| 112 | |
| 2 | 113 for _, rb := range raws { |
| 114 dirs := make([]Directive, len(rb.directives)) | |
| 115 for i, rd := range rb.directives { | |
| 116 args := make([]string, len(rd.args)) | |
| 117 for j, a := range rd.args { | |
| 118 args[j] = subst(a, nil) | |
| 119 } | |
| 120 dirs[i] = Directive{Key: rd.key, Args: args} | |
| 121 } | |
| 122 | |
| 123 if strings.HasPrefix(rb.id, "@") { | |
|
6
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
124 name := rb.id[1:] |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
125 c.abstract[name] = dirs |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
126 if rb.mixin != "" { |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
127 c.abstractMixin[name] = rb.mixin |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
128 } |
| 0 | 129 } else { |
| 2 | 130 c.Blocks = append(c.Blocks, ParsedBlock{ |
| 131 ID: rb.id, | |
| 132 Mixin: rb.mixin, | |
| 133 Directives: dirs, | |
| 134 }) | |
| 0 | 135 } |
| 136 } | |
| 137 | |
| 138 return c, nil | |
| 139 } | |
| 140 | |
| 2 | 141 // Abstract returns the directives of a named abstract block. |
| 142 // Returns nil if not found. | |
| 0 | 143 func (c *Config) Abstract(name string) []Directive { |
| 2 | 144 return c.abstract[name] |
| 0 | 145 } |
| 146 | |
| 147 // Match finds the most specific block matching input (e.g. "host/path") and | |
| 148 // returns resolved directives plus named captures. | |
| 2 | 149 // Domain is matched exactly; path uses prefix match. |
| 12 | 150 func (c *Config) Match(input string) ([]Directive, map[string]string, string) { |
| 0 | 151 inHost, inPath, _ := strings.Cut(input, "/") |
| 152 | |
| 153 type hit struct { | |
| 154 block ParsedBlock | |
| 155 captures map[string]string | |
| 156 score int | |
| 12 | 157 rem string |
| 0 | 158 } |
| 159 var best *hit | |
| 160 | |
| 161 for _, b := range c.Blocks { | |
| 162 patHost, patPath, hasPath := strings.Cut(b.ID, "/") | |
| 163 caps := make(map[string]string) | |
| 164 | |
| 165 domScore, ok := matchExact(patHost, inHost, caps) | |
| 166 if !ok { | |
| 167 continue | |
| 168 } | |
| 169 | |
| 170 pathScore := 0 | |
| 12 | 171 rem := inPath |
| 0 | 172 if hasPath { |
| 12 | 173 var pathRem string |
| 174 pathScore, pathRem, ok = matchPrefix(patPath, inPath, caps) | |
| 0 | 175 if !ok { |
| 176 continue | |
| 177 } | |
| 12 | 178 rem = pathRem |
| 0 | 179 } |
| 180 | |
| 181 score := domScore*1000 + pathScore | |
| 182 if best == nil || score > best.score { | |
| 12 | 183 best = &hit{block: b, captures: caps, score: score, rem: rem} |
| 0 | 184 } |
| 185 } | |
| 186 | |
| 187 if best == nil { | |
| 12 | 188 return nil, nil, "" |
| 0 | 189 } |
| 190 | |
| 13 | 191 // subPath is the filesystem path relative to rootDir. |
| 192 // rem is the unmatched suffix after the path pattern | |
|
14
a51dfcb0faeb
forgot to remove bestPatPath variable that's not used anymore
Atarwn Gard <a@qwa.su>
parents:
13
diff
changeset
|
193 _, _, bestHasPath := strings.Cut(best.block.ID, "/") |
| 12 | 194 subPath := best.rem |
| 13 | 195 if bestHasPath && subPath == "" { |
| 196 base := path.Base(inPath) | |
| 197 if strings.Contains(base, ".") { | |
| 198 subPath = "/" + inPath | |
| 199 } else { | |
| 200 subPath = "/" | |
| 201 } | |
| 202 } else if !strings.HasPrefix(subPath, "/") { | |
| 12 | 203 subPath = "/" + subPath |
| 204 } | |
| 205 return c.ResolveBlock(best.block, best.captures), best.captures, subPath | |
| 0 | 206 } |
| 207 | |
| 2 | 208 // ResolveBlock merges mixin directives (lower priority) with block directives, |
| 209 // then substitutes capture variables. | |
| 0 | 210 func (c *Config) ResolveBlock(b ParsedBlock, caps map[string]string) []Directive { |
| 211 var merged []Directive | |
| 212 if b.Mixin != "" { | |
|
6
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
213 merged = append(merged, c.resolveAbstract(b.Mixin, make(map[string]bool))...) |
| 0 | 214 } |
| 215 merged = append(merged, b.Directives...) | |
| 2 | 216 |
| 217 if len(caps) == 0 { | |
| 218 return merged | |
| 219 } | |
| 0 | 220 |
| 2 | 221 // Substitute capture variables into a copy |
| 222 subst := makeSubst(c.vars) | |
| 223 out := make([]Directive, len(merged)) | |
| 224 for i, d := range merged { | |
| 225 out[i].Key = d.Key | |
| 0 | 226 out[i].Args = make([]string, len(d.Args)) |
| 227 for j, a := range d.Args { | |
| 2 | 228 out[i].Args[j] = subst(a, caps) |
| 0 | 229 } |
| 230 } | |
| 231 return out | |
| 232 } | |
| 233 | |
|
6
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
234 // resolveAbstract recursively expands an abstract block and its mixin chain. |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
235 // visited guards against circular references. |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
236 func (c *Config) resolveAbstract(name string, visited map[string]bool) []Directive { |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
237 if visited[name] { |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
238 return nil |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
239 } |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
240 visited[name] = true |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
241 |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
242 var out []Directive |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
243 if parent, ok := c.abstractMixin[name]; ok { |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
244 out = append(out, c.resolveAbstract(parent, visited)...) |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
245 } |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
246 out = append(out, c.abstract[name]...) |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
247 return out |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
248 } |
|
54ab94198677
abstract mixins + building sctipt + documentation
Atarwn Gard <a@qwa.su>
parents:
4
diff
changeset
|
249 |
| 2 | 250 // makeSubst returns a function that substitutes $VAR in s, |
| 251 // checking caps first, then vars. | |
| 252 func makeSubst(vars map[string]string) func(s string, caps map[string]string) string { | |
| 253 return func(s string, caps map[string]string) string { | |
| 254 if !strings.Contains(s, "$") { | |
| 255 return s | |
| 0 | 256 } |
| 2 | 257 var b strings.Builder |
| 258 i := 0 | |
| 259 for i < len(s) { | |
| 260 if s[i] != '$' { | |
| 261 b.WriteByte(s[i]) | |
| 262 i++ | |
| 263 continue | |
| 264 } | |
| 265 j := i + 1 | |
| 266 for j < len(s) && isVarChar(s[j]) { | |
| 267 j++ | |
| 268 } | |
| 269 name := s[i+1 : j] | |
| 270 if caps != nil { | |
| 271 if v, ok := caps[name]; ok { | |
| 272 b.WriteString(v) | |
| 273 i = j | |
| 274 continue | |
| 275 } | |
| 276 } | |
| 277 if v, ok := vars[name]; ok { | |
| 278 b.WriteString(v) | |
| 279 } else { | |
| 280 b.WriteString(s[i:j]) | |
| 281 } | |
| 282 i = j | |
| 0 | 283 } |
| 2 | 284 return b.String() |
| 285 } | |
| 286 } | |
| 287 | |
| 288 func readLines(r io.Reader) ([]string, error) { | |
| 289 var out []string | |
| 290 scanner := bufio.NewScanner(r) | |
| 291 for scanner.Scan() { | |
| 292 line := stripComment(strings.TrimSpace(scanner.Text())) | |
| 293 if line != "" { | |
| 294 out = append(out, line) | |
| 0 | 295 } |
| 296 } | |
| 2 | 297 return out, scanner.Err() |
| 0 | 298 } |
| 299 | |
| 300 func matchExact(pat, s string, caps map[string]string) (int, bool) { | |
| 301 score, rem, ok := matchCaptures(pat, s, caps) | |
| 302 if !ok || rem != "" { | |
| 303 return 0, false | |
| 304 } | |
| 305 return score, true | |
| 306 } | |
| 307 | |
| 12 | 308 func matchPrefix(pat, s string, caps map[string]string) (int, string, bool) { |
| 309 score, rem, ok := matchCaptures(pat, s, caps) | |
| 310 return score, rem, ok | |
| 0 | 311 } |
| 312 | |
| 313 func matchCaptures(pat, inp string, caps map[string]string) (int, string, bool) { | |
| 314 for { | |
| 315 if pat == "" { | |
| 316 return 0, inp, true | |
| 317 } | |
| 318 | |
| 319 if strings.HasPrefix(pat, "<") { | |
| 320 end := strings.Index(pat, ">") | |
| 321 if end == -1 { | |
| 322 return 0, "", false | |
| 323 } | |
| 324 capName := pat[1:end] | |
| 325 rest := pat[end+1:] | |
| 326 | |
|
8
2ffb8028ccbb
add loggingmodes and fix path matching
Atarwn Gard <a@qwa.su>
parents:
6
diff
changeset
|
327 for split := len(inp); split >= 1; split-- { |
| 12 | 328 // Captures never span path separators: <n> matches one segment only. |
| 329 if strings.Contains(inp[:split], "/") { | |
| 330 continue | |
| 331 } | |
| 2 | 332 score, finalRem, ok := matchCaptures(rest, inp[split:], caps) |
| 0 | 333 if ok { |
| 334 if capName != "_" { | |
| 2 | 335 caps[capName] = inp[:split] |
| 0 | 336 } |
| 337 return score, finalRem, true | |
| 338 } | |
| 339 } | |
| 340 return 0, "", false | |
| 341 } | |
| 342 | |
| 343 if inp == "" || pat[0] != inp[0] { | |
| 344 return 0, "", false | |
| 345 } | |
| 346 score, rem, ok := matchCaptures(pat[1:], inp[1:], caps) | |
| 347 return score + 1, rem, ok | |
| 348 } | |
| 349 } | |
| 350 | |
| 351 func braceExpand(args []string) []string { | |
| 352 var out []string | |
| 353 for _, a := range args { | |
| 354 out = append(out, expandOne(a)...) | |
| 355 } | |
| 356 return out | |
| 357 } | |
| 358 | |
| 359 func expandOne(s string) []string { | |
| 360 start := strings.Index(s, "{") | |
| 361 end := strings.Index(s, "}") | |
| 362 if start == -1 || end == -1 || end < start { | |
| 363 return []string{s} | |
| 364 } | |
| 365 prefix := s[:start] | |
| 366 suffix := s[end+1:] | |
| 367 var out []string | |
| 368 for _, v := range strings.Split(s[start+1:end], ",") { | |
| 369 out = append(out, prefix+strings.TrimSpace(v)+suffix) | |
| 370 } | |
| 371 return out | |
| 372 } | |
| 373 | |
| 374 func stripComment(line string) string { | |
| 375 if strings.HasPrefix(line, ";") { | |
| 376 return "" | |
| 377 } | |
| 378 if i := strings.Index(line, " ;"); i != -1 { | |
| 379 return strings.TrimSpace(line[:i]) | |
| 380 } | |
| 381 return line | |
| 382 } | |
| 383 | |
| 384 func isVarName(s string) bool { | |
| 385 if s == "" { | |
| 386 return false | |
| 387 } | |
| 388 for i := 0; i < len(s); i++ { | |
| 389 if !isVarChar(s[i]) { | |
| 390 return false | |
| 391 } | |
| 392 } | |
| 393 return true | |
| 394 } | |
| 395 | |
| 396 func isVarChar(c byte) bool { | |
| 397 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || | |
| 398 (c >= '0' && c <= '9') || c == '_' | |
| 2 | 399 } |
