|
@@ -3,17 +3,24 @@ package staticmd
|
|
|
import (
|
|
|
"bufio"
|
|
|
"html/template"
|
|
|
+ "io"
|
|
|
"io/ioutil"
|
|
|
"os"
|
|
|
"path/filepath"
|
|
|
- "runtime"
|
|
|
"strings"
|
|
|
- "sync"
|
|
|
|
|
|
"github.com/russross/blackfriday"
|
|
|
)
|
|
|
|
|
|
-var MaxParallelism = runtime.NumCPU()
|
|
|
+var readfile = ioutil.ReadFile
|
|
|
+var create = os.Create
|
|
|
+var mkdirall = os.MkdirAll
|
|
|
+var parseFiles = template.ParseFiles
|
|
|
+var walk = filepath.Walk
|
|
|
+
|
|
|
+type ht interface {
|
|
|
+ Execute(io.Writer, interface{}) error
|
|
|
+}
|
|
|
|
|
|
type Generator struct {
|
|
|
Input string
|
|
@@ -25,11 +32,12 @@ type Generator struct {
|
|
|
|
|
|
version string
|
|
|
pages []string
|
|
|
- template *template.Template
|
|
|
+ template ht
|
|
|
}
|
|
|
|
|
|
// convert markdown input path to html output path
|
|
|
-// there is no reverse (because we support `.md`, `.mkd`, and `.markdown`)
|
|
|
+// @note: we cannot reverse since we do not track the extension
|
|
|
+// we support `.md`, `.mkd`, and `.markdown`
|
|
|
func (self *Generator) ior(path string) string {
|
|
|
return strings.TrimSuffix(strings.Replace(path, self.Input, self.Output, 1), filepath.Ext(path)) + ".html"
|
|
|
}
|
|
@@ -44,26 +52,19 @@ func (self *Generator) depth(path string) string {
|
|
|
return ""
|
|
|
}
|
|
|
|
|
|
-// walk the directories and build a list of pages
|
|
|
+// walk the directories and build a list of markdown files with content
|
|
|
func (self *Generator) walk(path string, file os.FileInfo, err error) error {
|
|
|
-
|
|
|
- // only pay attention to files with a size greater than 0
|
|
|
- if file == nil {
|
|
|
- } else if file.Mode().IsRegular() && file.Size() > 0 {
|
|
|
-
|
|
|
- // only add markdown files to our pages array (.md, .mkd, .markdown)
|
|
|
- if strings.HasSuffix(path, ".md") || strings.HasSuffix(path, ".mkd") || strings.HasSuffix(path, ".markdown") {
|
|
|
- self.pages = append(self.pages, path)
|
|
|
- }
|
|
|
+ if file != nil && file.Mode().IsRegular() && file.Size() > 0 && isMarkdown(path) {
|
|
|
+ self.pages = append(self.pages, path)
|
|
|
}
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
// build output concurrently to many pages
|
|
|
-func (self *Generator) multi() error {
|
|
|
+func (self *Generator) multi() (err error) {
|
|
|
|
|
|
// prepare navigation storage
|
|
|
- navigation := make(map[string][]Navigation)
|
|
|
+ navi := make(map[string][]navigation)
|
|
|
|
|
|
// loop pages to build table of contents
|
|
|
for i, _ := range self.pages {
|
|
@@ -73,13 +74,13 @@ func (self *Generator) multi() error {
|
|
|
dir := filepath.Dir(self.ior(out))
|
|
|
|
|
|
// create navigation object
|
|
|
- nav := Navigation{}
|
|
|
+ nav := navigation{}
|
|
|
|
|
|
// sub-index condition changes name, dir, and link
|
|
|
if filepath.Dir(out) != self.Output && strings.ToLower(basename(out)) == "index" {
|
|
|
|
|
|
// set name to containing folder
|
|
|
- nav.Name = basename(dir)
|
|
|
+ nav.Title = basename(dir)
|
|
|
|
|
|
// set relative or absolute link
|
|
|
if self.Relative {
|
|
@@ -93,7 +94,7 @@ func (self *Generator) multi() error {
|
|
|
} else {
|
|
|
|
|
|
// set name to files name
|
|
|
- nav.Name = basename(out)
|
|
|
+ nav.Title = basename(out)
|
|
|
|
|
|
// set relative or absolute link
|
|
|
if self.Relative {
|
|
@@ -104,110 +105,82 @@ func (self *Generator) multi() error {
|
|
|
}
|
|
|
|
|
|
// build indexes first-match
|
|
|
- if _, ok := navigation[dir]; !ok {
|
|
|
- navigation[dir] = make([]Navigation, 0)
|
|
|
+ if _, ok := navi[dir]; !ok {
|
|
|
+ navi[dir] = make([]navigation, 0)
|
|
|
|
|
|
// create output directory for when we create files
|
|
|
if ok, _ := exists(dir); !ok {
|
|
|
- if err := os.MkdirAll(dir, 0770); err != nil {
|
|
|
+ if err = mkdirall(dir, 0770); err != nil {
|
|
|
self.Logger.Error("Failed to create path: %s, %s", dir, err)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// append to navigational list
|
|
|
- navigation[dir] = append(navigation[dir], nav)
|
|
|
+ navi[dir] = append(navi[dir], nav)
|
|
|
}
|
|
|
|
|
|
- // debug navigation output
|
|
|
- self.Logger.Debug("navigation: %+v", navigation)
|
|
|
-
|
|
|
- // prepare waitgroup, bufferer channel, and add number of async handlers to wg
|
|
|
- var wg sync.WaitGroup
|
|
|
- pages := make(chan string, MaxParallelism)
|
|
|
- wg.Add(MaxParallelism)
|
|
|
-
|
|
|
- // prepare workers
|
|
|
- for i := 0; i < MaxParallelism; i++ {
|
|
|
- go func() {
|
|
|
- defer wg.Done()
|
|
|
-
|
|
|
- // iterate supplied pages
|
|
|
- for p := range pages {
|
|
|
-
|
|
|
- // acquire output filepath
|
|
|
- out := self.ior(p)
|
|
|
- dir := filepath.Dir(out)
|
|
|
-
|
|
|
- // prepare a new page object for our template to render
|
|
|
- page := Page{
|
|
|
- Name: basename(p),
|
|
|
- Version: self.version,
|
|
|
- Nav: navigation[self.Output],
|
|
|
- Depth: self.depth(out),
|
|
|
- }
|
|
|
+ // process all pages
|
|
|
+ for _, p := range self.pages {
|
|
|
|
|
|
- // read in page text
|
|
|
- if markdown, err := ioutil.ReadFile(p); err == nil {
|
|
|
+ // attempt to read entire document
|
|
|
+ var markdown []byte
|
|
|
+ if markdown, err = readfile(p); err != nil {
|
|
|
+ self.Logger.Error("failed to read file: %s, %s", p, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- // if this page happens to be a sub-index we can generate the table of contents
|
|
|
- if dir != self.Output && strings.ToLower(basename(p)) == "index" {
|
|
|
+ // acquire output filepath
|
|
|
+ out := self.ior(p)
|
|
|
+ dir := filepath.Dir(out)
|
|
|
|
|
|
- // iterate and build table of contents as basic markdown
|
|
|
- toc := "\n## Table of Contents:\n\n"
|
|
|
- for i, _ := range navigation[dir] {
|
|
|
- toc = toc + "- [" + navigation[dir][i].Name + "](" + navigation[dir][i].Link + ")\n"
|
|
|
- }
|
|
|
+ // prepare a new page object for our template to render
|
|
|
+ page := page{
|
|
|
+ Name: basename(p),
|
|
|
+ Version: self.version,
|
|
|
+ Nav: navi[self.Output],
|
|
|
+ Depth: self.depth(out),
|
|
|
+ }
|
|
|
|
|
|
- // debug table of contents output
|
|
|
- self.Logger.Debug("table of contents for %s, %s", out, toc)
|
|
|
+ // if this page happens to be a sub-index we can generate the table of contents
|
|
|
+ if dir != self.Output && strings.ToLower(basename(p)) == "index" {
|
|
|
|
|
|
- // prepend table of contents
|
|
|
- markdown = append([]byte(toc), markdown...)
|
|
|
- }
|
|
|
+ // iterate and build table of contents as basic markdown
|
|
|
+ toc := "\n## Table of Contents:\n\n"
|
|
|
+ for i, _ := range navi[dir] {
|
|
|
+ toc = toc + "- [" + navi[dir][i].Title + "](" + navi[dir][i].Link + ")\n"
|
|
|
+ }
|
|
|
|
|
|
- // convert to html, and accept as part of the template
|
|
|
- page.Content = template.HTML(blackfriday.MarkdownCommon(markdown))
|
|
|
- } else {
|
|
|
- self.Logger.Error("failed to read file: %s, %s", p, err)
|
|
|
- }
|
|
|
+ // debug: table of contents output
|
|
|
+ self.Logger.Debug("table of contents for %s, %s", out, toc)
|
|
|
|
|
|
- // debug page output
|
|
|
- self.Logger.Debug("page: %+v", page)
|
|
|
+ // prepend table of contents
|
|
|
+ markdown = append([]byte(toc), markdown...)
|
|
|
+ }
|
|
|
|
|
|
- // translate input path to output path & create a write context
|
|
|
- if f, err := os.Create(out); err == nil {
|
|
|
- defer f.Close()
|
|
|
+ // convert to html, and accept as part of the template
|
|
|
+ page.Content = template.HTML(blackfriday.MarkdownCommon(markdown))
|
|
|
|
|
|
- // prepare a writer /w buffer
|
|
|
- fb := bufio.NewWriter(f)
|
|
|
- defer fb.Flush()
|
|
|
+ // attempt to open file for output
|
|
|
+ var f *os.File
|
|
|
+ if f, err = create(out); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ defer f.Close()
|
|
|
|
|
|
- // attempt to use template to write output with page context
|
|
|
- if e := self.template.Execute(fb, page); e != nil {
|
|
|
- self.Logger.Error("Failed to write template: %s, %s", out, e)
|
|
|
- }
|
|
|
- } else {
|
|
|
- self.Logger.Error("failed to create new file: %s, %s", out, err)
|
|
|
- }
|
|
|
- }
|
|
|
- }()
|
|
|
- }
|
|
|
+ // prepare a writer /w buffer
|
|
|
+ fb := bufio.NewWriter(f)
|
|
|
+ defer fb.Flush()
|
|
|
|
|
|
- // send pages to workers for async rendering
|
|
|
- for i, _ := range self.pages {
|
|
|
- pages <- self.pages[i]
|
|
|
+ // attempt to use template to write output with page context
|
|
|
+ err = self.template.Execute(fb, page)
|
|
|
}
|
|
|
|
|
|
- // close channel and wait for async to finish before continuing
|
|
|
- close(pages)
|
|
|
- wg.Wait()
|
|
|
-
|
|
|
- return nil
|
|
|
+ return
|
|
|
}
|
|
|
|
|
|
// build output synchronously to a single page
|
|
|
-func (self *Generator) single() error {
|
|
|
+func (self *Generator) single() (err error) {
|
|
|
|
|
|
// prepare []byte array to store all files markdown
|
|
|
content := make([]byte, 0)
|
|
@@ -238,7 +211,8 @@ func (self *Generator) single() error {
|
|
|
toc = toc + strings.Repeat("\t", depth) + "- [" + basename(p) + "](#" + anchor + ")\n"
|
|
|
|
|
|
// read markdown from file or skip to next file
|
|
|
- markdown, err := ioutil.ReadFile(p)
|
|
|
+ var markdown []byte
|
|
|
+ markdown, err = readfile(p)
|
|
|
if err != nil {
|
|
|
self.Logger.Error("failed to read file: %s (%s)", p, err)
|
|
|
continue
|
|
@@ -260,26 +234,25 @@ func (self *Generator) single() error {
|
|
|
// prepend toc
|
|
|
content = append([]byte(toc), content...)
|
|
|
|
|
|
- // create page object with version & content
|
|
|
- page := Page{
|
|
|
- Version: self.version,
|
|
|
- Content: template.HTML(blackfriday.MarkdownCommon(content)),
|
|
|
- }
|
|
|
-
|
|
|
// prepare output directory
|
|
|
if ok, _ := exists(self.Output); !ok {
|
|
|
- if err := os.MkdirAll(self.Output, 0770); err != nil {
|
|
|
+ if err = mkdirall(self.Output, 0770); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // create page object with version & content
|
|
|
+ page := page{
|
|
|
+ Version: self.version,
|
|
|
+ Content: template.HTML(blackfriday.MarkdownCommon(content)),
|
|
|
+ }
|
|
|
+
|
|
|
// prepare output file path
|
|
|
out := filepath.Join(self.Output, "index.html")
|
|
|
|
|
|
// attempt to open file for output
|
|
|
var f *os.File
|
|
|
- var err error
|
|
|
- if f, err = os.Create(out); err != nil {
|
|
|
+ if f, err = create(out); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
defer f.Close()
|
|
@@ -289,14 +262,15 @@ func (self *Generator) single() error {
|
|
|
defer fb.Flush()
|
|
|
|
|
|
// attempt to use template to write output with page context
|
|
|
- return self.template.Execute(fb, page)
|
|
|
+ err = self.template.Execute(fb, page)
|
|
|
+ return
|
|
|
}
|
|
|
|
|
|
func (self *Generator) Generate() error {
|
|
|
|
|
|
// process template
|
|
|
var err error
|
|
|
- if self.template, err = template.ParseFiles(self.TemplateFile); err != nil {
|
|
|
+ if self.template, err = parseFiles(self.TemplateFile); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
@@ -311,16 +285,13 @@ func (self *Generator) Generate() error {
|
|
|
self.Input = filepath.Clean(self.Input)
|
|
|
self.Output = filepath.Clean(self.Output)
|
|
|
|
|
|
- // debug: print state
|
|
|
- self.Logger.Debug("generator state: %+v", self)
|
|
|
-
|
|
|
// walk the file system
|
|
|
- if err := filepath.Walk(self.Input, self.walk); err != nil {
|
|
|
+ if err := walk(self.Input, self.walk); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
- // debug: print pages
|
|
|
- self.Logger.Debug("pages: %+v", self.pages)
|
|
|
+ // debug: print state
|
|
|
+ self.Logger.Debug("generator state: %+v", self)
|
|
|
|
|
|
// determine assembly method
|
|
|
if self.Book {
|