# HG changeset patch # User yakumo.izuru # Date 1681007959 0 # Node ID c6785950280e8edcdf8e2feed2f321648c05d2ce # Parent 4d411cdd68c168dd250e3c02ddfbce97a083782e The fastest, period. Signed-off-by: Izuru Yakumo diff -r 4d411cdd68c1 -r c6785950280e .gitignore --- 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 diff -r 4d411cdd68c1 -r c6785950280e .goreleaser.yml --- 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/ diff -r 4d411cdd68c1 -r c6785950280e LICENSE --- 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 diff -r 4d411cdd68c1 -r c6785950280e Makefile --- 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 diff -r 4d411cdd68c1 -r c6785950280e README.md --- 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 \ "" \ "$title" \ - "http://zserge.com/$url" \ + "http://ayaerge.com/$url" \ "$descr" \ "$(date --date @$timestamp -R)" \ - "http://zserge.com/$url" \ + "http://ayaerge.com/$url" \ "" 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 ` re-builds one file and prints resulting content to stdout. +`aya build ` 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 [var1 var2...]` prints a list of variables defined in the +`aya var [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). diff -r 4d411cdd68c1 -r c6785950280e aya.1 --- /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 +.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 +.Ar ... +.Sh AUTHORS +.Nm +is maintained by Izuru Yakumo +.Aq Lk https://pub.chaotic.ninja/~yakumo_izuru/ diff -r 4d411cdd68c1 -r c6785950280e build_test.go --- 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 -} diff -r 4d411cdd68c1 -r c6785950280e cmd/aya/main.go --- /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], " [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) + } + } +} diff -r 4d411cdd68c1 -r c6785950280e go.mod --- 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 diff -r 4d411cdd68c1 -r c6785950280e main.go --- 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], " [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) - } - } -} diff -r 4d411cdd68c1 -r c6785950280e main_test.go --- 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") - } -} diff -r 4d411cdd68c1 -r c6785950280e testdata/blog/.test/about.html --- 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 @@ - - - About myself - - -

About myself

- -

Hi all. This is a brief description of who I am.

- - diff -r 4d411cdd68c1 -r c6785950280e testdata/blog/.test/index.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 @@ - - - My blog - - - -

Here goes list of posts

- - - diff -r 4d411cdd68c1 -r c6785950280e testdata/blog/.test/posts/hello.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 @@ - - - First post - - -

First post

- -

This is my first post

- - diff -r 4d411cdd68c1 -r c6785950280e testdata/blog/.test/posts/update.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 @@ - - - Second post - - -

Second post

- -

This is my second post

- - diff -r 4d411cdd68c1 -r c6785950280e testdata/blog/.test/styles.css --- 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 diff -r 4d411cdd68c1 -r c6785950280e testdata/blog/.zs/layout.amber --- 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)} diff -r 4d411cdd68c1 -r c6785950280e testdata/blog/about.md --- 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. diff -r 4d411cdd68c1 -r c6785950280e testdata/blog/index.amber --- 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 - diff -r 4d411cdd68c1 -r c6785950280e testdata/blog/posts/hello.md --- 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 - diff -r 4d411cdd68c1 -r c6785950280e testdata/blog/posts/update.md --- 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 - diff -r 4d411cdd68c1 -r c6785950280e testdata/blog/styles.gcss --- 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 diff -r 4d411cdd68c1 -r c6785950280e testdata/empty/.empty diff -r 4d411cdd68c1 -r c6785950280e testdata/page/.test/index.html --- 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 @@ - - -

Hello

- - diff -r 4d411cdd68c1 -r c6785950280e testdata/page/index.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 @@ - - -

{{ printf Hello }}

- - diff -r 4d411cdd68c1 -r c6785950280e testdata/sugar/.test/index.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 @@ - - -

Hello world

- - diff -r 4d411cdd68c1 -r c6785950280e testdata/sugar/.test/styles.css --- 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 diff -r 4d411cdd68c1 -r c6785950280e testdata/sugar/index.amber --- 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 diff -r 4d411cdd68c1 -r c6785950280e testdata/sugar/styles.gcss --- 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 diff -r 4d411cdd68c1 -r c6785950280e tools/release.sh --- 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 diff -r 4d411cdd68c1 -r c6785950280e version.go --- 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" diff -r 4d411cdd68c1 -r c6785950280e zs.1 --- 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 -.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 -.Ar ... -.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