Mercurial > yakumo_izuru > aya
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 } |