source: code/trunk/zs.go@ 40

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

more obvious rules of variable overriding

File size: 9.7 KB
RevLine 
[1]1package main
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "io/ioutil"
8 "log"
9 "os"
[33]10 "os/exec"
[1]11 "path/filepath"
12 "strings"
[19]13 "text/template"
[1]14 "time"
15
[23]16 "github.com/eknkc/amber"
[1]17 "github.com/russross/blackfriday"
[18]18 "github.com/yosssi/gcss"
[35]19 "gopkg.in/yaml.v2"
[1]20)
21
22const (
23 ZSDIR = ".zs"
24 PUBDIR = ".pub"
25)
26
[18]27type Vars map[string]string
[4]28
[34]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)
[33]35 }
[34]36 if oldext == "" || strings.HasSuffix(path, oldext) {
37 return strings.TrimSuffix(path, oldext) + newext
[33]38 } else {
39 return path
40 }
41}
42
[34]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
[33]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
[34]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...)
[33]71 env := []string{"ZS=" + os.Args[0], "ZS_OUTDIR=" + PUBDIR}
72 env = append(env, os.Environ()...)
[34]73 for k, v := range vars {
74 env = append(env, "ZS_"+strings.ToUpper(k)+"="+v)
[33]75 }
[34]76 c.Env = env
77 c.Stdout = &outbuf
[33]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 {
[34]86 return "", err
[33]87 }
[34]88 return string(outbuf.Bytes()), nil
[33]89}
90
[34]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) {
[18]96 b, err := ioutil.ReadFile(path)
97 if err != nil {
98 return nil, "", err
99 }
100 s := string(b)
[34]101
[37]102 // Pick some default values for content-dependent variables
[34]103 v := Vars{}
[37]104 title := strings.Replace(strings.Replace(path, "_", " ", -1), "-", " ", -1)
105 v["title"] = strings.ToTitle(title)
106 v["description"] = ""
[40]107 v["file"] = path
108 v["url"] = path[:len(path)-len(filepath.Ext(path))] + ".html"
109 v["output"] = filepath.Join(PUBDIR, v["url"])
[37]110
[40]111 // Override default values with globals
[30]112 for name, value := range globals {
113 v[name] = value
114 }
[34]115
[40]116 // Add layout if none is specified
117 if _, ok := v["layout"]; !ok {
118 if _, err := os.Stat(filepath.Join(ZSDIR, "layout.amber")); err == nil {
119 v["layout"] = "layout.amber"
120 } else {
121 v["layout"] = "layout.html"
122 }
[21]123 }
124
[35]125 delim := "\n---\n"
126 if sep := strings.Index(s, delim); sep == -1 {
[20]127 return v, s, nil
[34]128 } else {
129 header := s[:sep]
[35]130 body := s[sep+len(delim):]
131
[34]132 vars := Vars{}
133 if err := yaml.Unmarshal([]byte(header), &vars); err != nil {
134 fmt.Println("ERROR: failed to parse header", err)
[35]135 return nil, "", err
[34]136 } else {
[40]137 // Override default values + globals with the ones defines in the file
[34]138 for key, value := range vars {
139 v[key] = value
140 }
141 }
142 if strings.HasPrefix(v["url"], "./") {
143 v["url"] = v["url"][2:]
144 }
145 return v, body, nil
[3]146 }
[1]147}
148
[34]149// Render expanding zs plugins and variables
[33]150func render(s string, vars Vars) (string, error) {
[34]151 delim_open := "{{"
152 delim_close := "}}"
153
[19]154 out := &bytes.Buffer{}
[34]155 for {
156 if from := strings.Index(s, delim_open); from == -1 {
157 out.WriteString(s)
158 return out.String(), nil
159 } else {
160 if to := strings.Index(s, delim_close); to == -1 {
161 return "", fmt.Errorf("Close delim not found")
162 } else {
163 out.WriteString(s[:from])
164 cmd := s[from+len(delim_open) : to]
165 s = s[to+len(delim_close):]
166 m := strings.Fields(cmd)
167 if len(m) == 1 {
168 if v, ok := vars[m[0]]; ok {
169 out.WriteString(v)
170 continue
171 }
172 }
173 if res, err := run(vars, m[0], m[1:]...); err == nil {
174 out.WriteString(res)
175 } else {
176 fmt.Println(err)
177 }
178 }
179 }
[19]180 }
[34]181 return s, nil
[1]182}
183
[19]184// Renders markdown with the given layout into html expanding all the macros
[33]185func buildMarkdown(path string, w io.Writer, vars Vars) error {
[34]186 v, body, err := getVars(path, vars)
[1]187 if err != nil {
188 return err
189 }
[33]190 content, err := render(body, v)
[1]191 if err != nil {
192 return err
193 }
[32]194 v["content"] = string(blackfriday.MarkdownCommon([]byte(content)))
[24]195 if w == nil {
196 out, err := os.Create(filepath.Join(PUBDIR, renameExt(path, "", ".html")))
197 if err != nil {
198 return err
199 }
200 defer out.Close()
201 w = out
202 }
[19]203 if strings.HasSuffix(v["layout"], ".amber") {
[33]204 return buildAmber(filepath.Join(ZSDIR, v["layout"]), w, v)
[19]205 } else {
[33]206 return buildHTML(filepath.Join(ZSDIR, v["layout"]), w, v)
[19]207 }
[15]208}
209
[19]210// Renders text file expanding all variable macros inside it
[33]211func buildHTML(path string, w io.Writer, vars Vars) error {
[34]212 v, body, err := getVars(path, vars)
[1]213 if err != nil {
214 return err
215 }
[34]216 if body, err = render(body, v); err != nil {
217 return err
218 }
219 tmpl, err := template.New("").Delims("<%", "%>").Parse(body)
[1]220 if err != nil {
221 return err
222 }
[25]223 if w == nil {
224 f, err := os.Create(filepath.Join(PUBDIR, path))
225 if err != nil {
226 return err
227 }
228 defer f.Close()
229 w = f
[15]230 }
[34]231 return tmpl.Execute(w, vars)
[1]232}
233
[19]234// Renders .amber file into .html
[33]235func buildAmber(path string, w io.Writer, vars Vars) error {
[34]236 v, body, err := getVars(path, vars)
[18]237 if err != nil {
238 return err
239 }
[34]240 a := amber.New()
241 if err := a.Parse(body); err != nil {
[35]242 fmt.Println(body)
[34]243 return err
[25]244 }
245
[18]246 t, err := a.Compile()
247 if err != nil {
248 return err
249 }
[35]250
251 htmlBuf := &bytes.Buffer{}
252 if err := t.Execute(htmlBuf, v); err != nil {
253 return err
254 }
255
256 if body, err = render(string(htmlBuf.Bytes()), v); err != nil {
257 return err
258 }
259
[24]260 if w == nil {
261 f, err := os.Create(filepath.Join(PUBDIR, renameExt(path, ".amber", ".html")))
262 if err != nil {
263 return err
264 }
265 defer f.Close()
266 w = f
[18]267 }
[35]268 _, err = io.WriteString(w, body)
269 return err
[18]270}
271
[19]272// Compiles .gcss into .css
[24]273func buildGCSS(path string, w io.Writer) error {
[19]274 f, err := os.Open(path)
275 if err != nil {
276 return err
277 }
278 defer f.Close()
279
[24]280 if w == nil {
281 s := strings.TrimSuffix(path, ".gcss") + ".css"
282 css, err := os.Create(filepath.Join(PUBDIR, s))
283 if err != nil {
284 return err
[1]285 }
[24]286 defer css.Close()
287 w = css
[1]288 }
[24]289 _, err = gcss.Compile(w, f)
[8]290 return err
[1]291}
292
[24]293// Copies file as is from path to writer
294func buildRaw(path string, w io.Writer) error {
295 in, err := os.Open(path)
296 if err != nil {
297 return err
[19]298 }
[24]299 defer in.Close()
300 if w == nil {
301 if out, err := os.Create(filepath.Join(PUBDIR, path)); err != nil {
302 return err
303 } else {
304 defer out.Close()
305 w = out
[19]306 }
307 }
[24]308 _, err = io.Copy(w, in)
309 return err
[19]310}
311
[33]312func build(path string, w io.Writer, vars Vars) error {
[24]313 ext := filepath.Ext(path)
314 if ext == ".md" || ext == ".mkd" {
[33]315 return buildMarkdown(path, w, vars)
[24]316 } else if ext == ".html" || ext == ".xml" {
[33]317 return buildHTML(path, w, vars)
[24]318 } else if ext == ".amber" {
[33]319 return buildAmber(path, w, vars)
[24]320 } else if ext == ".gcss" {
321 return buildGCSS(path, w)
322 } else {
323 return buildRaw(path, w)
[21]324 }
325}
326
[24]327func buildAll(watch bool) {
[20]328 lastModified := time.Unix(0, 0)
329 modified := false
330
331 vars := globals()
[1]332 for {
333 os.Mkdir(PUBDIR, 0755)
[36]334 filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
[1]335 // ignore hidden files and directories
336 if filepath.Base(path)[0] == '.' || strings.HasPrefix(path, ".") {
337 return nil
338 }
[31]339 // inform user about fs walk errors, but continue iteration
340 if err != nil {
[36]341 fmt.Println("error:", err)
[31]342 return nil
343 }
[1]344
[8]345 if info.IsDir() {
346 os.Mkdir(filepath.Join(PUBDIR, path), 0755)
347 return nil
348 } else if info.ModTime().After(lastModified) {
349 if !modified {
[34]350 // First file in this build cycle is about to be modified
351 run(vars, "prehook")
[8]352 modified = true
353 }
[34]354 log.Println("build:", path)
[33]355 return build(path, nil, vars)
[1]356 }
357 return nil
358 })
[8]359 if modified {
[34]360 // At least one file in this build cycle has been modified
361 run(vars, "posthook")
[8]362 modified = false
363 }
[24]364 if !watch {
[1]365 break
366 }
[24]367 lastModified = time.Now()
[1]368 time.Sleep(1 * time.Second)
369 }
370}
371
[34]372func init() {
373 // prepend .zs to $PATH, so plugins will be found before OS commands
374 p := os.Getenv("PATH")
375 p = ZSDIR + ":" + p
376 os.Setenv("PATH", p)
377}
378
[1]379func main() {
380 if len(os.Args) == 1 {
381 fmt.Println(os.Args[0], "<command> [args]")
382 return
383 }
384 cmd := os.Args[1]
385 args := os.Args[2:]
386 switch cmd {
387 case "build":
[25]388 if len(args) == 0 {
389 buildAll(false)
390 } else if len(args) == 1 {
[33]391 if err := build(args[0], os.Stdout, globals()); err != nil {
[25]392 fmt.Println("ERROR: " + err.Error())
393 }
394 } else {
395 fmt.Println("ERROR: too many arguments")
396 }
[24]397 case "watch":
[1]398 buildAll(true)
[24]399 case "var":
[33]400 if len(args) == 0 {
401 fmt.Println("var: filename expected")
402 } else {
403 s := ""
[40]404 if vars, _, err := getVars(args[0], Vars{}); err != nil {
[33]405 fmt.Println("var: " + err.Error())
406 } else {
407 if len(args) > 1 {
408 for _, a := range args[1:] {
409 s = s + vars[a] + "\n"
410 }
411 } else {
412 for k, v := range vars {
413 s = s + k + ":" + v + "\n"
414 }
415 }
416 }
417 fmt.Println(strings.TrimSpace(s))
418 }
[1]419 default:
[34]420 if s, err := run(globals(), cmd, args...); err != nil {
421 fmt.Println(err)
422 } else {
423 fmt.Println(s)
[1]424 }
425 }
426}
Note: See TracBrowser for help on using the repository browser.