comparison zs.go @ 33:e3c902a7380d draft

rewritten using zs templates, allowing go templates using <% %> delimiters
author zaitsev.serge
date Wed, 02 Sep 2015 17:05:09 +0000
parents 75822e38c3e0
children ed40ca93db1e
comparison
equal deleted inserted replaced
32:75822e38c3e0 33:e3c902a7380d
6 "io" 6 "io"
7 "io/ioutil" 7 "io/ioutil"
8 "log" 8 "log"
9 "os" 9 "os"
10 "os/exec" 10 "os/exec"
11 "path"
12 "path/filepath" 11 "path/filepath"
13 "strings" 12 "strings"
14 "text/template" 13 "text/template"
15 "time" 14 "time"
16 15
17 "github.com/eknkc/amber" 16 "github.com/eknkc/amber"
18 "github.com/russross/blackfriday" 17 "github.com/russross/blackfriday"
19 "github.com/yosssi/gcss" 18 "github.com/yosssi/gcss"
19 "gopkg.in/yaml.v1"
20 ) 20 )
21 21
22 const ( 22 const (
23 ZSDIR = ".zs" 23 ZSDIR = ".zs"
24 PUBDIR = ".pub" 24 PUBDIR = ".pub"
25 ) 25 )
26 26
27 type Vars map[string]string 27 type Vars map[string]string
28 28
29 func renameExt(path, from, to string) string { 29 // renameExt renames extension (if any) from oldext to newext
30 if from == "" { 30 // If oldext is an empty string - extension is extracted automatically.
31 from = filepath.Ext(path) 31 // If path has no extension - new extension is appended
32 } 32 func renameExt(path, oldext, newext string) string {
33 if strings.HasSuffix(path, from) { 33 if oldext == "" {
34 return strings.TrimSuffix(path, from) + to 34 oldext = filepath.Ext(path)
35 }
36 if oldext == "" || strings.HasSuffix(path, oldext) {
37 return strings.TrimSuffix(path, oldext) + newext
35 } else { 38 } else {
36 return path 39 return path
37 } 40 }
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 func globals() Vars { 45 func globals() Vars {
41 vars := Vars{} 46 vars := Vars{}
42 for _, e := range os.Environ() { 47 for _, e := range os.Environ() {
43 pair := strings.Split(e, "=") 48 pair := strings.Split(e, "=")
44 if strings.HasPrefix(pair[0], "ZS_") { 49 if strings.HasPrefix(pair[0], "ZS_") {
46 } 51 }
47 } 52 }
48 return vars 53 return vars
49 } 54 }
50 55
51 // Converts zs markdown variables into environment variables 56 // run executes a command or a script. Vars define the command environment,
52 func env(vars Vars) []string { 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 env := []string{"ZS=" + os.Args[0], "ZS_OUTDIR=" + PUBDIR} 71 env := []string{"ZS=" + os.Args[0], "ZS_OUTDIR=" + PUBDIR}
54 env = append(env, os.Environ()...) 72 env = append(env, os.Environ()...)
55 if vars != nil { 73 for k, v := range vars {
56 for k, v := range vars { 74 env = append(env, "ZS_"+strings.ToUpper(k)+"="+v)
57 env = append(env, "ZS_"+strings.ToUpper(k)+"="+v) 75 }
58 } 76 c.Env = env
59 } 77 c.Stdout = &outbuf
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
70 c.Stderr = &errbuf 78 c.Stderr = &errbuf
71 79
72 err := c.Run() 80 err := c.Run()
73 81
74 if errbuf.Len() > 0 { 82 if errbuf.Len() > 0 {
75 log.Println("ERROR:", errbuf.String()) 83 log.Println("ERROR:", errbuf.String())
76 } 84 }
77 85 if err != nil {
78 if err != nil { 86 return "", err
79 return err 87 }
80 } 88 return string(outbuf.Bytes()), nil
81 return nil 89 }
82 } 90
83 91 // getVars returns list of variables defined in a text file and actual file
84 // Splits a string in exactly two parts by delimiter 92 // content following the variables declaration. Header is separated from
85 // If no delimiter is found - the second string is be empty 93 // content by an empty line. Header can be either YAML or JSON.
86 func split2(s, delim string) (string, string) { 94 // If no empty newline is found - file is treated as content-only.
87 parts := strings.SplitN(s, delim, 2) 95 func getVars(path string, globals Vars) (Vars, string, error) {
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) {
97 b, err := ioutil.ReadFile(path) 96 b, err := ioutil.ReadFile(path)
98 if err != nil { 97 if err != nil {
99 return nil, "", err 98 return nil, "", err
100 } 99 }
101 s := string(b) 100 s := string(b)
102 url := path[:len(path)-len(filepath.Ext(path))] + ".html" 101
103 v := Vars{ 102 // Copy globals first
104 "title": "", 103 v := Vars{}
105 "description": "",
106 "keywords": "",
107 }
108 for name, value := range globals { 104 for name, value := range globals {
109 v[name] = value 105 v[name] = value
110 } 106 }
107
108 // Override them by default values extracted from file name/path
111 if _, err := os.Stat(filepath.Join(ZSDIR, "layout.amber")); err == nil { 109 if _, err := os.Stat(filepath.Join(ZSDIR, "layout.amber")); err == nil {
112 v["layout"] = "layout.amber" 110 v["layout"] = "layout.amber"
113 } else { 111 } else {
114 v["layout"] = "layout.html" 112 v["layout"] = "layout.html"
115 } 113 }
116 v["file"] = path 114 v["file"] = path
117 v["url"] = url 115 v["url"] = path[:len(path)-len(filepath.Ext(path))] + ".html"
118 v["output"] = filepath.Join(PUBDIR, url) 116 v["output"] = filepath.Join(PUBDIR, v["url"])
119 117
120 if strings.Index(s, "\n\n") == -1 { 118 if sep := strings.Index(s, "\n\n"); sep == -1 {
121 return v, s, nil 119 return v, s, nil
122 } 120 } else {
123 header, body := split2(s, "\n\n") 121 header := s[:sep]
124 for _, line := range strings.Split(header, "\n") { 122 body := s[sep+len("\n\n"):]
125 key, value := split2(line, ":") 123 vars := Vars{}
126 v[strings.ToLower(strings.TrimSpace(key))] = strings.TrimSpace(value) 124 if err := yaml.Unmarshal([]byte(header), &vars); err != nil {
127 } 125 fmt.Println("ERROR: failed to parse header", err)
128 if strings.HasPrefix(v["url"], "./") { 126 } else {
129 v["url"] = v["url"][2:] 127 for key, value := range vars {
130 } 128 v[key] = value
131 return v, body, nil 129 }
132 } 130 }
133 131 if strings.HasPrefix(v["url"], "./") {
134 // Use standard Go templates 132 v["url"] = v["url"][2:]
133 }
134 return v, body, nil
135 }
136 }
137
138 // Render expanding zs plugins and variables
135 func render(s string, vars Vars) (string, error) { 139 func render(s string, vars Vars) (string, error) {
136 tmpl, err := template.New("").Parse(s) 140 delim_open := "{{"
137 if err != nil { 141 delim_close := "}}"
138 return "", err 142
139 }
140 out := &bytes.Buffer{} 143 out := &bytes.Buffer{}
141 if err := tmpl.Execute(out, vars); err != nil { 144 for {
142 return "", err 145 if from := strings.Index(s, delim_open); from == -1 {
143 } 146 out.WriteString(s)
144 return string(out.Bytes()), nil 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 // Renders markdown with the given layout into html expanding all the macros 173 // Renders markdown with the given layout into html expanding all the macros
148 func buildMarkdown(path string, w io.Writer, vars Vars) error { 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 if err != nil { 176 if err != nil {
151 return err 177 return err
152 } 178 }
153 content, err := render(body, v) 179 content, err := render(body, v)
154 if err != nil { 180 if err != nil {
170 } 196 }
171 } 197 }
172 198
173 // Renders text file expanding all variable macros inside it 199 // Renders text file expanding all variable macros inside it
174 func buildHTML(path string, w io.Writer, vars Vars) error { 200 func buildHTML(path string, w io.Writer, vars Vars) error {
175 b, err := ioutil.ReadFile(path) 201 v, body, err := getVars(path, vars)
176 if err != nil { 202 if err != nil {
177 return err 203 return err
178 } 204 }
179 content, err := render(string(b), vars) 205 if body, err = render(body, v); err != nil {
206 return err
207 }
208 tmpl, err := template.New("").Delims("<%", "%>").Parse(body)
180 if err != nil { 209 if err != nil {
181 return err 210 return err
182 } 211 }
183 if w == nil { 212 if w == nil {
184 f, err := os.Create(filepath.Join(PUBDIR, path)) 213 f, err := os.Create(filepath.Join(PUBDIR, path))
186 return err 215 return err
187 } 216 }
188 defer f.Close() 217 defer f.Close()
189 w = f 218 w = f
190 } 219 }
191 _, err = io.WriteString(w, content) 220 return tmpl.Execute(w, vars)
192 return err
193 } 221 }
194 222
195 // Renders .amber file into .html 223 // Renders .amber file into .html
196 func buildAmber(path string, w io.Writer, vars Vars) error { 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 a := amber.New() 233 a := amber.New()
198 err := a.ParseFile(path) 234 if err := a.Parse(body); err != nil {
199 if err != nil { 235 return err
200 return err
201 }
202
203 data := map[string]interface{}{}
204 for k, v := range vars {
205 data[k] = v
206 } 236 }
207 237
208 t, err := a.Compile() 238 t, err := a.Compile()
209 if err != nil { 239 if err != nil {
210 return err 240 return err
215 return err 245 return err
216 } 246 }
217 defer f.Close() 247 defer f.Close()
218 w = f 248 w = f
219 } 249 }
220 return t.Execute(w, data) 250 return t.Execute(w, vars)
221 } 251 }
222 252
223 // Compiles .gcss into .css 253 // Compiles .gcss into .css
224 func buildGCSS(path string, w io.Writer) error { 254 func buildGCSS(path string, w io.Writer) error {
225 f, err := os.Open(path) 255 f, err := os.Open(path)
296 if info.IsDir() { 326 if info.IsDir() {
297 os.Mkdir(filepath.Join(PUBDIR, path), 0755) 327 os.Mkdir(filepath.Join(PUBDIR, path), 0755)
298 return nil 328 return nil
299 } else if info.ModTime().After(lastModified) { 329 } else if info.ModTime().After(lastModified) {
300 if !modified { 330 if !modified {
301 // About to be modified, so run pre-build hook 331 // First file in this build cycle is about to be modified
302 // FIXME on windows it might not work well 332 run(vars, "prehook")
303 run(filepath.Join(ZSDIR, "pre"), []string{}, nil, nil)
304 modified = true 333 modified = true
305 } 334 }
306 log.Println("build: ", path) 335 log.Println("build:", path)
307 return build(path, nil, vars) 336 return build(path, nil, vars)
308 } 337 }
309 return nil 338 return nil
310 }) 339 })
311 if err != nil { 340 if err != nil {
312 log.Println("ERROR:", err) 341 log.Println("ERROR:", err)
313 } 342 }
314 if modified { 343 if modified {
315 // Something was modified, so post-build hook 344 // At least one file in this build cycle has been modified
316 // FIXME on windows it might not work well 345 run(vars, "posthook")
317 run(filepath.Join(ZSDIR, "post"), []string{}, nil, nil)
318 modified = false 346 modified = false
319 } 347 }
320 if !watch { 348 if !watch {
321 break 349 break
322 } 350 }
323 lastModified = time.Now() 351 lastModified = time.Now()
324 time.Sleep(1 * time.Second) 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
328 func main() { 363 func main() {
329 if len(os.Args) == 1 { 364 if len(os.Args) == 1 {
330 fmt.Println(os.Args[0], "<command> [args]") 365 fmt.Println(os.Args[0], "<command> [args]")
348 case "var": 383 case "var":
349 if len(args) == 0 { 384 if len(args) == 0 {
350 fmt.Println("var: filename expected") 385 fmt.Println("var: filename expected")
351 } else { 386 } else {
352 s := "" 387 s := ""
353 if vars, _, err := md(args[0], globals()); err != nil { 388 if vars, _, err := getVars(args[0], globals()); err != nil {
354 fmt.Println("var: " + err.Error()) 389 fmt.Println("var: " + err.Error())
355 } else { 390 } else {
356 if len(args) > 1 { 391 if len(args) > 1 {
357 for _, a := range args[1:] { 392 for _, a := range args[1:] {
358 s = s + vars[a] + "\n" 393 s = s + vars[a] + "\n"
364 } 399 }
365 } 400 }
366 fmt.Println(strings.TrimSpace(s)) 401 fmt.Println(strings.TrimSpace(s))
367 } 402 }
368 default: 403 default:
369 err := run(path.Join(ZSDIR, cmd), args, globals(), os.Stdout) 404 if s, err := run(globals(), cmd, args...); err != nil {
370 if err != nil { 405 fmt.Println(err)
371 log.Println("ERROR:", err) 406 } else {
372 } 407 fmt.Println(s)
373 } 408 }
374 } 409 }
410 }