From 6f2476510a5b31ada07a42c19065b47bbe784b7a Mon Sep 17 00:00:00 2001
From: Pacien
Date: Fri, 28 Jun 2013 15:31:04 +0200
Subject: First version
---
common.go | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
compiled.go | 46 ++++++++++++++++++++
dynamic.go | 69 +++++++++++++++++++++++++++++
files.go | 106 +++++++++++++++++++++++++++++++++++++++++++++
interactive.go | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
main.go | 60 ++++++++++++++++++++++++++
6 files changed, 546 insertions(+)
create mode 100644 common.go
create mode 100644 compiled.go
create mode 100644 dynamic.go
create mode 100644 files.go
create mode 100644 interactive.go
create mode 100644 main.go
diff --git a/common.go b/common.go
new file mode 100644
index 0000000..0cf2655
--- /dev/null
+++ b/common.go
@@ -0,0 +1,134 @@
+/*
+
+ This file is part of CompileTree (https://github.com/Pacien/CompileTree)
+
+ CompileTree is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ CompileTree is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with CompileTree. If not, see .
+
+*/
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "github.com/hoisie/mustache"
+ "github.com/russross/blackfriday"
+ "io/ioutil"
+ "path"
+ "strings"
+ "sync"
+)
+
+var wait sync.WaitGroup
+
+// Common templating
+
+func isParsable(fileName string) bool {
+ switch path.Ext(fileName) {
+ case ".md", ".html", ".txt":
+ return true
+ }
+ return false
+}
+
+func read(fileName string) ([]byte, error) {
+ fileBody, err := ioutil.ReadFile(fileName)
+ if err != nil {
+ return nil, err
+ }
+ if path.Ext(fileName) == ".md" {
+ fileBody = blackfriday.MarkdownCommon(fileBody)
+ }
+ return fileBody, nil
+}
+
+func merge(files map[string][]byte) (merged []byte) {
+ merged = files["index"]
+ for pass := 0; bytes.Contains(merged, []byte("{{> ")) && pass < 4000; pass++ {
+ for fileName, fileBody := range files {
+ merged = bytes.Replace(merged, []byte("{{> "+fileName+"}}"), fileBody, -1)
+ }
+ }
+ return
+}
+
+// COMPILED and INTERACTIVE modes
+
+// render and write everything inside
+
+func parse(dirPath string, elements map[string][]byte, overwrite bool) map[string][]byte {
+ _, filesList := ls(dirPath)
+ for _, fileName := range filesList {
+ if isParsable(fileName) && (overwrite || elements[fileName[:len(fileName)-len(path.Ext(fileName))]] == nil) {
+ var err error
+ elements[fileName[:len(fileName)-len(path.Ext(fileName))]], err = read(path.Join(dirPath, fileName))
+ if err != nil {
+ fmt.Println(err)
+ }
+ }
+ }
+ return elements
+}
+
+func compile(dirPath string, elements map[string][]byte, sourceDir, outputDir string, recursive bool) {
+ wait.Add(1)
+ defer wait.Done()
+
+ if strings.HasPrefix(dirPath, outputDir) {
+ return
+ }
+
+ elements = parse(dirPath, elements, true)
+
+ if recursive {
+ dirs, _ := ls(dirPath)
+ for _, dir := range dirs {
+ go compile(path.Join(dirPath, dir), elements, sourceDir, outputDir, recursive)
+ }
+ }
+
+ template := merge(elements)
+ page := mustache.Render(string(template), nil /* TODO: generate contextual variables */)
+
+ err := writeFile(path.Join(outputDir, strings.TrimPrefix(dirPath, sourceDir), "index.html"), []byte(page))
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+}
+
+func copyFiles(dirPath, sourceDir, outputDir string, recursive bool) {
+ wait.Add(1)
+ defer wait.Done()
+
+ if strings.HasPrefix(dirPath, outputDir) {
+ return
+ }
+
+ dirs, files := ls(dirPath)
+ for _, file := range files {
+ if !isParsable(file) {
+ err := cp(path.Join(dirPath, file), path.Join(outputDir, strings.TrimPrefix(dirPath, sourceDir), file))
+ if err != nil {
+ fmt.Println(err)
+ }
+ }
+ }
+
+ if recursive {
+ for _, dir := range dirs {
+ go copyFiles(path.Join(dirPath, dir), sourceDir, outputDir, recursive)
+ }
+ }
+}
diff --git a/compiled.go b/compiled.go
new file mode 100644
index 0000000..5b2c19b
--- /dev/null
+++ b/compiled.go
@@ -0,0 +1,46 @@
+/*
+
+ This file is part of CompileTree (https://github.com/Pacien/CompileTree)
+
+ CompileTree is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ CompileTree is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with CompileTree. If not, see .
+
+*/
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "time"
+)
+
+func compiled(sourceDir, outputDir string) {
+ // remove previously compiled site
+ err := os.RemoveAll(outputDir)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ // compile everything
+ go compile(sourceDir, make(map[string][]byte), sourceDir, outputDir, true)
+ go copyFiles(sourceDir, sourceDir, outputDir, true)
+
+ // sleep some milliseconds to prevent early exit
+ time.Sleep(time.Millisecond * 100)
+
+ // wait until all tasks are completed
+ wait.Wait()
+ fmt.Println("Compilation done.")
+}
diff --git a/dynamic.go b/dynamic.go
new file mode 100644
index 0000000..0307003
--- /dev/null
+++ b/dynamic.go
@@ -0,0 +1,69 @@
+/*
+
+ This file is part of CompileTree (https://github.com/Pacien/CompileTree)
+
+ CompileTree is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ CompileTree is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with CompileTree. If not, see .
+
+*/
+
+package main
+
+import (
+ "fmt"
+ "github.com/hoisie/mustache"
+ "net/http"
+ "path"
+ "strings"
+)
+
+func handle(w http.ResponseWriter, r *http.Request) {
+ // serve static files
+ if !(path.Ext(r.URL.Path) == "" || isParsable(path.Ext(r.URL.Path))) {
+ http.ServeFile(w, r, path.Join(*settings.sourceDir, r.URL.Path))
+ return
+ }
+
+ // get the list of dirs to parse
+ request := strings.Trim(r.URL.Path, "/")
+ dirs := strings.Split(request, "/")
+ if request != "" {
+ dirs = append(dirs, "")
+ }
+
+ // parse these dirs
+ elements := make(map[string][]byte)
+ for _, dir := range dirs {
+ parse(path.Join(*settings.sourceDir, dir), elements, false)
+ }
+
+ // render the page
+ template := merge(elements)
+ page := mustache.Render(string(template), nil /* TODO: generate contextual variables */)
+
+ // serve the page
+ _, err := w.Write([]byte(page))
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+}
+
+func dynamic(port string) {
+ fmt.Println("Listening on: localhost:" + port)
+ http.HandleFunc("/", handle)
+ err := http.ListenAndServe(":"+port, nil)
+ if err != nil {
+ fmt.Println(err)
+ }
+}
diff --git a/files.go b/files.go
new file mode 100644
index 0000000..afdac86
--- /dev/null
+++ b/files.go
@@ -0,0 +1,106 @@
+/*
+
+ This file is part of CompileTree (https://github.com/Pacien/CompileTree)
+
+ CompileTree is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ CompileTree is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with CompileTree. If not, see .
+
+*/
+
+package main
+
+import (
+ "io"
+ "io/ioutil"
+ "os"
+ "path"
+ "strings"
+)
+
+// Filesystem utils
+
+func isDir(dirPath string) bool {
+ stat, err := os.Stat(dirPath)
+ if err != nil {
+ return false
+ }
+ return stat.IsDir()
+}
+
+func isHidden(fileName string) bool {
+ return strings.HasPrefix(fileName, ".")
+}
+
+func ls(path string) (dirs []string, files []string) {
+ content, err := ioutil.ReadDir(path)
+ if err != nil {
+ return
+ }
+ for _, element := range content {
+ if isHidden(element.Name()) {
+ continue
+ }
+ if element.IsDir() {
+ dirs = append(dirs, element.Name())
+ } else {
+ files = append(files, element.Name())
+ }
+ }
+ return
+}
+
+func explore(dirPath string) (paths []string) {
+ dirs, _ := ls(dirPath)
+ for _, dir := range dirs {
+ sourceDir := path.Join(dirPath, dir)
+ paths = append(paths, sourceDir)
+ subDirs := explore(sourceDir)
+ for _, subDir := range subDirs {
+ paths = append(paths, subDir)
+ }
+ }
+ return
+}
+
+func cp(source, target string) error {
+ sourceFile, err := os.Open(source)
+ if err != nil {
+ return err
+ }
+ defer sourceFile.Close()
+
+ dir, _ := path.Split(target)
+ err = os.MkdirAll(dir, 0777)
+ if err != nil {
+ return err
+ }
+
+ targetFile, err := os.Create(target)
+ if err != nil {
+ return err
+ }
+ defer targetFile.Close()
+
+ _, err = io.Copy(targetFile, sourceFile)
+ return err
+}
+
+func writeFile(target string, body []byte) error {
+ dir, _ := path.Split(target)
+ err := os.MkdirAll(dir, 0777)
+ if err != nil {
+ return err
+ }
+ err = ioutil.WriteFile(target, body, 0777)
+ return err
+}
diff --git a/interactive.go b/interactive.go
new file mode 100644
index 0000000..34c2f68
--- /dev/null
+++ b/interactive.go
@@ -0,0 +1,131 @@
+/*
+
+ This file is part of CompileTree (https://github.com/Pacien/CompileTree)
+
+ CompileTree is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ CompileTree is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with CompileTree. If not, see .
+
+*/
+
+package main
+
+import (
+ "fmt"
+ "github.com/howeyc/fsnotify"
+ "os"
+ "path"
+ "strings"
+ "time"
+)
+
+func watch(dirPath string, watcher *fsnotify.Watcher) *fsnotify.Watcher {
+ watcher.Watch(dirPath)
+ dirs := explore(dirPath)
+ for _, dir := range dirs {
+ if !strings.HasPrefix(dir, *settings.outputDir) {
+ err := watcher.Watch(dir)
+ if err != nil {
+ fmt.Println(err)
+ }
+ }
+ }
+ return watcher
+}
+
+func parseParents(dir, sourceDir string) map[string][]byte {
+ dirs := strings.Split(strings.TrimPrefix(dir, sourceDir), "/")
+ elements := make(map[string][]byte)
+ for _, dir := range dirs {
+ elements = parse(path.Join(sourceDir, dir), elements, false)
+ }
+ return elements
+}
+
+func interactive(sourceDir, outputDir string) {
+
+ // compile the whole site
+ compiled(sourceDir, outputDir)
+
+ // watch the source dir
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ fmt.Println(err)
+ }
+ defer watcher.Close()
+ watcher = watch(sourceDir, watcher)
+
+ for {
+ select {
+ case ev := <-watcher.Event:
+ fmt.Println(ev)
+
+ // ignore hidden files
+ if isHidden(ev.Name) {
+ break
+ }
+
+ // manage watchers
+ if ev.IsDelete() || ev.IsRename() {
+ err = watcher.RemoveWatch(ev.Name)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ } else if ev.IsCreate() && isDir(ev.Name) {
+ watcher = watch(ev.Name, watcher)
+ }
+
+ dir, _ := path.Split(ev.Name)
+
+ // remove previously compiled files
+ if ev.IsDelete() || ev.IsRename() || ev.IsModify() {
+ var err error
+ if isDir(ev.Name) || !isParsable(ev.Name) {
+ err = os.RemoveAll(path.Join(outputDir, strings.TrimPrefix(ev.Name, sourceDir)))
+ } else {
+ err = os.RemoveAll(path.Join(outputDir, strings.TrimPrefix(dir, sourceDir)))
+ }
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ }
+
+ // recompile changed files
+ if ev.IsCreate() || ev.IsModify() {
+ if isDir(ev.Name) {
+ elements := parseParents(ev.Name, sourceDir)
+ dirPath := path.Join(sourceDir, strings.TrimPrefix(ev.Name, sourceDir))
+ go compile(dirPath, elements, sourceDir, outputDir, true)
+ go copyFiles(dirPath, sourceDir, outputDir, true)
+ } else {
+ dirPath := path.Join(sourceDir, strings.TrimPrefix(dir, sourceDir))
+ if isParsable(path.Ext(ev.Name)) {
+ elements := parseParents(dir, sourceDir)
+ go compile(dirPath, elements, sourceDir, outputDir, true)
+ }
+ go copyFiles(dirPath, sourceDir, outputDir, false)
+ }
+ }
+
+ // sleep some milliseconds to prevent early exit
+ time.Sleep(time.Millisecond * 100)
+
+ // wait until all tasks are completed
+ wait.Wait()
+
+ case err := <-watcher.Error:
+ fmt.Println(err)
+ }
+ }
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..49b27fa
--- /dev/null
+++ b/main.go
@@ -0,0 +1,60 @@
+/*
+
+ This file is part of CompileTree (https://github.com/Pacien/CompileTree)
+
+ CompileTree is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ CompileTree is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with CompileTree. If not, see .
+
+*/
+
+package main
+
+import (
+ "flag"
+ "fmt"
+)
+
+var settings struct {
+ mode *string // compiled, interactive or dynamic
+ sourceDir *string
+ outputDir *string // for compiled site
+ port *string // for the integrated web server (dynamic mode only)
+}
+
+func init() {
+ // read settings
+ settings.mode = flag.String("mode", "compiled", "compiled|interactive|dynamic")
+ settings.sourceDir = flag.String("source", ".", "Path to sources directory.")
+ settings.outputDir = flag.String("output", "./out", "[compiled mode] Path to output directory.")
+ settings.port = flag.String("port", "8080", "[dynamic mode] Port to listen.")
+ flag.Parse()
+}
+
+func main() {
+ fmt.Println("CompileTree")
+ fmt.Println("Mode: " + *settings.mode)
+ fmt.Println("Source: " + *settings.sourceDir)
+ fmt.Println("Output: " + *settings.outputDir)
+ fmt.Println("====================")
+
+ switch *settings.mode {
+ case "compiled":
+ compiled(*settings.sourceDir, *settings.outputDir)
+ case "interactive":
+ interactive(*settings.sourceDir, *settings.outputDir)
+ case "dynamic":
+ dynamic(*settings.port)
+ default:
+ panic("Invalid mode.")
+ }
+}
--
cgit v1.2.3