source: code/trunk/zs.go@ 20

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

added global variables, added default date for markdown

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