Changeset 34 in code


Ignore:
Timestamp:
Sep 2, 2015, 5:05:09 PM (10 years ago)
Author:
zaitsev.serge
Message:

rewritten using zs templates, allowing go templates using <% %> delimiters

Location:
trunk
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/testdata/blog/.test/index.html

    r22 r34  
    1 <html>
    2         <head>
    3                 <title>My blog</title>
    4                 <link href="styles.css" rel="stylesheet" type="text/css" />
    5         </head>
    6         <body>
    7                 <p>Here goes list of posts</p>
    8                 <ul>
    9                         <li>
    10                                 <a href="/posts/hello.html">First post</a>
    11                         </li>
    12                         <li>
    13                                 <a href="/posts/update.html">Second post</a>
    14                         </li>
    15                 </ul>
    16         </body>
    17 </html>
  • trunk/testdata/page/.test/index.html

    r17 r34  
    11<html>
    22        <body>
    3                 <h1>Hello
    4 </h1>
     3                <h1>Hello</h1>
    54        </body>
    65</html>
  • trunk/testdata/page/index.html

    r19 r34  
    11<html>
    22        <body>
    3                 <h1>{{ println "Hello" }}</h1>
     3                <h1>{{ printf Hello }}</h1>
    44        </body>
    55</html>
  • trunk/zs.go

    r33 r34  
    99        "os"
    1010        "os/exec"
    11         "path"
    1211        "path/filepath"
    1312        "strings"
     
    1817        "github.com/russross/blackfriday"
    1918        "github.com/yosssi/gcss"
     19        "gopkg.in/yaml.v1"
    2020)
    2121
     
    2727type Vars map[string]string
    2828
    29 func renameExt(path, from, to string) string {
    30         if from == "" {
    31                 from = filepath.Ext(path)
    32         }
    33         if strings.HasSuffix(path, from) {
    34                 return strings.TrimSuffix(path, from) + to
     29// renameExt renames extension (if any) from oldext to newext
     30// If oldext is an empty string - extension is extracted automatically.
     31// If path has no extension - new extension is appended
     32func renameExt(path, oldext, newext string) string {
     33        if oldext == "" {
     34                oldext = filepath.Ext(path)
     35        }
     36        if oldext == "" || strings.HasSuffix(path, oldext) {
     37                return strings.TrimSuffix(path, oldext) + newext
    3538        } else {
    3639                return path
     
    3841}
    3942
     43// globals returns list of global OS environment variables that start
     44// with ZS_ prefix as Vars, so the values can be used inside templates
    4045func globals() Vars {
    4146        vars := Vars{}
     
    4954}
    5055
    51 // Converts zs markdown variables into environment variables
    52 func env(vars Vars) []string {
     56// run executes a command or a script. Vars define the command environment,
     57// each zs var is converted into OS environemnt variable with ZS_ prefix
     58// prepended.  Additional variable $ZS contains path to the zs binary. Command
     59// stderr is printed to zs stderr, command output is returned as a string.
     60func run(vars Vars, cmd string, args ...string) (string, error) {
     61        // First check if partial exists (.amber or .html)
     62        if b, err := ioutil.ReadFile(filepath.Join(ZSDIR, cmd+".amber")); err == nil {
     63                return string(b), nil
     64        }
     65        if b, err := ioutil.ReadFile(filepath.Join(ZSDIR, cmd+".html")); err == nil {
     66                return string(b), nil
     67        }
     68
     69        var errbuf, outbuf bytes.Buffer
     70        c := exec.Command(cmd, args...)
    5371        env := []string{"ZS=" + os.Args[0], "ZS_OUTDIR=" + PUBDIR}
    5472        env = append(env, os.Environ()...)
    55         if vars != nil {
    56                 for k, v := range vars {
    57                         env = append(env, "ZS_"+strings.ToUpper(k)+"="+v)
    58                 }
    59         }
    60         return env
    61 }
    62 
    63 // Runs command with given arguments and variables, intercepts stderr and
    64 // redirects stdout into the given writer
    65 func run(cmd string, args []string, vars Vars, output io.Writer) error {
    66         var errbuf bytes.Buffer
    67         c := exec.Command(cmd, args...)
    68         c.Env = env(vars)
    69         c.Stdout = output
     73        for k, v := range vars {
     74                env = append(env, "ZS_"+strings.ToUpper(k)+"="+v)
     75        }
     76        c.Env = env
     77        c.Stdout = &outbuf
    7078        c.Stderr = &errbuf
    7179
     
    7583                log.Println("ERROR:", errbuf.String())
    7684        }
    77 
    78         if err != nil {
    79                 return err
    80         }
    81         return nil
    82 }
    83 
    84 // Splits a string in exactly two parts by delimiter
    85 // If no delimiter is found - the second string is be empty
    86 func split2(s, delim string) (string, string) {
    87         parts := strings.SplitN(s, delim, 2)
    88         if len(parts) == 2 {
    89                 return parts[0], parts[1]
    90         } else {
    91                 return parts[0], ""
    92         }
    93 }
    94 
    95 // Parses markdown content. Returns parsed header variables and content
    96 func md(path string, globals Vars) (Vars, string, error) {
     85        if err != nil {
     86                return "", err
     87        }
     88        return string(outbuf.Bytes()), nil
     89}
     90
     91// getVars returns list of variables defined in a text file and actual file
     92// content following the variables declaration. Header is separated from
     93// content by an empty line. Header can be either YAML or JSON.
     94// If no empty newline is found - file is treated as content-only.
     95func getVars(path string, globals Vars) (Vars, string, error) {
    9796        b, err := ioutil.ReadFile(path)
    9897        if err != nil {
     
    10099        }
    101100        s := string(b)
    102         url := path[:len(path)-len(filepath.Ext(path))] + ".html"
    103         v := Vars{
    104                 "title":       "",
    105                 "description": "",
    106                 "keywords":    "",
    107         }
     101
     102        // Copy globals first
     103        v := Vars{}
    108104        for name, value := range globals {
    109105                v[name] = value
    110106        }
     107
     108        // Override them by default values extracted from file name/path
    111109        if _, err := os.Stat(filepath.Join(ZSDIR, "layout.amber")); err == nil {
    112110                v["layout"] = "layout.amber"
     
    115113        }
    116114        v["file"] = path
    117         v["url"] = url
    118         v["output"] = filepath.Join(PUBDIR, url)
    119 
    120         if strings.Index(s, "\n\n") == -1 {
     115        v["url"] = path[:len(path)-len(filepath.Ext(path))] + ".html"
     116        v["output"] = filepath.Join(PUBDIR, v["url"])
     117
     118        if sep := strings.Index(s, "\n\n"); sep == -1 {
    121119                return v, s, nil
    122         }
    123         header, body := split2(s, "\n\n")
    124         for _, line := range strings.Split(header, "\n") {
    125                 key, value := split2(line, ":")
    126                 v[strings.ToLower(strings.TrimSpace(key))] = strings.TrimSpace(value)
    127         }
    128         if strings.HasPrefix(v["url"], "./") {
    129                 v["url"] = v["url"][2:]
    130         }
    131         return v, body, nil
    132 }
    133 
    134 // Use standard Go templates
     120        } else {
     121                header := s[:sep]
     122                body := s[sep+len("\n\n"):]
     123                vars := Vars{}
     124                if err := yaml.Unmarshal([]byte(header), &vars); err != nil {
     125                        fmt.Println("ERROR: failed to parse header", err)
     126                } else {
     127                        for key, value := range vars {
     128                                v[key] = value
     129                        }
     130                }
     131                if strings.HasPrefix(v["url"], "./") {
     132                        v["url"] = v["url"][2:]
     133                }
     134                return v, body, nil
     135        }
     136}
     137
     138// Render expanding zs plugins and variables
    135139func render(s string, vars Vars) (string, error) {
    136         tmpl, err := template.New("").Parse(s)
    137         if err != nil {
    138                 return "", err
    139         }
     140        delim_open := "{{"
     141        delim_close := "}}"
     142
    140143        out := &bytes.Buffer{}
    141         if err := tmpl.Execute(out, vars); err != nil {
    142                 return "", err
    143         }
    144         return string(out.Bytes()), nil
     144        for {
     145                if from := strings.Index(s, delim_open); from == -1 {
     146                        out.WriteString(s)
     147                        return out.String(), nil
     148                } else {
     149                        if to := strings.Index(s, delim_close); to == -1 {
     150                                return "", fmt.Errorf("Close delim not found")
     151                        } else {
     152                                out.WriteString(s[:from])
     153                                cmd := s[from+len(delim_open) : to]
     154                                s = s[to+len(delim_close):]
     155                                m := strings.Fields(cmd)
     156                                if len(m) == 1 {
     157                                        if v, ok := vars[m[0]]; ok {
     158                                                out.WriteString(v)
     159                                                continue
     160                                        }
     161                                }
     162                                if res, err := run(vars, m[0], m[1:]...); err == nil {
     163                                        out.WriteString(res)
     164                                } else {
     165                                        fmt.Println(err)
     166                                }
     167                        }
     168                }
     169        }
     170        return s, nil
    145171}
    146172
    147173// Renders markdown with the given layout into html expanding all the macros
    148174func buildMarkdown(path string, w io.Writer, vars Vars) error {
    149         v, body, err := md(path, vars)
     175        v, body, err := getVars(path, vars)
    150176        if err != nil {
    151177                return err
     
    173199// Renders text file expanding all variable macros inside it
    174200func buildHTML(path string, w io.Writer, vars Vars) error {
    175         b, err := ioutil.ReadFile(path)
    176         if err != nil {
    177                 return err
    178         }
    179         content, err := render(string(b), vars)
     201        v, body, err := getVars(path, vars)
     202        if err != nil {
     203                return err
     204        }
     205        if body, err = render(body, v); err != nil {
     206                return err
     207        }
     208        tmpl, err := template.New("").Delims("<%", "%>").Parse(body)
    180209        if err != nil {
    181210                return err
     
    189218                w = f
    190219        }
    191         _, err = io.WriteString(w, content)
    192         return err
     220        return tmpl.Execute(w, vars)
    193221}
    194222
    195223// Renders .amber file into .html
    196224func buildAmber(path string, w io.Writer, vars Vars) error {
     225        v, body, err := getVars(path, vars)
     226        if err != nil {
     227                return err
     228        }
     229        if body, err = render(body, v); err != nil {
     230                return err
     231        }
     232
    197233        a := amber.New()
    198         err := a.ParseFile(path)
    199         if err != nil {
    200                 return err
    201         }
    202 
    203         data := map[string]interface{}{}
    204         for k, v := range vars {
    205                 data[k] = v
     234        if err := a.Parse(body); err != nil {
     235                return err
    206236        }
    207237
     
    218248                w = f
    219249        }
    220         return t.Execute(w, data)
     250        return t.Execute(w, vars)
    221251}
    222252
     
    299329                        } else if info.ModTime().After(lastModified) {
    300330                                if !modified {
    301                                         // About to be modified, so run pre-build hook
    302                                         // FIXME on windows it might not work well
    303                                         run(filepath.Join(ZSDIR, "pre"), []string{}, nil, nil)
     331                                        // First file in this build cycle is about to be modified
     332                                        run(vars, "prehook")
    304333                                        modified = true
    305334                                }
    306                                 log.Println("build: ", path)
     335                                log.Println("build:", path)
    307336                                return build(path, nil, vars)
    308337                        }
     
    313342                }
    314343                if modified {
    315                         // Something was modified, so post-build hook
    316                         // FIXME on windows it might not work well
    317                         run(filepath.Join(ZSDIR, "post"), []string{}, nil, nil)
     344                        // At least one file in this build cycle has been modified
     345                        run(vars, "posthook")
    318346                        modified = false
    319347                }
     
    324352                time.Sleep(1 * time.Second)
    325353        }
     354}
     355
     356func init() {
     357        // prepend .zs to $PATH, so plugins will be found before OS commands
     358        p := os.Getenv("PATH")
     359        p = ZSDIR + ":" + p
     360        os.Setenv("PATH", p)
    326361}
    327362
     
    351386                } else {
    352387                        s := ""
    353                         if vars, _, err := md(args[0], globals()); err != nil {
     388                        if vars, _, err := getVars(args[0], globals()); err != nil {
    354389                                fmt.Println("var: " + err.Error())
    355390                        } else {
     
    367402                }
    368403        default:
    369                 err := run(path.Join(ZSDIR, cmd), args, globals(), os.Stdout)
    370                 if err != nil {
    371                         log.Println("ERROR:", err)
    372                 }
    373         }
    374 }
     404                if s, err := run(globals(), cmd, args...); err != nil {
     405                        fmt.Println(err)
     406                } else {
     407                        fmt.Println(s)
     408                }
     409        }
     410}
  • trunk/zs_test.go

    r24 r34  
    22
    33import (
    4         "bytes"
    5         "fmt"
    64        "io/ioutil"
    7         "log"
    85        "os"
    9         "strings"
     6        "path/filepath"
    107        "testing"
    118)
    129
    13 func TestSplit2(t *testing.T) {
    14         if a, b := split2("a:b", ":"); a != "a" || b != "b" {
    15                 t.Fail()
     10func TestRenameExt(t *testing.T) {
     11        if s := renameExt("foo.amber", ".amber", ".html"); s != "foo.html" {
     12                t.Error(s)
    1613        }
    17         if a, b := split2(":b", ":"); a != "" || b != "b" {
    18                 t.Fail()
     14        if s := renameExt("foo.amber", "", ".html"); s != "foo.html" {
     15                t.Error(s)
    1916        }
    20         if a, b := split2("a:", ":"); a != "a" || b != "" {
    21                 t.Fail()
     17        if s := renameExt("foo.amber", ".md", ".html"); s != "foo.amber" {
     18                t.Error(s)
    2219        }
    23         if a, b := split2(":", ":"); a != "" || b != "" {
    24                 t.Fail()
     20        if s := renameExt("foo", ".amber", ".html"); s != "foo" {
     21                t.Error(s)
    2522        }
    26         if a, b := split2("a", ":"); a != "a" || b != "" {
    27                 t.Fail()
    28         }
    29         if a, b := split2("", ":"); a != "" || b != "" {
    30                 t.Fail()
     23        if s := renameExt("foo", "", ".html"); s != "foo.html" {
     24                t.Error(s)
    3125        }
    3226}
    3327
    34 func tmpfile(path, s string) string {
    35         ioutil.WriteFile(path, []byte(s), 0644)
    36         return path
     28func TestRun(t *testing.T) {
     29        // external command
     30        if s, err := run(Vars{}, "echo", "hello"); err != nil || s != "hello\n" {
     31                t.Error(s, err)
     32        }
     33        // passing variables to plugins
     34        if s, err := run(Vars{"foo": "bar"}, "sh", "-c", "echo $ZS_FOO"); err != nil || s != "bar\n" {
     35                t.Error(s, err)
     36        }
     37
     38        // custom plugin overriding external command
     39        os.Mkdir(ZSDIR, 0755)
     40        script := `#!/bin/sh
     41echo foo
     42`
     43        ioutil.WriteFile(filepath.Join(ZSDIR, "echo"), []byte(script), 0755)
     44        if s, err := run(Vars{}, "echo", "hello"); err != nil || s != "foo\n" {
     45                t.Error(s, err)
     46        }
     47        os.Remove(filepath.Join(ZSDIR, "echo"))
     48        os.Remove(ZSDIR)
    3749}
    3850
    39 func TestMD(t *testing.T) {
    40         defer os.Remove("foo.md")
    41         v, body, _ := md(tmpfile("foo.md", `
    42         title: Hello, world!
    43         keywords: foo, bar, baz
    44         empty:
    45         bayan: [:|||:]
     51func TestVars(t *testing.T) {
     52        tests := map[string]Vars{
     53                `
     54foo: bar
     55title: Hello, world!
    4656
    47 this: is a content`), Vars{})
    48         if v["title"] != "Hello, world!" {
    49                 t.Error()
    50         }
    51         if v["keywords"] != "foo, bar, baz" {
    52                 t.Error()
    53         }
    54         if s, ok := v["empty"]; !ok || len(s) != 0 {
    55                 t.Error()
    56         }
    57         if v["bayan"] != "[:|||:]" {
    58                 t.Error()
    59         }
    60         if body != "this: is a content" {
    61                 t.Error(body)
     57Some content in markdown
     58`: Vars{
     59                        "foo":       "bar",
     60                        "title":     "Hello, world!",
     61                        "url":       "test.html",
     62                        "file":      "test.md",
     63                        "output":    filepath.Join(PUBDIR, "test.html"),
     64                        "__content": "Some content in markdown\n",
     65                },
     66                `url: "example.com/foo.html"
     67
     68Hello
     69`: Vars{
     70                        "url":       "example.com/foo.html",
     71                        "__content": "Hello\n",
     72                },
    6273        }
    6374
    64         // Test empty md
    65         v, body, _ = md(tmpfile("foo.md", ""), Vars{})
    66         if v["url"] != "foo.html" || len(body) != 0 {
    67                 t.Error(v, body)
    68         }
    69 
    70         // Test empty header
    71         v, body, _ = md(tmpfile("foo.md", "Hello"), Vars{})
    72         if v["url"] != "foo.html" || body != "Hello" {
    73                 t.Error(v, body)
     75        for script, vars := range tests {
     76                ioutil.WriteFile("test.md", []byte(script), 0644)
     77                if v, s, err := getVars("test.md", Vars{"baz": "123"}); err != nil {
     78                        t.Error(err)
     79                } else if s != vars["__content"] {
     80                        t.Error(s, vars["__content"])
     81                } else {
     82                        for key, value := range vars {
     83                                if key != "__content" && v[key] != value {
     84                                        t.Error(key, v[key], value)
     85                                }
     86                        }
     87                }
    7488        }
    7589}
     
    7791func TestRender(t *testing.T) {
    7892        vars := map[string]string{"foo": "bar"}
    79         funcs := Funcs{
    80                 "greet": func(s ...string) string {
    81                         if len(s) == 0 {
    82                                 return "hello"
    83                         } else {
    84                                 return "hello " + strings.Join(s, " ")
    85                         }
    86                 },
     93
     94        if s, _ := render("foo bar", vars); s != "foo bar" {
     95                t.Error(s)
    8796        }
    88 
    89         if s, err := render("plain text", funcs, vars); err != nil || s != "plain text" {
    90                 t.Error(s, err)
     97        if s, _ := render("a {{printf short}} text", vars); s != "a short text" {
     98                t.Error(s)
    9199        }
    92         if s, err := render("a {{greet}} text", funcs, vars); err != nil || s != "a hello text" {
    93                 t.Error(s, err)
    94         }
    95         if s, err := render("{{greet}} x{{foo}}z", funcs, vars); err != nil || s != "hello xbarz" {
    96                 t.Error(s, err)
     100        if s, _ := render("{{printf Hello}} x{{foo}}z", vars); s != "Hello xbarz" {
     101                t.Error(s)
    97102        }
    98103        // Test error case
    99         if s, err := render("a {{greet text ", funcs, vars); err == nil || len(s) != 0 {
    100                 t.Error(s, err)
     104        if _, err := render("a {{greet text ", vars); err == nil {
     105                t.Error("error expected")
    101106        }
    102107}
    103 
    104 func TestEnv(t *testing.T) {
    105         e := env(map[string]string{"foo": "bar", "baz": "hello world"})
    106         mustHave := []string{"ZS=" + os.Args[0], "ZS_FOO=bar", "ZS_BAZ=hello world", "PATH="}
    107         for _, s := range mustHave {
    108                 found := false
    109                 for _, v := range e {
    110                         if strings.HasPrefix(v, s) {
    111                                 found = true
    112                                 break
    113                         }
    114                 }
    115                 if !found {
    116                         t.Error("Missing", s)
    117                 }
    118         }
    119 }
    120 
    121 func TestRun(t *testing.T) {
    122         out := bytes.NewBuffer(nil)
    123         err := run("some_unbelievable_command_name", []string{}, map[string]string{}, out)
    124         if err == nil {
    125                 t.Error()
    126         }
    127 
    128         out = bytes.NewBuffer(nil)
    129         err = run(os.Args[0], []string{"-test.run=TestHelperProcess"},
    130                 map[string]string{"helper": "1", "out": "foo", "err": "bar"}, out)
    131         if err != nil {
    132                 t.Error(err)
    133         }
    134         if out.String() != "foo\n" {
    135                 t.Error(out.String())
    136         }
    137 }
    138 
    139 func TestHelperProcess(*testing.T) {
    140         if os.Getenv("ZS_HELPER") != "1" {
    141                 return
    142         }
    143         defer os.Exit(0)                 // TODO check exit code
    144         log.Println(os.Getenv("ZS_ERR")) // stderr
    145         fmt.Println(os.Getenv("ZS_OUT")) // stdout
    146 }
Note: See TracChangeset for help on using the changeset viewer.