changeset 6:54ab94198677

abstract mixins + building sctipt + documentation
author Atarwn Gard <a@qwa.su>
date Tue, 10 Mar 2026 10:22:02 +0500
parents 07b6f06899e0
children 8e4813b4e509
files icf/icf.go justfile man/d2o.1.md man/d2obase.5.md
diffstat 4 files changed, 225 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/icf/icf.go	Mon Mar 09 03:35:47 2026 +0500
+++ b/icf/icf.go	Tue Mar 10 10:22:02 2026 +0500
@@ -17,9 +17,10 @@
 }
 
 type Config struct {
-	vars     map[string]string
-	abstract map[string][]Directive
-	Blocks   []ParsedBlock
+	vars         map[string]string
+	abstract     map[string][]Directive
+	abstractMixin map[string]string // mixin name for each abstract block
+	Blocks       []ParsedBlock
 }
 
 type ParsedBlock struct {
@@ -103,8 +104,9 @@
 
 	// --- Pass 3: separate abstract from concrete, apply var substitution ---
 	c := &Config{
-		vars:     vars,
-		abstract: make(map[string][]Directive),
+		vars:          vars,
+		abstract:      make(map[string][]Directive),
+		abstractMixin: make(map[string]string),
 	}
 
 	for _, rb := range raws {
@@ -118,7 +120,11 @@
 		}
 
 		if strings.HasPrefix(rb.id, "@") {
-			c.abstract[rb.id[1:]] = dirs
+			name := rb.id[1:]
+			c.abstract[name] = dirs
+			if rb.mixin != "" {
+				c.abstractMixin[name] = rb.mixin
+			}
 		} else {
 			c.Blocks = append(c.Blocks, ParsedBlock{
 				ID:         rb.id,
@@ -185,7 +191,7 @@
 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, c.resolveAbstract(b.Mixin, make(map[string]bool))...)
 	}
 	merged = append(merged, b.Directives...)
 
@@ -206,6 +212,22 @@
 	return out
 }
 
+// resolveAbstract recursively expands an abstract block and its mixin chain.
+// visited guards against circular references.
+func (c *Config) resolveAbstract(name string, visited map[string]bool) []Directive {
+	if visited[name] {
+		return nil
+	}
+	visited[name] = true
+
+	var out []Directive
+	if parent, ok := c.abstractMixin[name]; ok {
+		out = append(out, c.resolveAbstract(parent, visited)...)
+	}
+	out = append(out, c.abstract[name]...)
+	return out
+}
+
 // 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 {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/justfile	Tue Mar 10 10:22:02 2026 +0500
@@ -0,0 +1,7 @@
+build: d2o man
+
+d2o:
+    go build .
+man:
+    lowdown -s -tman man/d2o.1.md -o d2o.1
+    lowdown -s -tman man/d2obase.5.md -o d2obase.5
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/man/d2o.1.md	Tue Mar 10 10:22:02 2026 +0500
@@ -0,0 +1,24 @@
+% d2o(1)
+% 2026-03-10
+
+# NAME
+d2o (heavy h2o) - an experimental web server inspired by h2o project
+
+# SYNOPSIS
+**d2o** \[*config*\]
+
+# DESCRIPTION
+d2o is a webserver, **the main feature** of which is its **unique ICF configuration format**, which uses inheritance and string substitution as the primary means of managing websites.
+
+Instead of spawning endless blocks and drowning in `location/`, It just uses pipelines. The entire server, with all subdomains, TLS, and proxying, fits into 12 lines that read from top to bottom. If a folder exists in $WWW, it automatically becomes a site through named wildcards. It’s not magic, it’s just the absence of unnecessary noise in a system that doesn’t try to appear more complex than it is.
+
+# OPTIONS
+*config*
+: The path to the configuration file in ICF format. If not specified, `/etc/d2obase' is used.
+
+# FILES
+*/etc/d2obase*
+: The default configuration file.
+
+# SEE ALSO
+d2obase(5)
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/man/d2obase.5.md	Tue Mar 10 10:22:02 2026 +0500
@@ -0,0 +1,165 @@
+% D2OBASE(5)
+% March 2026
+
+# NAME
+d2obase - syntax and configuration guide for the d2o web server
+
+# DESCRIPTION
+The **ICF** (Inheritance Config Format) uses a stream-oriented approach to web serving. It focuses on reducing "boilerplate" by using variables, reusable mixins, and execution pipelines.
+
+# SYNTAX
+A configuration file consists of variable assignments, mixin definitions, and site blocks containing pipelines.
+
+## Variables
+Variables are defined as `KEY=VALUE`. They are global and can be referenced later using the `$` prefix (e.g., `$DOM`).
+
+Variable names may only contain characters `[A-Za-z0-9_]`. Values are substituted at the point of declaration, so a variable can reference only those variables defined above it in the file.
+
+## Mixins (Templates)
+A mixin is defined with the `@` prefix. It stores a set of pipeline directives that can be "splatted" into a site block to avoid repetition.
+
+    @name
+    |> directive argument
+
+Mixins can inherit from other mixins:
+
+    @child @parent
+    |> directive argument
+
+The parent's directives are inserted before the child's, so the child can override them. Circular references are silently ignored.
+
+## Pipelines and Directives
+The core of the configuration is the pipeline. Each line starting with `|>` is a directive executed in top-down order.
+
+Directives follow the format: `|> directive [arguments...]`.
+
+### Argument Expansion
+Directives support curly brace expansion `{}`. For example:
+`file.{crt,key}` expands to two separate arguments: `file.crt` and `file.key`.
+
+Only one pair of braces per argument is expanded. Nested braces are not supported.
+
+## Site Blocks
+A site block starts with a domain name (or a variable containing one), followed by optional mixins, and a pipeline.
+
+    example.org @setup
+    |> directive argument
+
+### Pattern Matching
+The domain part of a site block is matched exactly. The path part (if present, separated by `/`) is matched by prefix.
+
+Named capture groups can be used in patterns with `<name>` syntax. The captured value is available as `$name` in the block's directives.
+
+    <sub>.example.org
+    |> root /srv/www/$sub
+
+#### Block Specificity
+
+When multiple blocks match a request, the most specific one wins. Specificity is the number of literal characters matched in the pattern - capture groups `<n>` do not contribute to the score. Host and path are scored separately; host score is weighted heavier (`score = host_score × 1000 + path_score`).
+
+For example, given these two blocks:
+
+    app.example.org
+    |> rprx http://127.0.0.1:8080
+
+    <sub>.example.org
+    |> root /srv/www/$sub
+
+A request for `app.example.org` matches both, but `app.example.org` wins - it matched 15 literal characters against 11 for `<sub>.example.org` (`.example.org` only). Order of declaration in the file does not matter.
+
+## Comments
+Lines beginning with `;` are ignored. Inline comments start with ` ;` (space before semicolon).
+
+    ; full line comment
+    KEY=value  ; inline comment
+
+# DIRECTIVES
+
+## Listening
+
+**port** *number*
+: Listen for plain HTTP on the given port.
+
+**port+tls** *number* [*cert* *key*]
+: Listen for HTTPS on the given port. Certificate and key paths are optional if a **tls** directive is present in the same block.
+
+**tls** *cert* *key*
+: Set the default certificate and key for **port+tls** directives in this block that do not specify their own paths.
+
+## Serving
+
+**root** *path* [**show**]
+: Serve static files from *path*. Without **show**, directory listing is forbidden (403). With **show**, an HTML directory listing is returned.
+
+**ndex** *file...*
+: List of index filenames to try when a directory is requested, checked left to right. If a matching file passes the **fcgi** pattern, it is handled by FastCGI instead of served directly.
+
+**fcgi** *address* [*pattern*]
+: Forward matching requests to a FastCGI server. *address* is either `unix:///path/to/socket` or `host:port`. *pattern* is a glob matched against the request path (default `*`). When used together with **root**, only requests matching the pattern are forwarded; everything else is served as static.
+
+**rprx** *address*
+: Reverse-proxy all requests to *address*. The `http://` scheme is assumed if not specified.
+
+**rdir** *code* *url*
+: Redirect to *url* with the given HTTP status code. If *code* is omitted or zero, 302 is used.
+
+## Global (in `@d2o` block only)
+
+**threads** *n*
+: Set `GOMAXPROCS` to *n*.
+
+# EXAMPLES
+Below is a complex setup using global variables, a shared mixin for PHP sites, and subdomain handling:
+
+    DOM=qwaderton.org
+    ACME=/etc/acme
+    WWW=/srv/www/$DOM
+
+    @ports
+    |> port 80
+    |> port+tls 443 $ACME/$DOM.{crt,key}
+    |> ndex index.php index.html
+
+    @ports+fcgi @ports
+    |> fcgi unix:///run/php-fpm.sock *.php
+
+    $DOM @ports+fcgi
+    |> root $WWW show
+
+    <sub>.$DOM @ports+fcgi
+    |> root $WWW/$sub
+
+    app.$DOM @ports
+    |> rprx http://127.0.0.1:8080
+
+    qwa.su
+    |> port 80
+    |> port+tls 443 $ACME/qwa.su.{crt,key}
+    |> rdir 307 https://qwaderton.org/
+
+We use this configuration on our web server (as it is at the time of writing this manual).
+
+If you need something simplier to start quickly, use this config:
+
+    DOM=mydomain
+    TLS=/etc/acme/$DOM
+    WWW=/srv/www/$DOM
+
+    $DOM
+    |> port 80
+    |> port+tls 443 $TLS.{crt,key}
+    |> root $WWW show
+
+# CAVEATS
+1. ICF does not support strings with spaces - there are **no quotes or escapes**. Every character except space is treated as part of a token, including `!`, `*`, `/`, and so on. Glob patterns passed to **fcgi** are forwarded as-is to the server.
+
+2. Variables are evaluated top-down at the point of declaration. Forward references do not work: if `B=$A` appears before `A=value`, `B` will contain the literal `$A`.
+
+3. Only one pair of curly braces is expanded per argument. `file.{a,b}.{x,y}` does not produce four arguments - only the first `{}` pair is expanded.
+
+4. A directive outside of any block (a `|>` line before the first block header) is a parse error and will prevent the server from starting.
+
+5. Duplicate ports across blocks are silently deduplicated - the first declaration wins. TLS certificate paths from a later block with the same port are ignored.
+
+# SEE ALSO
+**d2o**(1)
\ No newline at end of file