source: code/trunk/zs.go@ 21

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

replaced amber with my own fork, fixed file paths for amber and html

File size: 8.8 KB
Line 
1package main
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "io/ioutil"
8 "log"
9 "os"
10 "os/exec"
11 "path"
12 "path/filepath"
13 "strings"
14 "text/template"
15 "time"
16
17 "github.com/russross/blackfriday"
18 "github.com/yosssi/gcss"
19 "github.com/zserge/amber"
20)
21
22const (
23 ZSDIR = ".zs"
24 PUBDIR = ".pub"
25)
26
27type Vars map[string]string
28
29// Splits a string in exactly two parts by delimiter
30// If no delimiter is found - the second string is be empty
31func split2(s, delim string) (string, string) {
32 parts := strings.SplitN(s, delim, 2)
33 if len(parts) == 2 {
34 return parts[0], parts[1]
35 } else {
36 return parts[0], ""
37 }
38}
39
40// Parses markdown content. Returns parsed header variables and content
41func md(path string, globals Vars) (Vars, string, error) {
42 b, err := ioutil.ReadFile(path)
43 if err != nil {
44 return nil, "", err
45 }
46 s := string(b)
47 url := path[:len(path)-len(filepath.Ext(path))] + ".html"
48 v := Vars{
49 "file": path,
50 "url": url,
51 "output": filepath.Join(PUBDIR, url),
52 }
53 if _, err := os.Stat(filepath.Join(ZSDIR, "layout.amber")); err == nil {
54 v["layout"] = "layout.amber"
55 } else {
56 v["layout"] = "layout.html"
57 }
58
59 if info, err := os.Stat(path); err == nil {
60 v["date"] = info.ModTime().Format("02-01-2006")
61 }
62 for name, value := range globals {
63 v[name] = value
64 }
65 if strings.Index(s, "\n\n") == -1 {
66 return v, s, nil
67 }
68 header, body := split2(s, "\n\n")
69 for _, line := range strings.Split(header, "\n") {
70 key, value := split2(line, ":")
71 v[strings.ToLower(strings.TrimSpace(key))] = strings.TrimSpace(value)
72 }
73 if strings.HasPrefix(v["url"], "./") {
74 v["url"] = v["url"][2:]
75 }
76 return v, body, nil
77}
78
79// Use standard Go templates
80func render(s string, funcs template.FuncMap, vars Vars) (string, error) {
81 f := template.FuncMap{}
82 for k, v := range funcs {
83 f[k] = v
84 }
85 for k, v := range vars {
86 f[k] = varFunc(v)
87 }
88 tmpl, err := template.New("").Funcs(f).Parse(s)
89 if err != nil {
90 return "", err
91 }
92 out := &bytes.Buffer{}
93 if err := tmpl.Execute(out, vars); err != nil {
94 return "", err
95 }
96 return string(out.Bytes()), nil
97}
98
99// Converts zs markdown variables into environment variables
100func env(vars Vars) []string {
101 env := []string{"ZS=" + os.Args[0], "ZS_OUTDIR=" + PUBDIR}
102 env = append(env, os.Environ()...)
103 if vars != nil {
104 for k, v := range vars {
105 env = append(env, "ZS_"+strings.ToUpper(k)+"="+v)
106 }
107 }
108 return env
109}
110
111// Runs command with given arguments and variables, intercepts stderr and
112// redirects stdout into the given writer
113func run(cmd string, args []string, vars Vars, output io.Writer) error {
114 var errbuf bytes.Buffer
115 c := exec.Command(cmd, args...)
116 c.Env = env(vars)
117 c.Stdout = output
118 c.Stderr = &errbuf
119
120 err := c.Run()
121
122 if errbuf.Len() > 0 {
123 log.Println("ERROR:", errbuf.String())
124 }
125
126 if err != nil {
127 return err
128 }
129 return nil
130}
131
132// Expands macro: either replacing it with the variable value, or
133// running the plugin command and replacing it with the command's output
134func eval(cmd []string, vars Vars) (string, error) {
135 outbuf := bytes.NewBuffer(nil)
136 err := run(path.Join(ZSDIR, cmd[0]), cmd[1:], vars, outbuf)
137 if err != nil {
138 if _, ok := err.(*exec.ExitError); ok {
139 return "", err
140 }
141 outbuf = bytes.NewBuffer(nil)
142 err := run(cmd[0], cmd[1:], vars, outbuf)
143 // Return exit errors, but ignore if the command was not found
144 if _, ok := err.(*exec.ExitError); ok {
145 return "", err
146 }
147 }
148 return outbuf.String(), nil
149}
150
151// Renders markdown with the given layout into html expanding all the macros
152func buildMarkdown(path string, funcs template.FuncMap, vars Vars) error {
153 v, body, err := md(path, vars)
154 if err != nil {
155 return err
156 }
157 content, err := render(body, funcs, v)
158 if err != nil {
159 return err
160 }
161 v["content"] = string(blackfriday.MarkdownBasic([]byte(content)))
162 if strings.HasSuffix(v["layout"], ".amber") {
163 return buildAmber(filepath.Join(ZSDIR, v["layout"]),
164 renameExt(path, "", ".html"), funcs, v)
165 } else {
166 return buildPlain(filepath.Join(ZSDIR, v["layout"]),
167 renameExt(path, "", ".html"), funcs, v)
168 }
169}
170
171// Renders text file expanding all variable macros inside it
172func buildPlain(in, out string, funcs template.FuncMap, vars Vars) error {
173 b, err := ioutil.ReadFile(in)
174 if err != nil {
175 return err
176 }
177 content, err := render(string(b), funcs, vars)
178 if err != nil {
179 return err
180 }
181 output := filepath.Join(PUBDIR, out)
182 if s, ok := vars["output"]; ok {
183 output = s
184 }
185 err = ioutil.WriteFile(output, []byte(content), 0666)
186 if err != nil {
187 return err
188 }
189 return nil
190}
191
192// Renders .amber file into .html
193func buildAmber(in, out string, funcs template.FuncMap, vars Vars) error {
194 a := amber.New()
195 err := a.ParseFile(in)
196 if err != nil {
197 return err
198 }
199 t, err := a.Compile()
200 if err != nil {
201 return err
202 }
203 //amber.FuncMap = amber.FuncMap
204 f, err := os.Create(filepath.Join(PUBDIR, out))
205 if err != nil {
206 return err
207 }
208 defer f.Close()
209 return t.Execute(f, vars)
210}
211
212// Compiles .gcss into .css
213func buildGCSS(path string) error {
214 f, err := os.Open(path)
215 if err != nil {
216 return err
217 }
218 s := strings.TrimSuffix(path, ".gcss") + ".css"
219 css, err := os.Create(filepath.Join(PUBDIR, s))
220 if err != nil {
221 return err
222 }
223
224 defer f.Close()
225 defer css.Close()
226
227 _, err = gcss.Compile(css, f)
228 return err
229}
230
231// Copies file from working directory into public directory
232func copyFile(path string) (err error) {
233 var in, out *os.File
234 if in, err = os.Open(path); err == nil {
235 defer in.Close()
236 if out, err = os.Create(filepath.Join(PUBDIR, path)); err == nil {
237 defer out.Close()
238 _, err = io.Copy(out, in)
239 }
240 }
241 return err
242}
243
244func varFunc(s string) func() string {
245 return func() string {
246 return s
247 }
248}
249
250func pluginFunc(cmd string) func() string {
251 return func() string {
252 return "Not implemented yet"
253 }
254}
255
256func createFuncs() template.FuncMap {
257 // Builtin functions
258 funcs := template.FuncMap{
259 "exec": func(s ...string) string {
260 // Run external command with arguments
261 return ""
262 },
263 "zs": func(args ...string) string {
264 // Run zs with arguments
265 return ""
266 },
267 }
268 // Plugin functions
269 files, _ := ioutil.ReadDir(ZSDIR)
270 for _, f := range files {
271 if !f.IsDir() {
272 name := f.Name()
273 if !strings.HasSuffix(name, ".html") && !strings.HasSuffix(name, ".amber") {
274 funcs[strings.TrimSuffix(name, filepath.Ext(name))] = pluginFunc(name)
275 }
276 }
277 }
278 return funcs
279}
280
281func renameExt(path, from, to string) string {
282 if from == "" {
283 from = filepath.Ext(path)
284 }
285 return strings.TrimSuffix(path, from) + to
286}
287
288func globals() Vars {
289 vars := Vars{}
290 for _, e := range os.Environ() {
291 pair := strings.Split(e, "=")
292 if strings.HasPrefix(pair[0], "ZS_") {
293 vars[strings.ToLower(pair[0][3:])] = pair[1]
294 }
295 }
296 return vars
297}
298
299func buildAll(once bool) {
300 lastModified := time.Unix(0, 0)
301 modified := false
302
303 vars := globals()
304 for {
305 os.Mkdir(PUBDIR, 0755)
306 funcs := createFuncs()
307 err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
308 // ignore hidden files and directories
309 if filepath.Base(path)[0] == '.' || strings.HasPrefix(path, ".") {
310 return nil
311 }
312
313 if info.IsDir() {
314 os.Mkdir(filepath.Join(PUBDIR, path), 0755)
315 return nil
316 } else if info.ModTime().After(lastModified) {
317 if !modified {
318 // About to be modified, so run pre-build hook
319 // FIXME on windows it might not work well
320 run(filepath.Join(ZSDIR, "pre"), []string{}, nil, nil)
321 modified = true
322 }
323 ext := filepath.Ext(path)
324 if ext == ".md" || ext == ".mkd" {
325 log.Println("md: ", path)
326 return buildMarkdown(path, funcs, vars)
327 } else if ext == ".html" || ext == ".xml" {
328 log.Println("html: ", path)
329 return buildPlain(path, path, funcs, vars)
330 } else if ext == ".amber" {
331 log.Println("html: ", path)
332 return buildAmber(path, renameExt(path, ".amber", ".html"), funcs, vars)
333 } else if ext == ".gcss" {
334 log.Println("css: ", path)
335 return buildGCSS(path)
336 } else {
337 log.Println("raw: ", path)
338 return copyFile(path)
339 }
340 }
341 return nil
342 })
343 if err != nil {
344 log.Println("ERROR:", err)
345 }
346 if modified {
347 // Something was modified, so post-build hook
348 // FIXME on windows it might not work well
349 run(filepath.Join(ZSDIR, "post"), []string{}, nil, nil)
350 modified = false
351 }
352 lastModified = time.Now()
353 if once {
354 break
355 }
356 time.Sleep(1 * time.Second)
357 }
358}
359
360func main() {
361 if len(os.Args) == 1 {
362 fmt.Println(os.Args[0], "<command> [args]")
363 return
364 }
365 cmd := os.Args[1]
366 args := os.Args[2:]
367 switch cmd {
368 case "build":
369 buildAll(true)
370 case "watch":
371 buildAll(false) // pass duration
372 case "var":
373 if len(args) == 0 {
374 log.Println("ERROR: filename expected")
375 return
376 }
377 if vars, _, err := md(args[0], globals()); err == nil {
378 if len(args) > 1 {
379 for _, a := range args[1:] {
380 fmt.Println(vars[a])
381 }
382 } else {
383 for k, v := range vars {
384 fmt.Println(k + ":" + v)
385 }
386 }
387 } else {
388 log.Println("ERROR:", err)
389 }
390 default:
391 err := run(path.Join(ZSDIR, cmd), args, Vars{}, os.Stdout)
392 if err != nil {
393 log.Println("ERROR:", err)
394 }
395 }
396}
Note: See TracBrowser for help on using the repository browser.