source: code/trunk/zs.go@ 19

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

started migration to go templates

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