changeset 23:40f55059fbfa draft

fixed output file names in html pages, fixed amber function bindings, replaced print command with build, fixed plugin functions, implemented zs and exec functions
author zaitsev.serge
date Sun, 30 Aug 2015 12:20:35 +0000
parents f5627d4212a3
children d052f3a44195
files zs.go zs_ext.go zs_test.go zs_util.go
diffstat 4 files changed, 278 insertions(+), 213 deletions(-) [+]
line wrap: on
line diff
--- a/zs.go	Sat Aug 29 21:58:48 2015 +0000
+++ b/zs.go	Sun Aug 30 12:20:35 2015 +0000
@@ -7,7 +7,6 @@
 	"io/ioutil"
 	"log"
 	"os"
-	"os/exec"
 	"path"
 	"path/filepath"
 	"strings"
@@ -25,17 +24,7 @@
 )
 
 type Vars map[string]string
-
-// Splits a string in exactly two parts by delimiter
-// If no delimiter is found - the second string is be empty
-func split2(s, delim string) (string, string) {
-	parts := strings.SplitN(s, delim, 2)
-	if len(parts) == 2 {
-		return parts[0], parts[1]
-	} else {
-		return parts[0], ""
-	}
-}
+type Funcs template.FuncMap
 
 // Parses markdown content. Returns parsed header variables and content
 func md(path string, globals Vars) (Vars, string, error) {
@@ -77,15 +66,26 @@
 }
 
 // Use standard Go templates
-func render(s string, funcs template.FuncMap, vars Vars) (string, error) {
-	f := template.FuncMap{}
+func render(s string, funcs Funcs, vars Vars) (string, error) {
+	f := Funcs{}
 	for k, v := range funcs {
 		f[k] = v
 	}
 	for k, v := range vars {
 		f[k] = varFunc(v)
 	}
-	tmpl, err := template.New("").Funcs(f).Parse(s)
+	// Plugin functions
+	files, _ := ioutil.ReadDir(ZSDIR)
+	for _, file := range files {
+		if !file.IsDir() {
+			name := file.Name()
+			if !strings.HasSuffix(name, ".html") && !strings.HasSuffix(name, ".amber") {
+				f[strings.TrimSuffix(name, filepath.Ext(name))] = pluginFunc(name, vars)
+			}
+		}
+	}
+
+	tmpl, err := template.New("").Funcs(template.FuncMap(f)).Parse(s)
 	if err != nil {
 		return "", err
 	}
@@ -96,60 +96,8 @@
 	return string(out.Bytes()), nil
 }
 
-// Converts zs markdown variables into environment variables
-func env(vars Vars) []string {
-	env := []string{"ZS=" + os.Args[0], "ZS_OUTDIR=" + PUBDIR}
-	env = append(env, os.Environ()...)
-	if vars != nil {
-		for k, v := range vars {
-			env = append(env, "ZS_"+strings.ToUpper(k)+"="+v)
-		}
-	}
-	return env
-}
-
-// Runs command with given arguments and variables, intercepts stderr and
-// redirects stdout into the given writer
-func run(cmd string, args []string, vars Vars, output io.Writer) error {
-	var errbuf bytes.Buffer
-	c := exec.Command(cmd, args...)
-	c.Env = env(vars)
-	c.Stdout = output
-	c.Stderr = &errbuf
-
-	err := c.Run()
-
-	if errbuf.Len() > 0 {
-		log.Println("ERROR:", errbuf.String())
-	}
-
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
-// Expands macro: either replacing it with the variable value, or
-// running the plugin command and replacing it with the command's output
-func eval(cmd []string, vars Vars) (string, error) {
-	outbuf := bytes.NewBuffer(nil)
-	err := run(path.Join(ZSDIR, cmd[0]), cmd[1:], vars, outbuf)
-	if err != nil {
-		if _, ok := err.(*exec.ExitError); ok {
-			return "", err
-		}
-		outbuf = bytes.NewBuffer(nil)
-		err := run(cmd[0], cmd[1:], vars, outbuf)
-		// Return exit errors, but ignore if the command was not found
-		if _, ok := err.(*exec.ExitError); ok {
-			return "", err
-		}
-	}
-	return outbuf.String(), nil
-}
-
 // Renders markdown with the given layout into html expanding all the macros
-func buildMarkdown(path string, funcs template.FuncMap, vars Vars) error {
+func buildMarkdown(path string, w io.Writer, funcs Funcs, vars Vars) error {
 	v, body, err := md(path, vars)
 	if err != nil {
 		return err
@@ -159,18 +107,24 @@
 		return err
 	}
 	v["content"] = string(blackfriday.MarkdownBasic([]byte(content)))
+	if w == nil {
+		out, err := os.Create(filepath.Join(PUBDIR, renameExt(path, "", ".html")))
+		if err != nil {
+			return err
+		}
+		defer out.Close()
+		w = out
+	}
 	if strings.HasSuffix(v["layout"], ".amber") {
-		return buildAmber(filepath.Join(ZSDIR, v["layout"]),
-			renameExt(path, "", ".html"), funcs, v)
+		return buildAmber(filepath.Join(ZSDIR, v["layout"]), w, funcs, v)
 	} else {
-		return buildPlain(filepath.Join(ZSDIR, v["layout"]),
-			renameExt(path, "", ".html"), funcs, v)
+		return buildHTML(filepath.Join(ZSDIR, v["layout"]), w, funcs, v)
 	}
 }
 
 // Renders text file expanding all variable macros inside it
-func buildPlain(in, out string, funcs template.FuncMap, vars Vars) error {
-	b, err := ioutil.ReadFile(in)
+func buildHTML(path string, w io.Writer, funcs Funcs, vars Vars) error {
+	b, err := ioutil.ReadFile(path)
 	if err != nil {
 		return err
 	}
@@ -178,7 +132,7 @@
 	if err != nil {
 		return err
 	}
-	output := filepath.Join(PUBDIR, out)
+	output := filepath.Join(PUBDIR, path)
 	if s, ok := vars["output"]; ok {
 		output = s
 	}
@@ -190,9 +144,9 @@
 }
 
 // Renders .amber file into .html
-func buildAmber(in, out string, funcs template.FuncMap, vars Vars) error {
+func buildAmber(path string, w io.Writer, funcs Funcs, vars Vars) error {
 	a := amber.New()
-	err := a.ParseFile(in)
+	err := a.ParseFile(path)
 	if err != nil {
 		return err
 	}
@@ -200,110 +154,80 @@
 	if err != nil {
 		return err
 	}
-	//amber.FuncMap = amber.FuncMap
-	f, err := os.Create(filepath.Join(PUBDIR, out))
-	if err != nil {
-		return err
+	if w == nil {
+		f, err := os.Create(filepath.Join(PUBDIR, renameExt(path, ".amber", ".html")))
+		if err != nil {
+			return err
+		}
+		defer f.Close()
+		w = f
 	}
-	defer f.Close()
-	return t.Execute(f, vars)
+	return t.Execute(w, vars)
 }
 
 // Compiles .gcss into .css
-func buildGCSS(path string) error {
+func buildGCSS(path string, w io.Writer) error {
 	f, err := os.Open(path)
 	if err != nil {
 		return err
 	}
-	s := strings.TrimSuffix(path, ".gcss") + ".css"
-	css, err := os.Create(filepath.Join(PUBDIR, s))
-	if err != nil {
-		return err
+	defer f.Close()
+
+	if w == nil {
+		s := strings.TrimSuffix(path, ".gcss") + ".css"
+		css, err := os.Create(filepath.Join(PUBDIR, s))
+		if err != nil {
+			return err
+		}
+		defer css.Close()
+		w = css
 	}
-
-	defer f.Close()
-	defer css.Close()
-
-	_, err = gcss.Compile(css, f)
+	_, err = gcss.Compile(w, f)
 	return err
 }
 
-// Copies file from working directory into public directory
-func copyFile(path string) (err error) {
-	var in, out *os.File
-	if in, err = os.Open(path); err == nil {
-		defer in.Close()
-		if out, err = os.Create(filepath.Join(PUBDIR, path)); err == nil {
+// 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()
-			_, err = io.Copy(out, in)
+			w = out
 		}
 	}
+	_, err = io.Copy(w, in)
 	return err
 }
 
-func varFunc(s string) func() string {
-	return func() string {
-		return s
-	}
-}
-
-func pluginFunc(cmd string) func() string {
-	return func() string {
-		return "Not implemented yet"
+func build(path string, w io.Writer, funcs Funcs, vars Vars) error {
+	ext := filepath.Ext(path)
+	if ext == ".md" || ext == ".mkd" {
+		return buildMarkdown(path, w, funcs, vars)
+	} else if ext == ".html" || ext == ".xml" {
+		return buildHTML(path, w, funcs, vars)
+	} else if ext == ".amber" {
+		return buildAmber(path, w, funcs, vars)
+	} else if ext == ".gcss" {
+		return buildGCSS(path, w)
+	} else {
+		return buildRaw(path, w)
 	}
 }
 
-func createFuncs() template.FuncMap {
-	// Builtin functions
-	funcs := template.FuncMap{
-		"exec": func(s ...string) string {
-			// Run external command with arguments
-			return ""
-		},
-		"zs": func(args ...string) string {
-			// Run zs with arguments
-			return ""
-		},
-	}
-	// Plugin functions
-	files, _ := ioutil.ReadDir(ZSDIR)
-	for _, f := range files {
-		if !f.IsDir() {
-			name := f.Name()
-			if !strings.HasSuffix(name, ".html") && !strings.HasSuffix(name, ".amber") {
-				funcs[strings.TrimSuffix(name, filepath.Ext(name))] = pluginFunc(name)
-			}
-		}
-	}
-	return funcs
-}
-
-func renameExt(path, from, to string) string {
-	if from == "" {
-		from = filepath.Ext(path)
-	}
-	return strings.TrimSuffix(path, from) + to
-}
-
-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
-}
-
-func buildAll(once bool) {
+func buildAll(watch bool) {
 	lastModified := time.Unix(0, 0)
 	modified := false
 
 	vars := globals()
 	for {
 		os.Mkdir(PUBDIR, 0755)
-		funcs := createFuncs()
+		funcs := builtins()
 		err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
 			// ignore hidden files and directories
 			if filepath.Base(path)[0] == '.' || strings.HasPrefix(path, ".") {
@@ -320,23 +244,8 @@
 					run(filepath.Join(ZSDIR, "pre"), []string{}, nil, nil)
 					modified = true
 				}
-				ext := filepath.Ext(path)
-				if ext == ".md" || ext == ".mkd" {
-					log.Println("md: ", path)
-					return buildMarkdown(path, funcs, vars)
-				} else if ext == ".html" || ext == ".xml" {
-					log.Println("html: ", path)
-					return buildPlain(path, path, funcs, vars)
-				} else if ext == ".amber" {
-					log.Println("html: ", path)
-					return buildAmber(path, renameExt(path, ".amber", ".html"), funcs, vars)
-				} else if ext == ".gcss" {
-					log.Println("css: ", path)
-					return buildGCSS(path)
-				} else {
-					log.Println("raw: ", path)
-					return copyFile(path)
-				}
+				log.Println("build: ", path)
+				return build(path, nil, funcs, vars)
 			}
 			return nil
 		})
@@ -349,10 +258,10 @@
 			run(filepath.Join(ZSDIR, "post"), []string{}, nil, nil)
 			modified = false
 		}
-		lastModified = time.Now()
-		if once {
+		if !watch {
 			break
 		}
+		lastModified = time.Now()
 		time.Sleep(1 * time.Second)
 	}
 }
@@ -366,27 +275,23 @@
 	args := os.Args[2:]
 	switch cmd {
 	case "build":
-		buildAll(true)
+		buildAll(false)
 	case "watch":
-		buildAll(false) // pass duration
-	case "var":
-		if len(args) == 0 {
-			log.Println("ERROR: filename expected")
-			return
+		buildAll(true)
+	case "print":
+		if len(args) != 1 {
+			fmt.Println("ERROR: filename expected")
+		} else {
+			build(args[0], os.Stdout, builtins(), globals())
 		}
-		if vars, _, err := md(args[0], globals()); err == nil {
-			if len(args) > 1 {
-				for _, a := range args[1:] {
-					fmt.Println(vars[a])
-				}
-			} else {
-				for k, v := range vars {
-					fmt.Println(k + ":" + v)
-				}
-			}
-		} else {
-			log.Println("ERROR:", err)
-		}
+	case "var":
+		fmt.Println(Var(args))
+	case "lorem":
+		fmt.Println(Lorem(args))
+	case "dateparse":
+		fmt.Println(DateParse(args))
+	case "datefmt":
+		fmt.Println(DateFmt(args))
 	default:
 		err := run(path.Join(ZSDIR, cmd), args, Vars{}, os.Stdout)
 		if err != nil {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/zs_ext.go	Sun Aug 30 12:20:35 2015 +0000
@@ -0,0 +1,72 @@
+package main
+
+import (
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/drhodes/golorem"
+)
+
+// zs var <filename> -- returns list of variables and their values
+// zs var <filename> <var...> -- returns list of variable values
+func Var(args []string) string {
+	if len(args) == 0 {
+		return "var: filename expected"
+	} else {
+		s := ""
+		if vars, _, err := md(args[0], globals()); err != nil {
+			return "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"
+				}
+			}
+		}
+		return strings.TrimSpace(s)
+	}
+}
+
+// zs lorem <n> -- returns <n> random lorem ipsum sentences
+func Lorem(args []string) string {
+	if len(args) > 1 {
+		return "lorem: invalid usage"
+	}
+	if len(args) == 0 {
+		return lorem.Paragraph(5, 5)
+	}
+	if n, err := strconv.Atoi(args[0]); err == nil {
+		return lorem.Paragraph(n, n)
+	} else {
+		return "lorem: " + err.Error()
+	}
+}
+
+// zs datefmt <fmt> <date> -- returns formatted date from unix time
+func DateFmt(args []string) string {
+	if len(args) == 0 || len(args) > 2 {
+		return "datefmt: invalid usage"
+	}
+	if n, err := strconv.ParseInt(args[1], 10, 64); err == nil {
+		return time.Unix(n, 0).Format(args[0])
+	} else {
+		return "datefmt: " + err.Error()
+	}
+}
+
+// zs dateparse <fmt> <date> -- returns unix time from the formatted date
+func DateParse(args []string) string {
+	if len(args) == 0 || len(args) > 2 {
+		return "dateparse: invalid usage"
+	}
+	if d, err := time.Parse(args[0], args[1]); err != nil {
+		return "dateparse: " + err.Error()
+	} else {
+		return strconv.FormatInt(d.Unix(), 10)
+	}
+}
--- a/zs_test.go	Sat Aug 29 21:58:48 2015 +0000
+++ b/zs_test.go	Sun Aug 30 12:20:35 2015 +0000
@@ -6,10 +6,8 @@
 	"io/ioutil"
 	"log"
 	"os"
-	"os/exec"
 	"strings"
 	"testing"
-	"text/template"
 )
 
 func TestSplit2(t *testing.T) {
@@ -78,7 +76,7 @@
 
 func TestRender(t *testing.T) {
 	vars := map[string]string{"foo": "bar"}
-	funcs := template.FuncMap{
+	funcs := Funcs{
 		"greet": func(s ...string) string {
 			if len(s) == 0 {
 				return "hello"
@@ -138,24 +136,6 @@
 	}
 }
 
-func TestEvalCommand(t *testing.T) {
-	s, err := eval([]string{"echo", "hello"}, map[string]string{})
-	if err != nil {
-		t.Error(err)
-	}
-	if s != "hello\n" {
-		t.Error(s)
-	}
-	_, err = eval([]string{"cat", "bogus/file"}, map[string]string{})
-	if _, ok := err.(*exec.ExitError); !ok {
-		t.Error("expected ExitError")
-	}
-	_, err = eval([]string{"missing command"}, map[string]string{})
-	if err != nil {
-		t.Error("missing command should be ignored")
-	}
-}
-
 func TestHelperProcess(*testing.T) {
 	if os.Getenv("ZS_HELPER") != "1" {
 		return
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/zs_util.go	Sun Aug 30 12:20:35 2015 +0000
@@ -0,0 +1,108 @@
+package main
+
+import (
+	"bytes"
+	"io"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+)
+
+func varFunc(s string) func() string {
+	return func() string {
+		return s
+	}
+}
+
+func pluginFunc(cmd string, vars Vars) func(args ...string) string {
+	return func(args ...string) string {
+		out := bytes.NewBuffer(nil)
+		if err := run(cmd, args, vars, out); err != nil {
+			return cmd + ":" + err.Error()
+		} else {
+			return string(out.Bytes())
+		}
+	}
+}
+
+func builtins() Funcs {
+	exec := func(s ...string) string {
+		return ""
+	}
+	return Funcs{
+		"exec": exec,
+		"zs": func(args ...string) string {
+			cmd := []string{"zs"}
+			cmd = append(cmd, args...)
+			return exec(cmd...)
+		},
+	}
+}
+
+func renameExt(path, from, to string) string {
+	if from == "" {
+		from = filepath.Ext(path)
+	}
+	if strings.HasSuffix(path, from) {
+		return strings.TrimSuffix(path, from) + to
+	} else {
+		return path
+	}
+}
+
+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
+}
+
+// Converts zs markdown variables into environment variables
+func env(vars Vars) []string {
+	env := []string{"ZS=" + os.Args[0], "ZS_OUTDIR=" + PUBDIR}
+	env = append(env, os.Environ()...)
+	if vars != nil {
+		for k, v := range vars {
+			env = append(env, "ZS_"+strings.ToUpper(k)+"="+v)
+		}
+	}
+	return env
+}
+
+// Runs command with given arguments and variables, intercepts stderr and
+// redirects stdout into the given writer
+func run(cmd string, args []string, vars Vars, output io.Writer) error {
+	var errbuf bytes.Buffer
+	c := exec.Command(cmd, args...)
+	c.Env = env(vars)
+	c.Stdout = output
+	c.Stderr = &errbuf
+
+	err := c.Run()
+
+	if errbuf.Len() > 0 {
+		log.Println("ERROR:", errbuf.String())
+	}
+
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// Splits a string in exactly two parts by delimiter
+// If no delimiter is found - the second string is be empty
+func split2(s, delim string) (string, string) {
+	parts := strings.SplitN(s, delim, 2)
+	if len(parts) == 2 {
+		return parts[0], parts[1]
+	} else {
+		return parts[0], ""
+	}
+}