commit 912752f46b6075653e1e356845eee1aeeb5e52df
parent c92784b306e68ebb5ff5722e9f52e4cf65a39035
Author: gearsix <gearsix@tuta.io>
Date: Fri, 18 Mar 2022 15:00:03 +0000
REBRAND to 'dati - data and template interface'
Diffstat:
14 files changed, 346 insertions(+), 341 deletions(-)
diff --git a/CHANGELOG b/CHANGELOG
@@ -1,5 +1,9 @@
# CHANGELOG
+## v0.8.0
+
+- rebranded to dati - data and template interface
+
## v0.7.0
- added LoadTemplate, loads templates from io.Reader params.
diff --git a/README b/README
@@ -1,29 +1,30 @@
-suti
+
+dati
====
-simple unified templating interface
+data and template interface
USAGE
-----
- suti [OPTIONS]
+ dati [OPTIONS]
DESCRIPTION
-----------
- suti aims to provide a universal interface for executing data files,
+ dati aims to provide a universal interface for executing data files,
written in any data-serialization language, against template files,
written in any templating languages.
- Ideally suti will support any language you want to use.
+ Ideally dati will support any language you want to use.
- suti works by using various libraries that do all the hard work to
+ dati works by using various libraries that do all the hard work to
parse data and template files passed to it. It generates a data
structure of all the passed data files combined (a super-data
structure) and executes that structure against a set of root template
files.
The used libraries are listed below for credit/reference.
- suti can also be imported as a golang package to be used as a library.
+ dati can also be imported as a golang package to be used as a library.
OPTIONS
-------
@@ -54,7 +55,7 @@ OPTIONS
CONFIG
------
- It's possible you'll want to set the same options if you run suti multiple
+ It's possible you'll want to set the same options if you run dati multiple
times for the same project. This can be done by creating a file (written as
a data file) and passing the filepath to the -cfg argument.
@@ -71,10 +72,10 @@ CONFIG
DATA
----
- suti generates a single super-structure of all the data files passed to it.
+ dati generates a single super-structure of all the data files passed to it.
This super-structure is executed against each "root" template.
- The super-structure generated by suti will only have 1 definite key: "data"
+ The super-structure generated by dati will only have 1 definite key: "data"
(or the value of the "data-key" option). This key will overwrite any "global
data" keys in the root of the super-structure. Its value will be an array,
where each element is the resulting data structure of each parsed "data"
@@ -88,9 +89,9 @@ DATA
TEMPLATES
---------
- All "root" template files passed to suti that have a file extension matching
+ All "root" template files passed to dati that have a file extension matching
one of the supported templating languages will be parsed and executed
- against the super-structure generated by suti.
+ against the super-structure generated by dati.
All "parital" templates will be parsed into any "root" templates that have a
file extension that match the same templating language.
@@ -119,16 +120,16 @@ SUPPORTED LANGUAGES
EXAMPLES
--------
- suti -cfg ./suti.cfg -r templates/textfile.mst
+ dati -cfg ./dati.cfg -r templates/textfile.mst
- suti -r homepage.hmpl -p head.hmpl -p body.hmpl -gd meta.json -d posts/*
+ dati -r homepage.hmpl -p head.hmpl -p body.hmpl -gd meta.json -d posts/*
- see the examples/ directory in the suti repository for a cool example.
+ see the examples/ directory in the dati repository for a cool example.
LIBRARIES
---------
- As stated above, all of these libraries do the hard work, suti just combines
+ As stated above, all of these libraries do the hard work, dati just combines
it all together - so thanks to the authors. Also here for reference.
- The Go standard library is used for parsing JSON, .tmpl/.gotmpl, .hmpl/.gohmpl
diff --git a/cmd/dati.go b/cmd/dati.go
@@ -0,0 +1,295 @@
+package main
+
+/*
+ Copyright (C) 2021 gearsix <gearsix@tuta.io>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "notabug.org/gearsix/dati"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// Data is just a generic map for key/value data
+type Data map[string]interface{}
+
+type options struct {
+ RootPath string
+ PartialPaths []string
+ GlobalDataPaths []string
+ DataPaths []string
+ DataKey string
+ SortData string
+ ConfigFile string
+}
+
+var opts options
+var cwd string
+
+func warn(err error, msg string, args ...interface{}) {
+ warning := "WARNING "
+ if len(msg) > 0 {
+ warning += strings.TrimSuffix(fmt.Sprintf(msg, args...), "\n")
+ if err != nil {
+ warning += ": "
+ }
+ }
+ if err != nil {
+ warning += err.Error()
+ }
+ fmt.Println(warning)
+}
+
+func assert(err error, msg string, args ...interface{}) {
+ if err != nil {
+ fmt.Printf("ERROR %s\n%s\n", strings.TrimSuffix(fmt.Sprintf(msg, args...), "\n"), err)
+ os.Exit(1)
+ }
+}
+
+func basedir(path string) string {
+ if !filepath.IsAbs(path) {
+ path = filepath.Join(cwd, path)
+ }
+ return path
+}
+
+func init() {
+ if len(os.Args) <= 1 {
+ fmt.Println("nothing to do")
+ os.Exit(0)
+ }
+
+ opts = parseArgs(os.Args[1:], options{})
+ if len(opts.ConfigFile) != 0 {
+ cwd = filepath.Dir(opts.ConfigFile)
+ opts = parseConfig(opts.ConfigFile, opts)
+ }
+ opts = setDefaultOptions(opts)
+}
+
+func main() {
+ var err error
+ var global Data
+ var data []Data
+ var template dati.Template
+ var out bytes.Buffer
+
+ opts.GlobalDataPaths = loadFilePaths(opts.GlobalDataPaths...)
+ for _, path := range opts.GlobalDataPaths {
+ var d Data
+ err = dati.LoadDataFilepath(path, &d)
+ assert(err, "failed to load global data '%s'", path)
+ data = append(data, d)
+ }
+ global = mergeData(data)
+
+ opts.DataPaths = loadFilePaths(opts.DataPaths...)
+ opts.DataPaths, err = dati.SortFileList(opts.DataPaths, opts.SortData)
+ if err != nil {
+ warn(err, "failed to sort data files")
+ }
+ data = make([]Data, 0)
+ for _, path := range opts.DataPaths {
+ var d Data
+ err = dati.LoadDataFilepath(path, &d)
+ assert(err, "failed to load data '%s'", path)
+ data = append(data, d)
+ }
+ global[opts.DataKey] = data
+
+ template, err = dati.LoadTemplateFilepath(opts.RootPath, opts.PartialPaths...)
+ assert(err, "unable to load templates")
+
+ out, err = template.Execute(global)
+ assert(err, "failed to execute template '%s'", opts.RootPath)
+ fmt.Print(out.String())
+
+ return
+}
+
+func help() {
+ fmt.Print("Usage: dati [OPTIONS]\n\n")
+
+ fmt.Print("Options")
+ fmt.Print(`
+ -r path, -root path
+ path of template file to execute against.
+
+ -p path..., -partial path...
+ path of (multiple) template files that are called upon by at least one
+ root template. If a directory is passed then all files within that
+ directory will (recursively) be loaded.
+
+ -gd path..., -global-data path...
+ path of (multiple) data files to load as "global data". If a directory is
+ passed then all files within that directory will (recursively) be loaded.
+
+ -d path..., -data path...
+ path of (multiple) data files to load as "data". If a directory is passed
+ then all files within that directory will (recursively) be loaded.
+
+ -dk name, -data-key name
+ set the name of the key used for the generated array of data (default:
+ "data")
+
+ -sd attribute, -sort-data attribute
+ The file attribute to order data files by. If no value is provided, the data
+ will be provided in the order it's loaded.
+ Accepted values: "filename", "modified".
+ A suffix can be appended to each value to set the sort order: "-asc" (for
+ ascending), "-desc" (for descending). If not specified, this defaults to
+ "-asc".
+ -cfg file, -config file
+ A data file to provide default values for the above options (see CONFIG).
+
+`)
+
+ fmt.Println("See doc/dati.txt for further details")
+}
+
+// custom arg parser because golang.org/pkg/flag doesn't support list args
+func parseArgs(args []string, existing options) (o options) {
+ o = existing
+ var flag string
+ for a := 0; a < len(args); a++ {
+ arg := args[a]
+ if arg[0] == '-' && flag != "--" {
+ flag = arg
+ ndelims := 0
+ for len(flag) > 0 && flag[0] == '-' {
+ flag = flag[1:]
+ ndelims++
+ }
+
+ if ndelims > 2 {
+ warn(nil, "bad flag syntax: '%s'", arg)
+ flag = ""
+ }
+
+ if strings.Contains(flag, "=") {
+ split := strings.SplitN(flag, "=", 2)
+ flag = split[0]
+ args[a] = split[1]
+ a--
+ }
+
+ // set valid any flags that don't take arguments here
+ if flag == "h" || flag == "help" {
+ help()
+ os.Exit(0)
+ }
+ } else if (flag == "r" || flag == "root") && len(o.RootPath) == 0 {
+ o.RootPath = basedir(arg)
+ } else if flag == "p" || flag == "partial" {
+ o.PartialPaths = append(o.PartialPaths, basedir(arg))
+ } else if flag == "gd" || flag == "globaldata" {
+ o.GlobalDataPaths = append(o.GlobalDataPaths, basedir(arg))
+ } else if flag == "d" || flag == "data" {
+ o.DataPaths = append(o.DataPaths, basedir(arg))
+ } else if flag == "dk" || flag == "datakey" && len(o.DataKey) == 0 {
+ o.DataKey = arg
+ } else if flag == "sd" || flag == "sortdata" && len(o.SortData) == 0 {
+ o.SortData = arg
+ } else if flag == "cfg" || flag == "config" && len(o.ConfigFile) == 0 {
+ o.ConfigFile = basedir(arg)
+ } else if len(flag) == 0 {
+ // skip unknown flag arguments
+ } else {
+ warn(nil, "ignoring flag: '%s'", flag)
+ flag = ""
+ }
+ }
+
+ return
+}
+
+func parseConfig(fpath string, existing options) options {
+ var err error
+ var cfgf *os.File
+ if cfgf, err = os.Open(fpath); err != nil {
+ warn(err, "error loading config file '%s'", fpath)
+ }
+ defer cfgf.Close()
+
+ var args []string
+ scanf := bufio.NewScanner(cfgf)
+ for scanf.Scan() {
+ for i, arg := range strings.Split(scanf.Text(), "=") {
+ arg = strings.TrimSpace(arg)
+ if i == 0 {
+ arg = "-" + arg
+ }
+ args = append(args, arg)
+ }
+ }
+ return parseArgs(args, existing)
+}
+
+func setDefaultOptions(o options) options {
+ if len(o.SortData) == 0 {
+ o.SortData = "filename"
+ }
+ if len(o.DataKey) == 0 {
+ o.DataKey = "data"
+ }
+ return o
+}
+
+// load glob & dir filepaths as individual filepaths
+func loadFilePaths(paths ...string) (filepaths []string) {
+ for _, path := range paths {
+ var err error
+ if strings.Contains(path, "*") {
+ var glob []string
+ glob, err = filepath.Glob(path)
+ assert(err, "failed to glob '%s'", path)
+ for _, p := range glob {
+ filepaths = append(filepaths, p)
+ }
+ } else {
+ err = filepath.Walk(path,
+ func(p string, info os.FileInfo, e error) error {
+ if e == nil && !info.IsDir() {
+ filepaths = append(filepaths, p)
+ }
+ return e
+ })
+ }
+ if err != nil {
+ assert(err, "failed to load filepaths for '%s'", path)
+ }
+ }
+ return
+}
+
+func mergeData(data []Data) (merged Data) {
+ merged = make(Data)
+ for _, d := range data {
+ for key, val := range d {
+ if merged[key] == nil {
+ merged[key] = val
+ } else {
+ warn(nil, "merge conflict for global data key: '%s'", key)
+ }
+ }
+ }
+ return
+}
diff --git a/cmd/dati_test.sh b/cmd/dati_test.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env sh
+# if nothing prints, the test passed
+
+diff="diff -bs"
+fail=0
+
+go build -o dati dati.go
+
+if [ -e dati ]; then
+ ./dati -cfg ../examples/dati.cfg -r ../examples/template/html.hmpl > out.html
+ $diff out.html ../examples/out.html
+ if [ $? -ne 0 ]; then fail=1; else rm out.html; fi
+
+ ./dati -cfg ../examples/dati.cfg -r ../examples/template/txt.mst > out.txt
+ $diff out.txt ../examples/out.txt
+ if [ $? -ne 0 ]; then fail=1; else rm out.txt; fi
+
+ rm dati
+
+ if [ $fail -eq 1 ]; then echo "TEST FAIL"; else echo "TEST PASS"; fi
+fi
diff --git a/cmd/suti.go b/cmd/suti.go
@@ -1,295 +0,0 @@
-package main
-
-/*
- Copyright (C) 2021 gearsix <gearsix@tuta.io>
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import (
- "bufio"
- "bytes"
- "fmt"
- "notabug.org/gearsix/suti"
- "os"
- "path/filepath"
- "strings"
-)
-
-// Data is just a generic map for key/value data
-type Data map[string]interface{}
-
-type options struct {
- RootPath string
- PartialPaths []string
- GlobalDataPaths []string
- DataPaths []string
- DataKey string
- SortData string
- ConfigFile string
-}
-
-var opts options
-var cwd string
-
-func warn(err error, msg string, args ...interface{}) {
- warning := "WARNING "
- if len(msg) > 0 {
- warning += strings.TrimSuffix(fmt.Sprintf(msg, args...), "\n")
- if err != nil {
- warning += ": "
- }
- }
- if err != nil {
- warning += err.Error()
- }
- fmt.Println(warning)
-}
-
-func assert(err error, msg string, args ...interface{}) {
- if err != nil {
- fmt.Printf("ERROR %s\n%s\n", strings.TrimSuffix(fmt.Sprintf(msg, args...), "\n"), err)
- os.Exit(1)
- }
-}
-
-func basedir(path string) string {
- if !filepath.IsAbs(path) {
- path = filepath.Join(cwd, path)
- }
- return path
-}
-
-func init() {
- if len(os.Args) <= 1 {
- fmt.Println("nothing to do")
- os.Exit(0)
- }
-
- opts = parseArgs(os.Args[1:], options{})
- if len(opts.ConfigFile) != 0 {
- cwd = filepath.Dir(opts.ConfigFile)
- opts = parseConfig(opts.ConfigFile, opts)
- }
- opts = setDefaultOptions(opts)
-}
-
-func main() {
- var err error
- var global Data
- var data []Data
- var template suti.Template
- var out bytes.Buffer
-
- opts.GlobalDataPaths = loadFilePaths(opts.GlobalDataPaths...)
- for _, path := range opts.GlobalDataPaths {
- var d Data
- err = suti.LoadDataFilepath(path, &d)
- assert(err, "failed to load global data '%s'", path)
- data = append(data, d)
- }
- global = mergeData(data)
-
- opts.DataPaths = loadFilePaths(opts.DataPaths...)
- opts.DataPaths, err = suti.SortFileList(opts.DataPaths, opts.SortData)
- if err != nil {
- warn(err, "failed to sort data files")
- }
- data = make([]Data, 0)
- for _, path := range opts.DataPaths {
- var d Data
- err = suti.LoadDataFilepath(path, &d)
- assert(err, "failed to load data '%s'", path)
- data = append(data, d)
- }
- global[opts.DataKey] = data
-
- template, err = suti.LoadTemplateFilepath(opts.RootPath, opts.PartialPaths...)
- assert(err, "unable to load templates")
-
- out, err = template.Execute(global)
- assert(err, "failed to execute template '%s'", opts.RootPath)
- fmt.Print(out.String())
-
- return
-}
-
-func help() {
- fmt.Print("Usage: suti [OPTIONS]\n\n")
-
- fmt.Print("Options")
- fmt.Print(`
- -r path, -root path
- path of template file to execute against.
-
- -p path..., -partial path...
- path of (multiple) template files that are called upon by at least one
- root template. If a directory is passed then all files within that
- directory will (recursively) be loaded.
-
- -gd path..., -global-data path...
- path of (multiple) data files to load as "global data". If a directory is
- passed then all files within that directory will (recursively) be loaded.
-
- -d path..., -data path...
- path of (multiple) data files to load as "data". If a directory is passed
- then all files within that directory will (recursively) be loaded.
-
- -dk name, -data-key name
- set the name of the key used for the generated array of data (default:
- "data")
-
- -sd attribute, -sort-data attribute
- The file attribute to order data files by. If no value is provided, the data
- will be provided in the order it's loaded.
- Accepted values: "filename", "modified".
- A suffix can be appended to each value to set the sort order: "-asc" (for
- ascending), "-desc" (for descending). If not specified, this defaults to
- "-asc".
- -cfg file, -config file
- A data file to provide default values for the above options (see CONFIG).
-
-`)
-
- fmt.Println("See doc/suti.txt for further details")
-}
-
-// custom arg parser because golang.org/pkg/flag doesn't support list args
-func parseArgs(args []string, existing options) (o options) {
- o = existing
- var flag string
- for a := 0; a < len(args); a++ {
- arg := args[a]
- if arg[0] == '-' && flag != "--" {
- flag = arg
- ndelims := 0
- for len(flag) > 0 && flag[0] == '-' {
- flag = flag[1:]
- ndelims++
- }
-
- if ndelims > 2 {
- warn(nil, "bad flag syntax: '%s'", arg)
- flag = ""
- }
-
- if strings.Contains(flag, "=") {
- split := strings.SplitN(flag, "=", 2)
- flag = split[0]
- args[a] = split[1]
- a--
- }
-
- // set valid any flags that don't take arguments here
- if flag == "h" || flag == "help" {
- help()
- os.Exit(0)
- }
- } else if (flag == "r" || flag == "root") && len(o.RootPath) == 0 {
- o.RootPath = basedir(arg)
- } else if flag == "p" || flag == "partial" {
- o.PartialPaths = append(o.PartialPaths, basedir(arg))
- } else if flag == "gd" || flag == "globaldata" {
- o.GlobalDataPaths = append(o.GlobalDataPaths, basedir(arg))
- } else if flag == "d" || flag == "data" {
- o.DataPaths = append(o.DataPaths, basedir(arg))
- } else if flag == "dk" || flag == "datakey" && len(o.DataKey) == 0 {
- o.DataKey = arg
- } else if flag == "sd" || flag == "sortdata" && len(o.SortData) == 0 {
- o.SortData = arg
- } else if flag == "cfg" || flag == "config" && len(o.ConfigFile) == 0 {
- o.ConfigFile = basedir(arg)
- } else if len(flag) == 0 {
- // skip unknown flag arguments
- } else {
- warn(nil, "ignoring flag: '%s'", flag)
- flag = ""
- }
- }
-
- return
-}
-
-func parseConfig(fpath string, existing options) options {
- var err error
- var cfgf *os.File
- if cfgf, err = os.Open(fpath); err != nil {
- warn(err, "error loading config file '%s'", fpath)
- }
- defer cfgf.Close()
-
- var args []string
- scanf := bufio.NewScanner(cfgf)
- for scanf.Scan() {
- for i, arg := range strings.Split(scanf.Text(), "=") {
- arg = strings.TrimSpace(arg)
- if i == 0 {
- arg = "-" + arg
- }
- args = append(args, arg)
- }
- }
- return parseArgs(args, existing)
-}
-
-func setDefaultOptions(o options) options {
- if len(o.SortData) == 0 {
- o.SortData = "filename"
- }
- if len(o.DataKey) == 0 {
- o.DataKey = "data"
- }
- return o
-}
-
-// load glob & dir filepaths as individual filepaths
-func loadFilePaths(paths ...string) (filepaths []string) {
- for _, path := range paths {
- var err error
- if strings.Contains(path, "*") {
- var glob []string
- glob, err = filepath.Glob(path)
- assert(err, "failed to glob '%s'", path)
- for _, p := range glob {
- filepaths = append(filepaths, p)
- }
- } else {
- err = filepath.Walk(path,
- func(p string, info os.FileInfo, e error) error {
- if e == nil && !info.IsDir() {
- filepaths = append(filepaths, p)
- }
- return e
- })
- }
- if err != nil {
- assert(err, "failed to load filepaths for '%s'", path)
- }
- }
- return
-}
-
-func mergeData(data []Data) (merged Data) {
- merged = make(Data)
- for _, d := range data {
- for key, val := range d {
- if merged[key] == nil {
- merged[key] = val
- } else {
- warn(nil, "merge conflict for global data key: '%s'", key)
- }
- }
- }
- return
-}
diff --git a/cmd/suti_test.sh b/cmd/suti_test.sh
@@ -1,21 +0,0 @@
-#!/bin/sh
-# if nothing prints, the test passed
-
-diff="diff -bs"
-fail=0
-
-go build -o suti suti.go
-
-if [ -e suti ]; then
- ./suti -cfg ../examples/suti.cfg -r ../examples/template/html.hmpl > out.html
- $diff out.html ../examples/out.html
- if [ $? -ne 0 ]; then fail=1; else rm out.html; fi
-
- ./suti -cfg ../examples/suti.cfg -r ../examples/template/txt.mst > out.txt
- $diff out.txt ../examples/out.txt
- if [ $? -ne 0 ]; then fail=1; else rm out.txt; fi
-
- rm suti
-
- if [ $fail -eq 1 ]; then echo "TEST FAIL"; else echo "TEST PASS"; fi
-fi
diff --git a/data.go b/data.go
@@ -1,4 +1,4 @@
-package suti
+package dati
/*
Copyright (C) 2021 gearsix <gearsix@tuta.io>
diff --git a/data_test.go b/data_test.go
@@ -1,4 +1,4 @@
-package suti
+package dati
/*
Copyright (C) 2021 gearsix <gearsix@tuta.io>
diff --git a/examples/README b/examples/README
@@ -6,7 +6,7 @@ denoted by the filename (e.g. html.tmpl produces a html file).
An example of how you can run one of these examples:
- suti -cfg suti.cfg -r templates/html.tmpl > out.html
+ dati -cfg dati.cfg -r templates/html.tmpl > out.html
Then open out.html to see the results.
diff --git a/file.go b/file.go
@@ -1,4 +1,4 @@
-package suti
+package dati
/*
Copyright (C) 2021 gearsix <gearsix@tuta.io>
diff --git a/file_test.go b/file_test.go
@@ -1,4 +1,4 @@
-package suti
+package dati
import (
"os"
diff --git a/go.mod b/go.mod
@@ -1,4 +1,4 @@
-module notabug.org/gearsix/suti
+module notabug.org/gearsix/dati
go 1.13
diff --git a/template.go b/template.go
@@ -1,4 +1,4 @@
-package suti
+package dati
/*
Copyright (C) 2021 gearsix <gearsix@tuta.io>
@@ -54,7 +54,7 @@ func getTemplateType(path string) string {
return strings.TrimPrefix(filepath.Ext(path), ".")
}
-// Template is a wrapper to interface with any template parsed by suti.
+// Template is a wrapper to interface with any template parsed by dati.
// Ideally it would have just been an interface{} that defines Execute but
// the libaries being used aren't that uniform.
type Template struct {
diff --git a/template_test.go b/template_test.go
@@ -1,4 +1,4 @@
-package suti
+package dati
/*
Copyright (C) 2021 gearsix <gearsix@tuta.io>