From 4a8cfff1bd6c490da7755eb9a3df0cf5915a5894 Mon Sep 17 00:00:00 2001 From: Izuru Yakumo Date: Tue, 12 Dec 2023 11:09:17 -0300 Subject: [PATCH] A good time to finally release a stable version Signed-off-by: Izuru Yakumo --- Makefile | 4 +- README.md | 31 +++--- cmd/aya/build.go | 23 +++++ cmd/aya/buildall.go | 57 +++++++++++ cmd/aya/getvars.go | 64 +++++++++++++ cmd/aya/globals.go | 19 ++++ cmd/aya/main.go | 223 +------------------------------------------ cmd/aya/renameext.go | 19 ++++ cmd/aya/render.go | 42 ++++++++ cmd/aya/run.go | 42 ++++++++ usage.go | 2 +- version.go | 22 +++-- 12 files changed, 301 insertions(+), 247 deletions(-) create mode 100644 cmd/aya/build.go create mode 100644 cmd/aya/buildall.go create mode 100644 cmd/aya/getvars.go create mode 100644 cmd/aya/globals.go create mode 100644 cmd/aya/renameext.go create mode 100644 cmd/aya/render.go create mode 100644 cmd/aya/run.go diff --git a/Makefile b/Makefile index edecdb8..9fdf9a7 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ DESTDIR ?= -GOFLAGS ?= -v -buildvcs=false -mod=vendor -buildmode=exe +GOFLAGS ?= -v -buildvcs=false -mod=vendor -buildmode=exe -ldflags "-w -X `go list`.Date=${DATE} -X `go list`.Vendor=${GOOS} -X `go list`.Version=${VERSION}" PREFIX ?= /usr/local +DATE ?= `date -u +%F` +GOOS ?= `go env GOOS` VERSION ?= `git describe --tags` build: diff --git a/README.md b/README.md index a1cefb0..6b89dca 100644 --- a/README.md +++ b/README.md @@ -17,28 +17,33 @@ Named after [Aya Shameimaru](https://en.touhouwiki.net/wiki/Aya_Shameimaru) from Build it manually assuming you have Go (>=1.17) installed: - $ go install marisa.chaotic.ninja/aya/cmd/aya@latest + $ go install marisa.chaotic.ninja/aya/cmd/aya@latest (1) --- or --- $ git clone https://git.chaotic.ninja/yakumo.izuru/aya $ cd aya $ make # make install - + +(1) If you use this method, the `aya version` subcommand may print the wrong string, +but it should not be a problem unless you use it on a page. + ## Ideology -Keep your texts in markdown, or HTML format right in the main directory +Keep your texts in markdown, [amber](https://github.com/eknkc/amber), or html format right in the main directory of your blog/site. Keep all service files (extensions, layout pages, deployment scripts etc) in the `.aya` subdirectory. -Define variables in the header of the content files using [YAML]: +Define variables in the header of the content files using [YAML](https://www.yaml.io) : - title: My web site - keywords: best website, hello, world - --- +```markdown +title: My web site +keywords: best website, hello, world +--- - Markdown text goes after a header *separator* +Markdown text goes after a header *separator* +``` Use placeholders for variables and plugins in your markdown or html files, e.g. `{{ title }}` or `{{ command arg1 arg2 }}. @@ -46,10 +51,11 @@ files, e.g. `{{ title }}` or `{{ command arg1 arg2 }}. 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 +Everything the extensions prints to [stdout](https://man.freebsd.org/cgi/man.cgi?fd) becomes the value of the placeholder. -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: +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: * `$AYA` - a path to the `aya` executable * `$AYA\_OUTDIR` - a path to the directory with generated files @@ -93,10 +99,7 @@ content generation, or additional commands, like LESS to CSS conversion: lessc < $AYA_OUTDIR/styles.less > $AYA_OUTDIR/styles.css rm -f $AYA_OUTDIR/styles.css -## Extras - -`aya` also supports generating `.html` and `.css` by means of using `.amber` -and `.gcss` files. See more at [eknkc/amber](https://github.com/eknkc/amber) [yosssi/gcss](https://github.com/yosssi/gcss) +Note, you can also place `.gcss` files for [gcss](https://github.com/yosssi/gcss) to process instead ## Command line usage diff --git a/cmd/aya/build.go b/cmd/aya/build.go new file mode 100644 index 0000000..e868d6b --- /dev/null +++ b/cmd/aya/build.go @@ -0,0 +1,23 @@ +// This function passes the files to build to their corresponding functions +// As far as I'm aware, Markdown has three possible filename extensions, +// but .md is the most common one known. +package main + +import ( + "io" + "path/filepath" +) +func build(path string, w io.Writer, vars Vars) error { + ext := filepath.Ext(path) + if ext == ".md" || ext == ".mkd" || ext == ".markdown" { + return buildMarkdown(path, w, vars) + } else if ext == ".htm" || ext == ".html" || ext == ".xht" || ext == ".xhtml" { + return buildHTML(path, w, vars) + } else if ext == ".amber" { + return buildAmber(path, w, vars) + } else if ext == ".gcss" { + return buildGCSS(path, w) + } else { + return buildRaw(path, w) + } +} diff --git a/cmd/aya/buildall.go b/cmd/aya/buildall.go new file mode 100644 index 0000000..e426d8e --- /dev/null +++ b/cmd/aya/buildall.go @@ -0,0 +1,57 @@ +// Build everything and store it on PUBDIR +// If boolean watch is true, it keeps on going +// every time you modify something. +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" +) + +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 + } + fmt.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) + } +} diff --git a/cmd/aya/getvars.go b/cmd/aya/getvars.go new file mode 100644 index 0000000..fc55529 --- /dev/null +++ b/cmd/aya/getvars.go @@ -0,0 +1,64 @@ +// 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 is in YAML +// If no empty newline is found - file is treated as content-only. +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "gopkg.in/yaml.v3" +) + +func getVars(path string, globals Vars) (Vars, string, error) { + b, err := os.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 a layout if none is specified + // For this to work, the layout must be available + // in AYADIR + 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):1] + + 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 and globals with the ones defined 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 + } +} diff --git a/cmd/aya/globals.go b/cmd/aya/globals.go new file mode 100644 index 0000000..969a6d6 --- /dev/null +++ b/cmd/aya/globals.go @@ -0,0 +1,19 @@ +// globals returns list of global OS environment variables that start +// with AYA_ prefix as Vars, so the values can be used inside templates +package main + +import ( + "os" + "strings" +) + +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 +} diff --git a/cmd/aya/main.go b/cmd/aya/main.go index 71009cf..d4b95e4 100644 --- a/cmd/aya/main.go +++ b/cmd/aya/main.go @@ -1,17 +1,11 @@ -// $TheSupernovaDuo: marisa.chaotic.ninja/aya/cmd/aya, v0.7.0 2023-12-11 17:22:51+0000, yakumo_izuru Exp $ +// $TheSupernovaDuo: marisa.chaotic.ninja/aya/cmd/aya, v1.0.0 2023-12-12 13:44:23+0000, yakumo_izuru Exp $ package main import ( - "bytes" "fmt" - "io" "os" - "os/exec" - "path/filepath" "strings" - "time" - "gopkg.in/yaml.v3" "marisa.chaotic.ninja/aya" ) @@ -23,219 +17,6 @@ const ( 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 := os.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 { - fmt.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 := os.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) { - delimOpen := "{{" - delimClose := "}}" - - out := &bytes.Buffer{} - for { - if from := strings.Index(s, delimOpen); from == -1 { - out.WriteString(s) - return out.String(), nil - } else { - if to := strings.Index(s, delimClose); to == -1 { - return "", fmt.Errorf("Closing delimiter not found") - } else { - out.WriteString(s[:from]) - cmd := s[from+len(delimOpen) : to] - s = s[to+len(delimClose):] - 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) - } - } - } - } - -} - -// This function passes the files to build to their corresponding functions -// As far as I'm aware, Markdown has three possible filename extensions, -// but .md is the most common one known. -func build(path string, w io.Writer, vars Vars) error { - ext := filepath.Ext(path) - if ext == ".md" || ext == ".mkd" || ext == ".markdown" { - return buildMarkdown(path, w, vars) - } else if ext == ".htm" || ext == ".html" || ext == ".xht" || ext == ".xhtml" { - return buildHTML(path, w, vars) - } else if ext == ".amber" { - return buildAmber(path, w, vars) - } else if ext == ".gcss" { - return buildGCSS(path, w) - } else { - return buildRaw(path, w) - } -} - -// Build everything and store it on PUBDIR -// If boolean watch is true, it keeps on going -// every time you modify something. -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 - } - fmt.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) - } -} - // Initialize the environment func init() { // prepend .aya to $PATH, so plugins will be found before OS commands @@ -294,7 +75,7 @@ func main() { fmt.Println(strings.TrimSpace(s)) } case "version": - fmt.Printf("%v\n", aya.FullVersion()) + fmt.Printf("%v\n", aya.PrintVersion()) os.Exit(0) case "watch": buildAll(true) diff --git a/cmd/aya/renameext.go b/cmd/aya/renameext.go new file mode 100644 index 0000000..8102e85 --- /dev/null +++ b/cmd/aya/renameext.go @@ -0,0 +1,19 @@ +// 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 +package main + +import ( + "path/filepath" + "strings" +) + +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 +} diff --git a/cmd/aya/render.go b/cmd/aya/render.go new file mode 100644 index 0000000..cc4e766 --- /dev/null +++ b/cmd/aya/render.go @@ -0,0 +1,42 @@ +// Render expanding aya plugins and variables +package main + +import ( + "bytes" + "fmt" + "strings" +) + +func render(s string, vars Vars) (string, error) { + delimOpen := "{{" + delimClose := "}}" + + out := &bytes.Buffer{} + for { + if from := strings.Index(s, delimOpen); from == -1 { + out.WriteString(s) + return out.String(), nil + } else { + if to := strings.Index(s, delimClose); to == -1 { + return "", fmt.Errorf("Closing delimiter not found") + } else { + out.WriteString(s[:from]) + cmd := s[from+len(delimOpen) : to] + s = s[to+len(delimClose):] + 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) + } + } + } + } + +} diff --git a/cmd/aya/run.go b/cmd/aya/run.go new file mode 100644 index 0000000..10b87e3 --- /dev/null +++ b/cmd/aya/run.go @@ -0,0 +1,42 @@ +// run() executes a command or a script. +// Vars define the command environment, each aya var is converted into OS environment variable with AYA_ prefix prepended. +// Additional variable $AYA contains path to the aya binary. +// Command stderr(4) is printed to aya stderr(4), command stdout(4) is returned as a string +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func run(vars Vars, cmd string, args ...string) (string, error) { + // First check if partial exists (.html) + if b, err := os.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 { + fmt.Println("ERROR:", errbuf.String()) + } + if err != nil { + return "", err + } + return string(outbuf.Bytes()), nil +} diff --git a/usage.go b/usage.go index 75a9874..de29f00 100644 --- a/usage.go +++ b/usage.go @@ -6,7 +6,7 @@ import ( // This function is called by the `aya help` subcommand func PrintUsage() { - fmt.Printf("aya/%v\n", FullVersion()) + fmt.Printf("aya/%v\n", PrintFullVersion()) fmt.Println("Homepage: https://aya.chaotic.ninja") fmt.Println("Repository: https://git.chaotic.ninja/yakumo.izuru/aya") fmt.Println("==") diff --git a/version.go b/version.go index 0210513..c23512a 100644 --- a/version.go +++ b/version.go @@ -3,19 +3,21 @@ package aya import ( "fmt" - "time" ) var ( - // Set to current tag - Version = "v0.7.0" - Time = time.Now() + // Variables set at build-time + Date string + Vendor string + Version string ) -// FullVersion display the full version and build -func FullVersion() string { - d := Time.Day() - m := Time.Month() - y := Time.Year() - return fmt.Sprintf("%v || %d.%d.%d", Version, y, m, d) +// PrintVersion only displays the obvious +func PrintVersion() string { + return fmt.Sprintf("%s", Version) +} + +// PrintFullVersion display the full version and build +func PrintFullVersion() string { + return fmt.Sprintf("%s, built at %s, on %s", Version, Date, Vendor) } -- 2.43.0