فهرست منبع

address public page and navigation structs

make file type check modular

implement complete unit tests for the library
Casey DeLorme 8 سال پیش
والد
کامیت
835b8d706f
6فایلهای تغییر یافته به همراه337 افزوده شده و 123 حذف شده
  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()
+	}
+}