source: code/trunk/zs.go@ 34

Last change on this file since 34 was 34, checked in by zaitsev.serge, 10 years ago

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

File size: 9.2 KB
Line 
1package main
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "io/ioutil"
8 "log"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "strings"
13 "text/template"
14 "time"
15
16 "github.com/eknkc/amber"
17 "github.com/russross/blackfriday"
18 "github.com/yosssi/gcss"
19 "gopkg.in/yaml.v1"
20)
21
22const (
23 ZSDIR = ".zs"
24 PUBDIR = ".pub"
25)
26
27type Vars map[string]string
28
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
38 } else {
39 return path
40 }
41}
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
45func globals() Vars {
46 vars := Vars{}
47 for _, e := range os.Environ() {
48 pair := strings.Split(e, "=")
49 if strings.HasPrefix(pair[0], "ZS_") {
50 vars[strings.ToLower(pair[0][3:])] = pair[1]
51 }
52 }
53 return vars
54}
55
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...)
71 env := []string{"ZS=" + os.Args[0], "ZS_OUTDIR=" + PUBDIR}
72 env = append(env, os.Environ()...)
73 for k, v := range vars {
74 env = append(env, "ZS_"+strings.ToUpper(k)+"="+v)
75 }
76 c.Env = env
77 c.Stdout = &outbuf
78 c.Stderr = &errbuf
79
80 err := c.Run()
81
82 if errbuf.Len() > 0 {
83 log.Println("ERROR:", errbuf.String())
84 }
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) {
96 b, err := ioutil.ReadFile(path)
97 if err != nil {
98 return nil, "", err
99 }
100 s := string(b)
101
102 // Copy globals first
103 v := Vars{}
104 for name, value := range globals {
105 v[name] = value
106 }
107
108 // Override them by default values extracted from file name/path
109 if _, err := os.Stat(filepath.Join(ZSDIR, "layout.amber")); err == nil {
110 v["layout"] = "layout.amber"
111 } else {
112 v["layout"] = "layout.html"
113 }
114 v["file"] = path
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 {
119 return v, s, nil
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
139func render(s string, vars Vars) (string, error) {
140 delim_open := "{{"
141 delim_close := "}}"
142
143 out := &bytes.Buffer{}
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
171}
172
173// Renders markdown with the given layout into html expanding all the macros
174func buildMarkdown(path string, w io.Writer, vars Vars) error {
175 v, body, err := getVars(path, vars)
176 if err != nil {
177 return err
178 }
179 content, err := render(body, v)
180 if err != nil {
181 return err
182 }
183 v["content"] = string(blackfriday.MarkdownCommon([]byte(content)))
184 if w == nil {
185 out, err := os.Create(filepath.Join(PUBDIR, renameExt(path, "", ".html")))
186 if err != nil {
187 return err
188 }
189 defer out.Close()
190 w = out
191 }
192 if strings.HasSuffix(v["layout"], ".amber") {
193 return buildAmber(filepath.Join(ZSDIR, v["layout"]), w, v)
194 } else {
195 return buildHTML(filepath.Join(ZSDIR, v["layout"]), w, v)
196 }
197}
198
199// Renders text file expanding all variable macros inside it
200func buildHTML(path string, w io.Writer, vars Vars) error {
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)
209 if err != nil {
210 return err
211 }
212 if w == nil {
213 f, err := os.Create(filepath.Join(PUBDIR, path))
214 if err != nil {
215 return err
216 }
217 defer f.Close()
218 w = f
219 }
220 return tmpl.Execute(w, vars)
221}
222
223// Renders .amber file into .html
224func 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
233 a := amber.New()
234 if err := a.Parse(body); err != nil {
235 return err
236 }
237
238 t, err := a.Compile()
239 if err != nil {
240 return err
241 }
242 if w == nil {
243 f, err := os.Create(filepath.Join(PUBDIR, renameExt(path, ".amber", ".html")))
244 if err != nil {
245 return err
246 }
247 defer f.Close()
248 w = f
249 }
250 return t.Execute(w, vars)
251}
252
253// Compiles .gcss into .css
254func buildGCSS(path string, w io.Writer) error {
255 f, err := os.Open(path)
256 if err != nil {
257 return err
258 }
259 defer f.Close()
260
261 if w == nil {
262 s := strings.TrimSuffix(path, ".gcss") + ".css"
263 css, err := os.Create(filepath.Join(PUBDIR, s))
264 if err != nil {
265 return err
266 }
267 defer css.Close()
268 w = css
269 }
270 _, err = gcss.Compile(w, f)
271 return err
272}
273
274// Copies file as is from path to writer
275func buildRaw(path string, w io.Writer) error {
276 in, err := os.Open(path)
277 if err != nil {
278 return err
279 }
280 defer in.Close()
281 if w == nil {
282 if out, err := os.Create(filepath.Join(PUBDIR, path)); err != nil {
283 return err
284 } else {
285 defer out.Close()
286 w = out
287 }
288 }
289 _, err = io.Copy(w, in)
290 return err
291}
292
293func build(path string, w io.Writer, vars Vars) error {
294 ext := filepath.Ext(path)
295 if ext == ".md" || ext == ".mkd" {
296 return buildMarkdown(path, w, vars)
297 } else if ext == ".html" || ext == ".xml" {
298 return buildHTML(path, w, vars)
299 } else if ext == ".amber" {
300 return buildAmber(path, w, vars)
301 } else if ext == ".gcss" {
302 return buildGCSS(path, w)
303 } else {
304 return buildRaw(path, w)
305 }
306}
307
308func buildAll(watch bool) {
309 lastModified := time.Unix(0, 0)
310 modified := false
311
312 vars := globals()
313 for {
314 os.Mkdir(PUBDIR, 0755)
315 err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
316 // ignore hidden files and directories
317 if filepath.Base(path)[0] == '.' || strings.HasPrefix(path, ".") {
318 return nil
319 }
320 // inform user about fs walk errors, but continue iteration
321 if err != nil {
322 log.Println("ERROR:", err)
323 return nil
324 }
325
326 if info.IsDir() {
327 os.Mkdir(filepath.Join(PUBDIR, path), 0755)
328 return nil
329 } else if info.ModTime().After(lastModified) {
330 if !modified {
331 // First file in this build cycle is about to be modified
332 run(vars, "prehook")
333 modified = true
334 }
335 log.Println("build:", path)
336 return build(path, nil, vars)
337 }
338 return nil
339 })
340 if err != nil {
341 log.Println("ERROR:", err)
342 }
343 if modified {
344 // At least one file in this build cycle has been modified
345 run(vars, "posthook")
346 modified = false
347 }
348 if !watch {
349 break
350 }
351 lastModified = time.Now()
352 time.Sleep(1 * time.Second)
353 }
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)
361}
362
363func main() {
364 if len(os.Args) == 1 {
365 fmt.Println(os.Args[0], "<command> [args]")
366 return
367 }
368 cmd := os.Args[1]
369 args := os.Args[2:]
370 switch cmd {
371 case "build":
372 if len(args) == 0 {
373 buildAll(false)
374 } else if len(args) == 1 {
375 if err := build(args[0], os.Stdout, globals()); err != nil {
376 fmt.Println("ERROR: " + err.Error())
377 }
378 } else {
379 fmt.Println("ERROR: too many arguments")
380 }
381 case "watch":
382 buildAll(true)
383 case "var":
384 if len(args) == 0 {
385 fmt.Println("var: filename expected")
386 } else {
387 s := ""
388 if vars, _, err := getVars(args[0], globals()); err != nil {
389 fmt.Println("var: " + err.Error())
390 } else {
391 if len(args) > 1 {
392 for _, a := range args[1:] {
393 s = s + vars[a] + "\n"
394 }
395 } else {
396 for k, v := range vars {
397 s = s + k + ":" + v + "\n"
398 }
399 }
400 }
401 fmt.Println(strings.TrimSpace(s))
402 }
403 default:
404 if s, err := run(globals(), cmd, args...); err != nil {
405 fmt.Println(err)
406 } else {
407 fmt.Println(s)
408 }
409 }
410}
Note: See TracBrowser for help on using the repository browser.