source: code/trunk/zs.go@ 23

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

restored the original amber since my issue has been fixed

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/eknkc/amber"
18 "github.com/russross/blackfriday"
19 "github.com/yosssi/gcss"
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.