Explorar el Código

address public page and navigation structs

make file type check modular

implement complete unit tests for the library
Casey DeLorme hace 10 años
padre
commit
835b8d706f
Se han modificado 6 ficheros con 337 adiciones y 123 borrados
  1. 86 115
      generator.go
  2. 216 0
      generator_test.go
  3. 3 3
      navigation.go
  4. 2 2
      page.go
  5. 14 3
      staticmd.go
  6. 16 0
      staticmd_test.go

+ 86 - 115
generator.go

@@ -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 {

+ 216 - 0
generator_test.go

@@ -0,0 +1,216 @@
+package staticmd
+
+import (
+	"html/template"
+	"io"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+)
+
+var mockReadfileBytes []byte
+var mockReadfileError error
+var mockTemplateError error
+var mockCreateFile *os.File
+var mockCreateError error
+var mockMkdirError error
+var mockParseTemplate *template.Template
+var mockParseError error
+var mockWalkError error
+
+func init() {
+	readfile = func(_ string) ([]byte, error) { return mockReadfileBytes, mockReadfileError }
+	create = func(_ string) (*os.File, error) { return mockCreateFile, mockCreateError }
+	mkdirall = func(_ string, _ os.FileMode) error { return mockMkdirError }
+	parseFiles = func(...string) (*template.Template, error) { return mockParseTemplate, mockParseError }
+	walk = func(_ string, _ filepath.WalkFunc) error { return mockWalkError }
+}
+
+type mockLogger struct{}
+
+func (self *mockLogger) Error(_ string, _ ...interface{}) {}
+func (self *mockLogger) Debug(_ string, _ ...interface{}) {}
+func (self *mockLogger) Info(_ string, _ ...interface{})  {}
+
+type mockTemplate struct{}
+
+func (self *mockTemplate) Execute(_ io.Writer, _ interface{}) error { return mockTemplateError }
+
+type mockFileInfo struct {
+	N  string
+	S  int64
+	Fm uint32
+	T  time.Time
+	D  bool
+	So interface{}
+}
+
+func (self *mockFileInfo) Name() string       { return self.N }
+func (self *mockFileInfo) Size() int64        { return self.S }
+func (self *mockFileInfo) Mode() os.FileMode  { return os.FileMode(self.Fm) }
+func (self *mockFileInfo) ModTime() time.Time { return self.T }
+func (self *mockFileInfo) IsDir() bool        { return self.D }
+func (self *mockFileInfo) Sys() interface{}   { return self.So }
+
+func TestIor(t *testing.T) {
+	t.Parallel()
+	g := Generator{}
+	if s := g.ior("some.md"); len(s) == 0 {
+		t.FailNow()
+	}
+}
+
+func TestDepth(t *testing.T) {
+	t.Parallel()
+	absp := "/abs/path/"
+	g := Generator{Output: absp}
+
+	// test abs depth
+	if d := g.depth("somefile"); len(d) > 0 {
+		t.FailNow()
+	}
+
+	// test relative depth
+	g.Relative = true
+	if d := g.depth(absp + "somefile"); len(d) == 0 {
+		t.Logf("Path: %s\n", d)
+		t.FailNow()
+	}
+}
+
+func TestWalk(t *testing.T) {
+	t.Parallel()
+	g := Generator{}
+
+	p := "valid.md"
+	var f os.FileInfo = &mockFileInfo{S: 1}
+	var e error
+
+	// test with valid file
+	if err := g.walk(p, f, e); err != nil {
+		t.FailNow()
+	}
+}
+
+func TestMulti(t *testing.T) {
+	g := Generator{Logger: &mockLogger{}, template: &mockTemplate{}, pages: []string{"fuck.md", "deeper/than/index.md", "deeper/than/data.md"}}
+
+	// reset defaults for parameters
+	mockCreateError = nil
+	mockReadfileError = nil
+	mockMkdirError = nil
+	statError = nil
+	notExist = false
+	mockCreateFile = &os.File{}
+
+	// no pages
+	if e := g.multi(); e != nil {
+		t.FailNow()
+	}
+
+	// test full pass
+	if e := g.multi(); e != nil {
+		t.FailNow()
+	}
+
+	// test full pass relative
+	g.Relative = true
+	if e := g.multi(); e != nil {
+		t.FailNow()
+	}
+
+	// test failing file creation
+	mockCreateError = mockError
+	if e := g.multi(); e == nil {
+		t.FailNow()
+	}
+
+	// test failing to read the file
+	mockReadfileError = mockError
+	if e := g.multi(); e == nil {
+		t.FailNow()
+	}
+
+	// test dir creation failure
+	mockMkdirError = mockError
+	statError = mockError
+	notExist = true
+	if e := g.multi(); e == nil {
+		t.FailNow()
+	}
+}
+
+func TestSingle(t *testing.T) {
+	t.Parallel()
+	g := Generator{Logger: &mockLogger{}, template: &mockTemplate{}, pages: []string{"fuck.md", "deeper/than/index.md", "deeper/than/data.md"}}
+
+	// reset defaults for parameters
+	mockCreateError = nil
+	mockReadfileError = nil
+	mockMkdirError = nil
+	statError = nil
+	notExist = false
+	mockCreateFile = &os.File{}
+
+	// test full pass
+	if e := g.single(); e != nil {
+		t.FailNow()
+	}
+
+	// test create error
+	mockCreateError = mockError
+	if e := g.single(); e == nil {
+		t.FailNow()
+	}
+
+	// test fail mkdirall
+	mockMkdirError = mockError
+	statError = mockError
+	if e := g.single(); e == nil {
+		t.FailNow()
+	}
+
+	// test fail readfile
+	mockReadfileError = mockError
+	if e := g.single(); e == nil {
+		t.FailNow()
+	}
+}
+
+func TestGenerate(t *testing.T) {
+	t.Parallel()
+	g := Generator{Logger: &mockLogger{}}
+
+	// reset defaults for parameters
+	mockParseTemplate = template.New("test")
+	mockCreateError = nil
+	mockReadfileError = nil
+	mockMkdirError = nil
+	statError = nil
+	notExist = false
+	mockTemplateError = nil
+
+	// test full pass
+	if e := g.Generate(); e != nil {
+		t.FailNow()
+	}
+
+	// test book mode full pass
+	g.Book = true
+	if e := g.Generate(); e == nil {
+		t.FailNow()
+	}
+
+	// test walk error
+	mockWalkError = mockError
+	if e := g.Generate(); e == nil {
+		t.FailNow()
+	}
+
+	// test template error
+	mockParseError = mockError
+	if e := g.Generate(); e == nil {
+		t.FailNow()
+	}
+}

+ 3 - 3
navigation.go

@@ -1,6 +1,6 @@
 package staticmd
 
-type Navigation struct {
-	Link string
-	Name string
+type navigation struct {
+	Link  string
+	Title string
 }

+ 2 - 2
page.go

@@ -2,10 +2,10 @@ package staticmd
 
 import "html/template"
 
-type Page struct {
+type page struct {
 	Name    string
 	Version string
-	Nav     []Navigation
+	Nav     []navigation
 	Depth   string
 	Content template.HTML
 }

+ 14 - 3
staticmd.go

@@ -20,17 +20,18 @@ type logger interface {
 	Info(string, ...interface{})
 }
 
-var stat = os.Stat
-var isNotExist = os.IsNotExist
-
 type runner interface {
 	Run(string, ...string) ([]byte, error)
 }
 
+var stat = os.Stat
+var isNotExist = os.IsNotExist
+var extensions = []string{".md", ".mkd", ".markdown"}
 var runnable runner
 
 type cmd struct{}
 
+// a command runner to abstract exec
 func (self cmd) Run(command string, args ...string) ([]byte, error) {
 	return exec.Command(command, args...).Output()
 }
@@ -65,3 +66,13 @@ func version(dir string) string {
 func basename(name string) string {
 	return filepath.Base(strings.TrimSuffix(name, filepath.Ext(name)))
 }
+
+// check for markdown in supported extensions array
+func isMarkdown(path string) bool {
+	for i := range extensions {
+		if strings.HasSuffix(path, extensions[i]) {
+			return true
+		}
+	}
+	return false
+}

+ 16 - 0
staticmd_test.go

@@ -87,3 +87,19 @@ func TestBasename(t *testing.T) {
 		t.FailNow()
 	}
 }
+
+func TestIsMarkdown(t *testing.T) {
+	t.Parallel()
+
+	// test matching types
+	for i := range extensions {
+		if !isMarkdown("file." + extensions[i]) {
+			t.FailNow()
+		}
+	}
+
+	// test non matching type
+	if isMarkdown("nope") {
+		t.FailNow()
+	}
+}