changeset 55:c6785950280e draft

The fastest, period. Signed-off-by: Izuru Yakumo <yakumo.izuru@chaotic.ninja>
author yakumo.izuru
date Sun, 09 Apr 2023 02:39:19 +0000
parents 4d411cdd68c1
children 315dee06253e
files .gitignore .goreleaser.yml LICENSE Makefile README.md aya.1 build_test.go cmd/aya/main.go go.mod main.go main_test.go testdata/blog/.test/about.html testdata/blog/.test/index.html testdata/blog/.test/posts/hello.html testdata/blog/.test/posts/update.html testdata/blog/.test/styles.css testdata/blog/.zs/layout.amber testdata/blog/about.md testdata/blog/index.amber testdata/blog/posts/hello.md testdata/blog/posts/update.md testdata/blog/styles.gcss testdata/empty/.empty testdata/page/.test/index.html testdata/page/index.html testdata/sugar/.test/index.html testdata/sugar/.test/styles.css testdata/sugar/index.amber testdata/sugar/styles.gcss tools/release.sh version.go zs.1
diffstat 31 files changed, 432 insertions(+), 789 deletions(-) [+]
line wrap: on
line diff
--- a/.gitignore	Fri Jan 07 23:20:51 2022 +0000
+++ b/.gitignore	Sun Apr 09 02:39:19 2023 +0000
@@ -2,6 +2,6 @@
 *.bak
 **.pub
 
-/zs
+/aya
 /dist
 /test.md
--- a/.goreleaser.yml	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
----
-builds:
-  -
-    id: zs
-    binary: zs
-    main: .
-    flags: -tags "static_build"
-    ldflags: -w -X main.Version={{.Version}} -X main.Commit={{.Commit}}
-    env:
-      - CGO_ENABLED=0
-    goos:
-      - darwin
-      - linux
-    goarch:
-      - amd64
-      - arm64
-signs:
-  - artifacts: checksum
-release:
-  gitea:
-    owner: prologic
-    name: zs
-  draft: true
-gitea_urls:
-  api: https://git.mills.io/api/v1/
--- a/LICENSE	Fri Jan 07 23:20:51 2022 +0000
+++ b/LICENSE	Sun Apr 09 02:39:19 2023 +0000
@@ -1,6 +1,7 @@
 The MIT License (MIT)
 
 Copyright (c) 2014 zserge
+Copyright (c) 2023 Izuru Yakumo
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
--- a/Makefile	Fri Jan 07 23:20:51 2022 +0000
+++ b/Makefile	Sun Apr 09 02:39:19 2023 +0000
@@ -1,13 +1,17 @@
 destdir ?= 
+goflags ?= -v -ldflags "-w -X `go list`.Version=$(version) -X `go list`.Commit=$(commit)" -tags "static_build"
 prefix ?= /usr/local
+version ?= `git rev-list --count HEAD || echo "$version"`
+commit ?= `git rev-parse --short HEAD || echo "$commit"`
+
 
 build:
-	go build -v
+	go build ${goflags} ./cmd/aya
 clean:
-	rm -f zs
+	rm -f aya
 install:
-	install -m0755 zs ${destdir}${prefix}/bin/zs
-	install -m0644 zs.1 ${destdir}${prefix}/share/man/man1/zs.1
+	install -Dm0755 aya ${destdir}${prefix}/bin/aya
+	install -Dm0644 aya.1 ${destdir}${prefix}/share/man/man1/aya.1
 uninstall:
-	rm -f ${prefix}/bin/zs
-	rm -f ${prefix}/share/man/man1/zs.1
+	rm -f ${prefix}/bin/aya
+	rm -f ${prefix}/share/man/man1/aya.1
--- a/README.md	Fri Jan 07 23:20:51 2022 +0000
+++ b/README.md	Sun Apr 09 02:39:19 2023 +0000
@@ -1,10 +1,8 @@
-# zs
-
-zs is an extremely minimal static site generator written in Go.
+# aya
 
-It's inspired by `zas` generator, but is even more minimal.
+aya is an extremely minimal static site generator written in Go.
 
-The name stands for 'zen static' as well as it's my initials.
+This crow tengu stands for 'the fastest one in Gensokyo' and yes this is also a Touhou Project reference.
 
 ## Features
 
@@ -17,9 +15,9 @@
 
 ## Installation
 
-Download the binaries from Github or build it manually:
+Build it manually assuming you have Go installed:
 
-	$ go get git.mills.io/prologic/zs
+	$ go install marisa.chaotic.ninja/aya@latest
 
 ## Ideology
 
@@ -27,7 +25,7 @@
 of your blog/site.
 
 Keep all service files (extensions, layout pages, deployment scripts etc)
-in the `.zs` subdirectory.
+in the `.aya` subdirectory.
 
 Define variables in the header of the content files using [YAML]:
 
@@ -40,18 +38,18 @@
 Use placeholders for variables and plugins in your markdown or html
 files, e.g. `{{ title }}` or `{{ command arg1 arg2 }}.
 
-Write extensions in any language you like and put them into the `.zs`
+Write extensions in any language you like and put them into the `.aya`
 subdiretory.
 
 Everything the extensions prints to stdout becomes the value of the
 placeholder.
 
-Every variable from the content header will be passed via environment variables like `title` becomes `$ZS_TITLE` and so on. There are some special variables:
+Every variable from the content header will be passed via environment variables like `title` becomes `$AYA_TITLE` and so on. There are some special variables:
 
-* `$ZS` - a path to the `zs` executable
-* `$ZS_OUTDIR` - a path to the directory with generated files
-* `$ZS_FILE` - a path to the currently processed markdown file
-* `$ZS_URL` - a URL for the currently generated page
+* `$AYA` - a path to the `aya` executable
+* `$AYA_OUTDIR` - a path to the directory with generated files
+* `$AYA_FILE` - a path to the currently processed markdown file
+* `$AYA_URL` - a URL for the currently generated page
 
 ## Example of RSS generation
 
@@ -59,19 +57,19 @@
 
 ``` bash
 for f in ./blog/*.md ; do
-	d=$($ZS var $f date)
+	d=$($AYA var $f date)
 	if [ ! -z $d ] ; then
 		timestamp=`date --date "$d" +%s`
-		url=`$ZS var $f url`
-		title=`$ZS var $f title | tr A-Z a-z`
-		descr=`$ZS var $f description`
+		url=`$AYA var $f url`
+		title=`$AYA var $f title | tr A-Z a-z`
+		descr=`$AYA var $f description`
 		echo $timestamp \
 			"<item>" \
 			"<title>$title</title>" \
-			"<link>http://zserge.com/$url</link>" \
+			"<link>http://ayaerge.com/$url</link>" \
 			"<description>$descr</description>" \
 			"<pubDate>$(date --date @$timestamp -R)</pubDate>" \
-			"<guid>http://zserge.com/$url</guid>" \
+			"<guid>http://ayaerge.com/$url</guid>" \
 		"</item>"
 	fi
 done | sort -r -n | cut -d' ' -f2-
@@ -83,21 +81,21 @@
 happens - `prehook` and `posthook`. You can define some global actions here like
 content generation, or additional commands, like LESS to CSS conversion:
 
-	# .zs/post
+	# .aya/post
 
 	#!/bin/sh
-	lessc < $ZS_OUTDIR/styles.less > $ZS_OUTDIR/styles.css
-	rm -f $ZS_OUTDIR/styles.css
+	lessc < $AYA_OUTDIR/styles.less > $AYA_OUTDIR/styles.css
+	rm -f $AYA_OUTDIR/styles.css
 
 ## Command line usage
 
-`zs build` re-builds your site.
+`aya build` re-builds your site.
 
-`zs build <file>` re-builds one file and prints resulting content to stdout.
+`aya build <file>` re-builds one file and prints resulting content to stdout.
 
-`zs watch` rebuilds your site every time you modify any file.
+`aya watch` rebuilds your site every time you modify any file.
 
-`zs var <filename> [var1 var2...]` prints a list of variables defined in the
+`aya var <filename> [var1 var2...]` prints a list of variables defined in the
 header of a given markdown file, or the values of certain variables (even if
 it's an empty string).
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/aya.1	Sun Apr 09 02:39:19 2023 +0000
@@ -0,0 +1,37 @@
+.Dd $Mdocdate$
+.Dt AYA 1
+.Os
+.Sh NAME
+.Nm aya
+.Nd A really fast static site generator
+.Sh DESCRIPTION
+Does it need one?
+.Sh FEATURES
+.Bl -tag -width 11n -compact
+.It Zero configuration (no configuration file needed)
+.It Cross-platform
+.It Highly extensible
+.It Works well for blogs and generic static websites (landing pages etc)
+.It Easy to learn
+.It Fast (of course)
+.El
+.Sh USAGE
+.Ss (Re-)build your site.
+.Nm 
+.Cm build
+.Ss (Re-)build one file and prints resulting content to standard output.
+.Nm
+.Cm build
+.Ar <file>
+.Ss (Re-)build your site every time you modify any file.
+.Nm
+.Cm watch
+.Ss Print a list of variables defined in the header of a given markdown file.
+.Nm 
+.Cm var
+.Ar <file>
+.Ar <var1> <var2> ...
+.Sh AUTHORS
+.Nm
+is maintained by Izuru Yakumo 
+.Aq Lk https://pub.chaotic.ninja/~yakumo_izuru/
--- a/build_test.go	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-package main
-
-import (
-	"crypto/md5"
-	"encoding/hex"
-	"io"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"strings"
-	"testing"
-)
-
-const TESTDIR = ".test"
-
-func TestBuild(t *testing.T) {
-	files, _ := ioutil.ReadDir("testdata")
-	for _, f := range files {
-		if f.IsDir() {
-			testBuild(filepath.Join("testdata", f.Name()), t)
-		}
-	}
-}
-
-func testBuild(path string, t *testing.T) {
-	wd, _ := os.Getwd()
-	os.Chdir(path)
-	args := os.Args[:]
-	os.Args = []string{"zs", "build"}
-	t.Log("--- BUILD", path)
-	main()
-
-	compare(PUBDIR, TESTDIR, t)
-
-	os.Chdir(wd)
-	os.Args = args
-}
-
-func compare(pub, test string, t *testing.T) {
-	a := md5dir(pub)
-	b := md5dir(test)
-	for k, v := range a {
-		if s, ok := b[k]; !ok {
-			t.Error("Unexpected file:", k, v)
-		} else if s != v {
-			t.Error("Different file:", k, v, s)
-		} else {
-			t.Log("Matching file", k, v)
-		}
-	}
-	for k, v := range b {
-		if _, ok := a[k]; !ok {
-			t.Error("Missing file:", k, v)
-		}
-	}
-}
-
-func md5dir(path string) map[string]string {
-	files := map[string]string{}
-	filepath.Walk(path, func(s string, info os.FileInfo, err error) error {
-		if err == nil && !info.IsDir() {
-			if f, err := os.Open(s); err == nil {
-				defer f.Close()
-				hash := md5.New()
-				io.Copy(hash, f)
-				files[strings.TrimPrefix(s, path)] = hex.EncodeToString(hash.Sum(nil))
-			}
-		}
-		return nil
-	})
-	return files
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/aya/main.go	Sun Apr 09 02:39:19 2023 +0000
@@ -0,0 +1,356 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"text/template"
+	"time"
+
+	"github.com/russross/blackfriday/v2"
+	"gopkg.in/yaml.v2"
+	"marisa.chaotic.ninja/aya"
+)
+
+const (
+	AYADIR  = ".aya"
+	PUBDIR = ".pub"
+)
+
+type Vars map[string]string
+
+// renameExt renames extension (if any) from oldext to newext
+// If oldext is an empty string - extension is extracted automatically.
+// If path has no extension - new extension is appended
+func renameExt(path, oldext, newext string) string {
+	if oldext == "" {
+		oldext = filepath.Ext(path)
+	}
+	if oldext == "" || strings.HasSuffix(path, oldext) {
+		return strings.TrimSuffix(path, oldext) + newext
+	}
+	return path
+}
+
+// globals returns list of global OS environment variables that start
+// with AYA_ prefix as Vars, so the values can be used inside templates
+func globals() Vars {
+	vars := Vars{}
+	for _, e := range os.Environ() {
+		pair := strings.Split(e, "=")
+		if strings.HasPrefix(pair[0], "AYA_") {
+			vars[strings.ToLower(pair[0][3:])] = pair[1]
+		}
+	}
+	return vars
+}
+
+// run executes a command or a script. Vars define the command environment,
+// each aya var is converted into OS environemnt variable with AYA_ prefix
+// prepended.  Additional variable $AYA contains path to the aya binary. Command
+// stderr is printed to aya stderr, command output is returned as a string.
+func run(vars Vars, cmd string, args ...string) (string, error) {
+	// First check if partial exists (.html)
+	if b, err := ioutil.ReadFile(filepath.Join(AYADIR, cmd+".html")); err == nil {
+		return string(b), nil
+	}
+
+	var errbuf, outbuf bytes.Buffer
+	c := exec.Command(cmd, args...)
+	env := []string{"AYA=" + os.Args[0], "AYA_OUTDIR=" + PUBDIR}
+	env = append(env, os.Environ()...)
+	for k, v := range vars {
+		env = append(env, "AYA_"+strings.ToUpper(k)+"="+v)
+	}
+	c.Env = env
+	c.Stdout = &outbuf
+	c.Stderr = &errbuf
+
+	err := c.Run()
+
+	if errbuf.Len() > 0 {
+		log.Println("ERROR:", errbuf.String())
+	}
+	if err != nil {
+		return "", err
+	}
+	return string(outbuf.Bytes()), nil
+}
+
+// getVars returns list of variables defined in a text file and actual file
+// content following the variables declaration. Header is separated from
+// content by an empty line. Header can be either YAML or JSON.
+// If no empty newline is found - file is treated as content-only.
+func getVars(path string, globals Vars) (Vars, string, error) {
+	b, err := ioutil.ReadFile(path)
+	if err != nil {
+		return nil, "", err
+	}
+	s := string(b)
+
+	// Pick some default values for content-dependent variables
+	v := Vars{}
+	title := strings.Replace(strings.Replace(path, "_", " ", -1), "-", " ", -1)
+	v["title"] = strings.ToTitle(title)
+	v["description"] = ""
+	v["file"] = path
+	v["url"] = path[:len(path)-len(filepath.Ext(path))] + ".html"
+	v["output"] = filepath.Join(PUBDIR, v["url"])
+
+	// Override default values with globals
+	for name, value := range globals {
+		v[name] = value
+	}
+
+	// Add layout if none is specified
+	if _, ok := v["layout"]; !ok {
+		v["layout"] = "layout.html"
+	}
+
+	delim := "\n---\n"
+	if sep := strings.Index(s, delim); sep == -1 {
+		return v, s, nil
+	} else {
+		header := s[:sep]
+		body := s[sep+len(delim):]
+
+		vars := Vars{}
+		if err := yaml.Unmarshal([]byte(header), &vars); err != nil {
+			fmt.Println("ERROR: failed to parse header", err)
+			return nil, "", err
+		} else {
+			// Override default values + globals with the ones defines in the file
+			for key, value := range vars {
+				v[key] = value
+			}
+		}
+		if strings.HasPrefix(v["url"], "./") {
+			v["url"] = v["url"][2:]
+		}
+		return v, body, nil
+	}
+}
+
+// Render expanding aya plugins and variables
+func render(s string, vars Vars) (string, error) {
+	delim_open := "{{"
+	delim_close := "}}"
+
+	out := &bytes.Buffer{}
+	for {
+		if from := strings.Index(s, delim_open); from == -1 {
+			out.WriteString(s)
+			return out.String(), nil
+		} else {
+			if to := strings.Index(s, delim_close); to == -1 {
+				return "", fmt.Errorf("Close delim not found")
+			} else {
+				out.WriteString(s[:from])
+				cmd := s[from+len(delim_open) : to]
+				s = s[to+len(delim_close):]
+				m := strings.Fields(cmd)
+				if len(m) == 1 {
+					if v, ok := vars[m[0]]; ok {
+						out.WriteString(v)
+						continue
+					}
+				}
+				if res, err := run(vars, m[0], m[1:]...); err == nil {
+					out.WriteString(res)
+				} else {
+					fmt.Println(err)
+				}
+			}
+		}
+	}
+	
+}
+
+// Renders markdown with the given layout into html expanding all the macros
+func buildMarkdown(path string, w io.Writer, vars Vars) error {
+	v, body, err := getVars(path, vars)
+	if err != nil {
+		return err
+	}
+	content, err := render(body, v)
+	if err != nil {
+		return err
+	}
+	v["content"] = string(blackfriday.Run(
+		[]byte(content),
+		blackfriday.WithExtensions(blackfriday.CommonExtensions|blackfriday.AutoHeadingIDs),
+	))
+	if w == nil {
+		out, err := os.Create(filepath.Join(PUBDIR, renameExt(path, "", ".html")))
+		if err != nil {
+			return err
+		}
+		defer out.Close()
+		w = out
+	}
+	return buildHTML(filepath.Join(AYADIR, v["layout"]), w, v)
+}
+
+// Renders text file expanding all variable macros inside it
+func buildHTML(path string, w io.Writer, vars Vars) error {
+	v, body, err := getVars(path, vars)
+	if err != nil {
+		return err
+	}
+	if body, err = render(body, v); err != nil {
+		return err
+	}
+	tmpl, err := template.New("").Delims("<%", "%>").Parse(body)
+	if err != nil {
+		return err
+	}
+	if w == nil {
+		f, err := os.Create(filepath.Join(PUBDIR, path))
+		if err != nil {
+			return err
+		}
+		defer f.Close()
+		w = f
+	}
+	return tmpl.Execute(w, vars)
+}
+
+// Copies file as is from path to writer
+func buildRaw(path string, w io.Writer) error {
+	in, err := os.Open(path)
+	if err != nil {
+		return err
+	}
+	defer in.Close()
+	if w == nil {
+		if out, err := os.Create(filepath.Join(PUBDIR, path)); err != nil {
+			return err
+		} else {
+			defer out.Close()
+			w = out
+		}
+	}
+	_, err = io.Copy(w, in)
+	return err
+}
+
+func build(path string, w io.Writer, vars Vars) error {
+	ext := filepath.Ext(path)
+	if ext == ".md" || ext == ".mkd" {
+		return buildMarkdown(path, w, vars)
+	} else if ext == ".html" || ext == ".xml" {
+		return buildHTML(path, w, vars)
+	} else {
+		return buildRaw(path, w)
+	}
+}
+
+func buildAll(watch bool) {
+	lastModified := time.Unix(0, 0)
+	modified := false
+
+	vars := globals()
+	for {
+		os.Mkdir(PUBDIR, 0755)
+		filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
+			// ignore hidden files and directories
+			if filepath.Base(path)[0] == '.' || strings.HasPrefix(path, ".") {
+				return nil
+			}
+			// inform user about fs walk errors, but continue iteration
+			if err != nil {
+				fmt.Println("error:", err)
+				return nil
+			}
+
+			if info.IsDir() {
+				os.Mkdir(filepath.Join(PUBDIR, path), 0755)
+				return nil
+			} else if info.ModTime().After(lastModified) {
+				if !modified {
+					// First file in this build cycle is about to be modified
+					run(vars, "prehook")
+					modified = true
+				}
+				log.Println("build:", path)
+				return build(path, nil, vars)
+			}
+			return nil
+		})
+		if modified {
+			// At least one file in this build cycle has been modified
+			run(vars, "posthook")
+			modified = false
+		}
+		if !watch {
+			break
+		}
+		lastModified = time.Now()
+		time.Sleep(1 * time.Second)
+	}
+}
+
+func init() {
+	// prepend .aya to $PATH, so plugins will be found before OS commands
+	p := os.Getenv("PATH")
+	p = AYADIR + ":" + p
+	os.Setenv("PATH", p)
+}
+
+func main() {
+	if len(os.Args) == 1 {
+		fmt.Println(os.Args[0], "<command> [args]")
+		return
+	}
+	cmd := os.Args[1]
+	args := os.Args[2:]
+	switch cmd {
+	case "build":
+		if len(args) == 0 {
+			buildAll(false)
+		} else if len(args) == 1 {
+			if err := build(args[0], os.Stdout, globals()); err != nil {
+				fmt.Println("ERROR: " + err.Error())
+			}
+		} else {
+			fmt.Println("ERROR: too many arguments")
+		}
+	case "watch":
+		buildAll(true)
+	case "var":
+		if len(args) == 0 {
+			fmt.Println("var: filename expected")
+		} else {
+			s := ""
+			if vars, _, err := getVars(args[0], Vars{}); err != nil {
+				fmt.Println("var: " + err.Error())
+			} else {
+				if len(args) > 1 {
+					for _, a := range args[1:] {
+						s = s + vars[a] + "\n"
+					}
+				} else {
+					for k, v := range vars {
+						s = s + k + ":" + v + "\n"
+					}
+				}
+			}
+			fmt.Println(strings.TrimSpace(s))
+		}
+	case "version":
+		fmt.Printf("%v\n", aya.Version)
+		os.Exit(0)
+	default:
+		if s, err := run(globals(), cmd, args...); err != nil {
+			fmt.Println(err)
+		} else {
+			fmt.Println(s)
+		}
+	}
+}
--- a/go.mod	Fri Jan 07 23:20:51 2022 +0000
+++ b/go.mod	Sun Apr 09 02:39:19 2023 +0000
@@ -1,4 +1,4 @@
-module git.mills.io/prologic/zs
+module marisa.chaotic.ninja/aya
 
 go 1.17
 
--- a/main.go	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,352 +0,0 @@
-package main
-
-import (
-	"bytes"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"log"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"strings"
-	"text/template"
-	"time"
-
-	"github.com/russross/blackfriday/v2"
-	"gopkg.in/yaml.v2"
-)
-
-const (
-	ZSDIR  = ".zs"
-	PUBDIR = ".pub"
-)
-
-type Vars map[string]string
-
-// renameExt renames extension (if any) from oldext to newext
-// If oldext is an empty string - extension is extracted automatically.
-// If path has no extension - new extension is appended
-func renameExt(path, oldext, newext string) string {
-	if oldext == "" {
-		oldext = filepath.Ext(path)
-	}
-	if oldext == "" || strings.HasSuffix(path, oldext) {
-		return strings.TrimSuffix(path, oldext) + newext
-	}
-	return path
-}
-
-// globals returns list of global OS environment variables that start
-// with ZS_ prefix as Vars, so the values can be used inside templates
-func globals() Vars {
-	vars := Vars{}
-	for _, e := range os.Environ() {
-		pair := strings.Split(e, "=")
-		if strings.HasPrefix(pair[0], "ZS_") {
-			vars[strings.ToLower(pair[0][3:])] = pair[1]
-		}
-	}
-	return vars
-}
-
-// run executes a command or a script. Vars define the command environment,
-// each zs var is converted into OS environemnt variable with ZS_ prefix
-// prepended.  Additional variable $ZS contains path to the zs binary. Command
-// stderr is printed to zs stderr, command output is returned as a string.
-func run(vars Vars, cmd string, args ...string) (string, error) {
-	// First check if partial exists (.html)
-	if b, err := ioutil.ReadFile(filepath.Join(ZSDIR, cmd+".html")); err == nil {
-		return string(b), nil
-	}
-
-	var errbuf, outbuf bytes.Buffer
-	c := exec.Command(cmd, args...)
-	env := []string{"ZS=" + os.Args[0], "ZS_OUTDIR=" + PUBDIR}
-	env = append(env, os.Environ()...)
-	for k, v := range vars {
-		env = append(env, "ZS_"+strings.ToUpper(k)+"="+v)
-	}
-	c.Env = env
-	c.Stdout = &outbuf
-	c.Stderr = &errbuf
-
-	err := c.Run()
-
-	if errbuf.Len() > 0 {
-		log.Println("ERROR:", errbuf.String())
-	}
-	if err != nil {
-		return "", err
-	}
-	return string(outbuf.Bytes()), nil
-}
-
-// getVars returns list of variables defined in a text file and actual file
-// content following the variables declaration. Header is separated from
-// content by an empty line. Header can be either YAML or JSON.
-// If no empty newline is found - file is treated as content-only.
-func getVars(path string, globals Vars) (Vars, string, error) {
-	b, err := ioutil.ReadFile(path)
-	if err != nil {
-		return nil, "", err
-	}
-	s := string(b)
-
-	// Pick some default values for content-dependent variables
-	v := Vars{}
-	title := strings.Replace(strings.Replace(path, "_", " ", -1), "-", " ", -1)
-	v["title"] = strings.ToTitle(title)
-	v["description"] = ""
-	v["file"] = path
-	v["url"] = path[:len(path)-len(filepath.Ext(path))] + ".html"
-	v["output"] = filepath.Join(PUBDIR, v["url"])
-
-	// Override default values with globals
-	for name, value := range globals {
-		v[name] = value
-	}
-
-	// Add layout if none is specified
-	if _, ok := v["layout"]; !ok {
-		v["layout"] = "layout.html"
-	}
-
-	delim := "\n---\n"
-	if sep := strings.Index(s, delim); sep == -1 {
-		return v, s, nil
-	} else {
-		header := s[:sep]
-		body := s[sep+len(delim):]
-
-		vars := Vars{}
-		if err := yaml.Unmarshal([]byte(header), &vars); err != nil {
-			fmt.Println("ERROR: failed to parse header", err)
-			return nil, "", err
-		} else {
-			// Override default values + globals with the ones defines in the file
-			for key, value := range vars {
-				v[key] = value
-			}
-		}
-		if strings.HasPrefix(v["url"], "./") {
-			v["url"] = v["url"][2:]
-		}
-		return v, body, nil
-	}
-}
-
-// Render expanding zs plugins and variables
-func render(s string, vars Vars) (string, error) {
-	delim_open := "{{"
-	delim_close := "}}"
-
-	out := &bytes.Buffer{}
-	for {
-		if from := strings.Index(s, delim_open); from == -1 {
-			out.WriteString(s)
-			return out.String(), nil
-		} else {
-			if to := strings.Index(s, delim_close); to == -1 {
-				return "", fmt.Errorf("Close delim not found")
-			} else {
-				out.WriteString(s[:from])
-				cmd := s[from+len(delim_open) : to]
-				s = s[to+len(delim_close):]
-				m := strings.Fields(cmd)
-				if len(m) == 1 {
-					if v, ok := vars[m[0]]; ok {
-						out.WriteString(v)
-						continue
-					}
-				}
-				if res, err := run(vars, m[0], m[1:]...); err == nil {
-					out.WriteString(res)
-				} else {
-					fmt.Println(err)
-				}
-			}
-		}
-	}
-	
-}
-
-// Renders markdown with the given layout into html expanding all the macros
-func buildMarkdown(path string, w io.Writer, vars Vars) error {
-	v, body, err := getVars(path, vars)
-	if err != nil {
-		return err
-	}
-	content, err := render(body, v)
-	if err != nil {
-		return err
-	}
-	v["content"] = string(blackfriday.Run(
-		[]byte(content),
-		blackfriday.WithExtensions(blackfriday.CommonExtensions|blackfriday.AutoHeadingIDs),
-	))
-	if w == nil {
-		out, err := os.Create(filepath.Join(PUBDIR, renameExt(path, "", ".html")))
-		if err != nil {
-			return err
-		}
-		defer out.Close()
-		w = out
-	}
-	return buildHTML(filepath.Join(ZSDIR, v["layout"]), w, v)
-}
-
-// Renders text file expanding all variable macros inside it
-func buildHTML(path string, w io.Writer, vars Vars) error {
-	v, body, err := getVars(path, vars)
-	if err != nil {
-		return err
-	}
-	if body, err = render(body, v); err != nil {
-		return err
-	}
-	tmpl, err := template.New("").Delims("<%", "%>").Parse(body)
-	if err != nil {
-		return err
-	}
-	if w == nil {
-		f, err := os.Create(filepath.Join(PUBDIR, path))
-		if err != nil {
-			return err
-		}
-		defer f.Close()
-		w = f
-	}
-	return tmpl.Execute(w, vars)
-}
-
-// Copies file as is from path to writer
-func buildRaw(path string, w io.Writer) error {
-	in, err := os.Open(path)
-	if err != nil {
-		return err
-	}
-	defer in.Close()
-	if w == nil {
-		if out, err := os.Create(filepath.Join(PUBDIR, path)); err != nil {
-			return err
-		} else {
-			defer out.Close()
-			w = out
-		}
-	}
-	_, err = io.Copy(w, in)
-	return err
-}
-
-func build(path string, w io.Writer, vars Vars) error {
-	ext := filepath.Ext(path)
-	if ext == ".md" || ext == ".mkd" {
-		return buildMarkdown(path, w, vars)
-	} else if ext == ".html" || ext == ".xml" {
-		return buildHTML(path, w, vars)
-	} else {
-		return buildRaw(path, w)
-	}
-}
-
-func buildAll(watch bool) {
-	lastModified := time.Unix(0, 0)
-	modified := false
-
-	vars := globals()
-	for {
-		os.Mkdir(PUBDIR, 0755)
-		filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
-			// ignore hidden files and directories
-			if filepath.Base(path)[0] == '.' || strings.HasPrefix(path, ".") {
-				return nil
-			}
-			// inform user about fs walk errors, but continue iteration
-			if err != nil {
-				fmt.Println("error:", err)
-				return nil
-			}
-
-			if info.IsDir() {
-				os.Mkdir(filepath.Join(PUBDIR, path), 0755)
-				return nil
-			} else if info.ModTime().After(lastModified) {
-				if !modified {
-					// First file in this build cycle is about to be modified
-					run(vars, "prehook")
-					modified = true
-				}
-				log.Println("build:", path)
-				return build(path, nil, vars)
-			}
-			return nil
-		})
-		if modified {
-			// At least one file in this build cycle has been modified
-			run(vars, "posthook")
-			modified = false
-		}
-		if !watch {
-			break
-		}
-		lastModified = time.Now()
-		time.Sleep(1 * time.Second)
-	}
-}
-
-func init() {
-	// prepend .zs to $PATH, so plugins will be found before OS commands
-	p := os.Getenv("PATH")
-	p = ZSDIR + ":" + p
-	os.Setenv("PATH", p)
-}
-
-func main() {
-	if len(os.Args) == 1 {
-		fmt.Println(os.Args[0], "<command> [args]")
-		return
-	}
-	cmd := os.Args[1]
-	args := os.Args[2:]
-	switch cmd {
-	case "build":
-		if len(args) == 0 {
-			buildAll(false)
-		} else if len(args) == 1 {
-			if err := build(args[0], os.Stdout, globals()); err != nil {
-				fmt.Println("ERROR: " + err.Error())
-			}
-		} else {
-			fmt.Println("ERROR: too many arguments")
-		}
-	case "watch":
-		buildAll(true)
-	case "var":
-		if len(args) == 0 {
-			fmt.Println("var: filename expected")
-		} else {
-			s := ""
-			if vars, _, err := getVars(args[0], Vars{}); err != nil {
-				fmt.Println("var: " + err.Error())
-			} else {
-				if len(args) > 1 {
-					for _, a := range args[1:] {
-						s = s + vars[a] + "\n"
-					}
-				} else {
-					for k, v := range vars {
-						s = s + k + ":" + v + "\n"
-					}
-				}
-			}
-			fmt.Println(strings.TrimSpace(s))
-		}
-	default:
-		if s, err := run(globals(), cmd, args...); err != nil {
-			fmt.Println(err)
-		} else {
-			fmt.Println(s)
-		}
-	}
-}
--- a/main_test.go	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,108 +0,0 @@
-package main
-
-import (
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"testing"
-)
-
-func TestRenameExt(t *testing.T) {
-	if s := renameExt("foo.amber", ".amber", ".html"); s != "foo.html" {
-		t.Error(s)
-	}
-	if s := renameExt("foo.amber", "", ".html"); s != "foo.html" {
-		t.Error(s)
-	}
-	if s := renameExt("foo.amber", ".md", ".html"); s != "foo.amber" {
-		t.Error(s)
-	}
-	if s := renameExt("foo", ".amber", ".html"); s != "foo" {
-		t.Error(s)
-	}
-	if s := renameExt("foo", "", ".html"); s != "foo.html" {
-		t.Error(s)
-	}
-}
-
-func TestRun(t *testing.T) {
-	// external command
-	if s, err := run(Vars{}, "echo", "hello"); err != nil || s != "hello\n" {
-		t.Error(s, err)
-	}
-	// passing variables to plugins
-	if s, err := run(Vars{"foo": "bar"}, "sh", "-c", "echo $ZS_FOO"); err != nil || s != "bar\n" {
-		t.Error(s, err)
-	}
-
-	// custom plugin overriding external command
-	os.Mkdir(ZSDIR, 0755)
-	script := `#!/bin/sh
-echo foo
-`
-	ioutil.WriteFile(filepath.Join(ZSDIR, "echo"), []byte(script), 0755)
-	if s, err := run(Vars{}, "echo", "hello"); err != nil || s != "foo\n" {
-		t.Error(s, err)
-	}
-	os.Remove(filepath.Join(ZSDIR, "echo"))
-	os.Remove(ZSDIR)
-}
-
-func TestVars(t *testing.T) {
-	tests := map[string]Vars{
-		`
-foo: bar
-title: Hello, world!
----
-Some content in markdown
-`: {
-			"foo":       "bar",
-			"title":     "Hello, world!",
-			"url":       "test.html",
-			"file":      "test.md",
-			"output":    filepath.Join(PUBDIR, "test.html"),
-			"__content": "Some content in markdown\n",
-		},
-		`
-url: "example.com/foo.html"
----
-Hello
-`: {
-			"url":       "example.com/foo.html",
-			"__content": "Hello\n",
-		},
-	}
-
-	for script, vars := range tests {
-		ioutil.WriteFile("test.md", []byte(script), 0644)
-		if v, s, err := getVars("test.md", Vars{"baz": "123"}); err != nil {
-			t.Error(err)
-		} else if s != vars["__content"] {
-			t.Error(s, vars["__content"])
-		} else {
-			for key, value := range vars {
-				if key != "__content" && v[key] != value {
-					t.Error(key, v[key], value)
-				}
-			}
-		}
-	}
-}
-
-func TestRender(t *testing.T) {
-	vars := map[string]string{"foo": "bar"}
-
-	if s, _ := render("foo bar", vars); s != "foo bar" {
-		t.Error(s)
-	}
-	if s, _ := render("a {{printf short}} text", vars); s != "a short text" {
-		t.Error(s)
-	}
-	if s, _ := render("{{printf Hello}} x{{foo}}z", vars); s != "Hello xbarz" {
-		t.Error(s)
-	}
-	// Test error case
-	if _, err := render("a {{greet text ", vars); err == nil {
-		t.Error("error expected")
-	}
-}
--- a/testdata/blog/.test/about.html	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-<html>
-	<head>
-		<title>About myself</title>
-		<link href="styles.css" rel="stylesheet" type="text/css" />
-	</head>
-	<body><h1>About myself</h1>
-
-<p>Hi all. This is a brief description of who I am.</p>
-</body>
-</html>
--- a/testdata/blog/.test/index.html	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-<html>
-	<head>
-		<title>My blog</title>
-		<link href="styles.css" rel="stylesheet" type="text/css" />
-	</head>
-	<body>
-		<p>Here goes list of posts</p>
-		<ul>
-			<li>
-				<a href="/posts/hello.html">First post</a>
-			</li>
-			<li>
-				<a href="/posts/update.html">Second post</a>
-			</li>
-		</ul>
-	</body>
-</html>
--- a/testdata/blog/.test/posts/hello.html	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-<html>
-	<head>
-		<title>First post</title>
-		<link href="styles.css" rel="stylesheet" type="text/css" />
-	</head>
-	<body><h1>First post</h1>
-
-<p>This is my first post</p>
-</body>
-</html>
--- a/testdata/blog/.test/posts/update.html	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-<html>
-	<head>
-		<title>Second post</title>
-		<link href="styles.css" rel="stylesheet" type="text/css" />
-	</head>
-	<body><h1>Second post</h1>
-
-<p>This is my second post</p>
-</body>
-</html>
--- a/testdata/blog/.test/styles.css	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-html{margin:0;padding:0;box-sizing:border-box;}body{font-size:16pt;}
\ No newline at end of file
--- a/testdata/blog/.zs/layout.amber	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-html
-	head
-		title #{title}
-		link[href="styles.css"][rel="stylesheet"][type="text/css"]
-	body
-		#{unescaped(content)}
--- a/testdata/blog/about.md	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-title: About myself
-date: 28-08-2015
----
-
-# {{title}}
-
-Hi all. This is a brief description of who I am.
--- a/testdata/blog/index.amber	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-html
-	head
-		title My blog
-		link[href="styles.css"][rel="stylesheet"][type="text/css"]
-	body
-		p Here goes list of posts
-		ul
-			li
-				a[href="/posts/hello.html"] First post
-			li
-				a[href="/posts/update.html"] Second post
-
--- a/testdata/blog/posts/hello.md	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-title: First post
-date: 28-08-2015
----
-
-# {{title}}
-
-This is my first post
-
--- a/testdata/blog/posts/update.md	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-title: Second post
-date: 29-08-2015
----
-
-# {{title}}
-
-This is my second post
-
--- a/testdata/blog/styles.gcss	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-html
-  margin: 0
-  padding: 0
-  box-sizing: border-box
-
-body
-  font-size: 16pt
--- a/testdata/page/.test/index.html	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-<html>
-	<body>
-		<h1>Hello</h1>
-	</body>
-</html>
--- a/testdata/page/index.html	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-<html>
-	<body>
-		<h1>{{ printf Hello }}</h1>
-	</body>
-</html>
--- a/testdata/sugar/.test/index.html	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-<html>
-	<body>
-		<p>Hello world</p>
-	</body>
-</html>
--- a/testdata/sugar/.test/styles.css	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-body{font:100% Helvetica, sans-serif;color:blue;}
\ No newline at end of file
--- a/testdata/sugar/index.amber	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-html
-	body
-		p Hello world
--- a/testdata/sugar/styles.gcss	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-$base-font: Helvetica, sans-serif
-$main-color: blue
-
-body
-  font: 100% $base-font
-  color: $main-color
--- a/tools/release.sh	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-#!/bin/sh
-
-# Get the highest tag number
-VERSION="$(git describe --abbrev=0 --tags)"
-VERSION=${VERSION:-'0.0.0'}
-
-# Get number parts
-MAJOR="${VERSION%%.*}"; VERSION="${VERSION#*.}"
-MINOR="${VERSION%%.*}"; VERSION="${VERSION#*.}"
-PATCH="${VERSION%%.*}"; VERSION="${VERSION#*.}"
-
-# Increase version
-PATCH=$((PATCH+1))
-
-TAG="${1}"
-
-if [ "${TAG}" = "" ]; then
-  TAG="${MAJOR}.${MINOR}.${PATCH}"
-fi
-
-echo "Releasing ${TAG} ..."
-
-git tag -a -s -m "Release ${TAG}" "${TAG}"
-git push --tags
-goreleaser release --rm-dist
--- a/version.go	Fri Jan 07 23:20:51 2022 +0000
+++ b/version.go	Sun Apr 09 02:39:19 2023 +0000
@@ -1,4 +1,4 @@
-package main
+package aya
 
 import (
 	"fmt"
--- a/zs.1	Fri Jan 07 23:20:51 2022 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-.Dd January 7, 2022
-.Dt ZS 1
-.Os
-.Sh NAME
-.Nm zs
-.Nd Absolutely minimal static site generator written in Go.
-.Sh DESCRIPTION
-.Nm
-is an extremely minimal static site generator written in Go.
-.Pp
-It's inspired by
-.Em zas
-generator, but is even more minimal.
-.Pp
-The name stands for 'zen static'
-.Sh FEATURES
-.Li Zero configuration (no configuration file needed)
-.Pp
-.Li Cross-platform
-.Pp
-.Li Highly extensible
-.Pp
-.Li Works well for blogs and generic static websites (landing pages etc)
-.Pp
-.Li Easy to learn
-.Pp
-.Li Fast
-.Sh USAGE
-.Ss (Re-)build your site.
-.Nm 
-.Cm build
-.Ss (Re-)build one file and prints resulting content to standard output.
-.Nm
-.Cm build
-.Ar <file>
-.Ss (Re-)build your site every time you modify any file.
-.Nm
-.Cm watch
-.Ss Print a list of variables defined in the header of a given markdown file.
-.Nm 
-.Cm var
-.Ar <file>
-.Ar <var1> <var2> ...
-.Sh AUTHORS
-.Nm
-is maintained by James Mills 
-.Aq Lk https://prologic.shortcircuit.net.au/
-.Pp
-This manual page was written by Nova 
-.Aq Lk https://tilde.cafe/~novaburst