Changeset 34 in code
- Timestamp:
- Sep 2, 2015, 5:05:09 PM (10 years ago)
- 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 1 1 <html> 2 2 <body> 3 <h1>Hello 4 </h1> 3 <h1>Hello</h1> 5 4 </body> 6 5 </html> -
trunk/testdata/page/index.html
r19 r34 1 1 <html> 2 2 <body> 3 <h1>{{ print ln "Hello"}}</h1>3 <h1>{{ printf Hello }}</h1> 4 4 </body> 5 5 </html> -
trunk/zs.go
r33 r34 9 9 "os" 10 10 "os/exec" 11 "path"12 11 "path/filepath" 13 12 "strings" … … 18 17 "github.com/russross/blackfriday" 19 18 "github.com/yosssi/gcss" 19 "gopkg.in/yaml.v1" 20 20 ) 21 21 … … 27 27 type Vars map[string]string 28 28 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 32 func 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 35 38 } else { 36 39 return path … … 38 41 } 39 42 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 40 45 func globals() Vars { 41 46 vars := Vars{} … … 49 54 } 50 55 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. 60 func 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...) 53 71 env := []string{"ZS=" + os.Args[0], "ZS_OUTDIR=" + PUBDIR} 54 72 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 70 78 c.Stderr = &errbuf 71 79 … … 75 83 log.Println("ERROR:", errbuf.String()) 76 84 } 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. 95 func getVars(path string, globals Vars) (Vars, string, error) { 97 96 b, err := ioutil.ReadFile(path) 98 97 if err != nil { … … 100 99 } 101 100 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{} 108 104 for name, value := range globals { 109 105 v[name] = value 110 106 } 107 108 // Override them by default values extracted from file name/path 111 109 if _, err := os.Stat(filepath.Join(ZSDIR, "layout.amber")); err == nil { 112 110 v["layout"] = "layout.amber" … … 115 113 } 116 114 v["file"] = path 117 v["url"] = url118 v["output"] = filepath.Join(PUBDIR, url)119 120 if s trings.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 { 121 119 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 135 139 func 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 140 143 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 145 171 } 146 172 147 173 // Renders markdown with the given layout into html expanding all the macros 148 174 func buildMarkdown(path string, w io.Writer, vars Vars) error { 149 v, body, err := md(path, vars)175 v, body, err := getVars(path, vars) 150 176 if err != nil { 151 177 return err … … 173 199 // Renders text file expanding all variable macros inside it 174 200 func 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) 180 209 if err != nil { 181 210 return err … … 189 218 w = f 190 219 } 191 _, err = io.WriteString(w, content) 192 return err 220 return tmpl.Execute(w, vars) 193 221 } 194 222 195 223 // Renders .amber file into .html 196 224 func 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 197 233 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 206 236 } 207 237 … … 218 248 w = f 219 249 } 220 return t.Execute(w, data)250 return t.Execute(w, vars) 221 251 } 222 252 … … 299 329 } else if info.ModTime().After(lastModified) { 300 330 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") 304 333 modified = true 305 334 } 306 log.Println("build: 335 log.Println("build:", path) 307 336 return build(path, nil, vars) 308 337 } … … 313 342 } 314 343 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") 318 346 modified = false 319 347 } … … 324 352 time.Sleep(1 * time.Second) 325 353 } 354 } 355 356 func 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) 326 361 } 327 362 … … 351 386 } else { 352 387 s := "" 353 if vars, _, err := md(args[0], globals()); err != nil {388 if vars, _, err := getVars(args[0], globals()); err != nil { 354 389 fmt.Println("var: " + err.Error()) 355 390 } else { … … 367 402 } 368 403 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 2 2 3 3 import ( 4 "bytes"5 "fmt"6 4 "io/ioutil" 7 "log"8 5 "os" 9 " strings"6 "path/filepath" 10 7 "testing" 11 8 ) 12 9 13 func Test Split2(t *testing.T) {14 if a, b := split2("a:b", ":"); a != "a" || b != "b" {15 t. Fail()10 func TestRenameExt(t *testing.T) { 11 if s := renameExt("foo.amber", ".amber", ".html"); s != "foo.html" { 12 t.Error(s) 16 13 } 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) 19 16 } 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) 22 19 } 23 if a, b := split2(":", ":"); a != "" || b != "" {24 t. Fail()20 if s := renameExt("foo", ".amber", ".html"); s != "foo" { 21 t.Error(s) 25 22 } 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) 31 25 } 32 26 } 33 27 34 func tmpfile(path, s string) string { 35 ioutil.WriteFile(path, []byte(s), 0644) 36 return path 28 func 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 41 echo 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) 37 49 } 38 50 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: [:|||:] 51 func TestVars(t *testing.T) { 52 tests := map[string]Vars{ 53 ` 54 foo: bar 55 title: Hello, world! 46 56 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) 57 Some 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 68 Hello 69 `: Vars{ 70 "url": "example.com/foo.html", 71 "__content": "Hello\n", 72 }, 62 73 } 63 74 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 } 74 88 } 75 89 } … … 77 91 func TestRender(t *testing.T) { 78 92 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) 87 96 } 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) 91 99 } 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) 97 102 } 98 103 // 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") 101 106 } 102 107 } 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 := false109 for _, v := range e {110 if strings.HasPrefix(v, s) {111 found = true112 break113 }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 return142 }143 defer os.Exit(0) // TODO check exit code144 log.Println(os.Getenv("ZS_ERR")) // stderr145 fmt.Println(os.Getenv("ZS_OUT")) // stdout146 }
Note:
See TracChangeset
for help on using the changeset viewer.