source: code/trunk/zs.go@ 33

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

removed funcs, ext and utils

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