Переглянути джерело

update static library code to be much simpler

refresh tests to operate via black-box

relocate templates and embed with go-bindata
cdelorme 7 роки тому
батько
коміт
c1b6ae4dc7
9 змінених файлів з 325 додано та 552 видалено
  1. 210 173
      markdown.go
  2. 29 186
      markdown_test.go
  3. 0 6
      navigation.go
  4. 0 11
      page.go
  5. 12 66
      static.go
  6. 1 104
      static_test.go
  7. 71 0
      templates.go
  8. 1 1
      templates/book.tmpl
  9. 1 5
      templates/web.tmpl

+ 210 - 173
markdown.go

@@ -1,224 +1,261 @@
+//go:generate go-bindata -pkg static -o templates.go templates/
 package static
 
 import (
-	"bufio"
 	"html/template"
-	"io"
 	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
 )
 
-var readfile = ioutil.ReadFile
+var readall = ioutil.ReadAll
+var open = os.Open
 var create = os.Create
 var mkdirall = os.MkdirAll
-var parseFiles = template.ParseFiles
-var walk = filepath.Walk
 
-type executor interface {
-	Execute(io.Writer, interface{}) error
+type logger interface {
+	Info(string, ...interface{})
+	Debug(string, ...interface{})
+	Error(string, ...interface{})
 }
 
 type operation func([]byte) []byte
 
+// This is the compiler that collects the list markdown files, a title, the
+// input and output paths, and whether to produce multiple files (web mode) or
+// to produce a single file (default, book mode).
+//
+// All public properties are not thread safe, so concurrent execution may yield
+// errors if those properties are being modified or accessed in parallel.
 type Markdown struct {
-	Book         bool   `json:"book,omitempty"`
-	Input        string `json:"input,omitempty"`
-	Output       string `json:"output,omitempty"`
-	Relative     bool   `json:"relative,omitempty"`
-	TemplateFile string `json:"template,omitempty"`
-
-	L logger
-
-	version  string
-	pages    []string
-	template executor
+	Title    string `json:"title,omitempty"`
+	Input    string `json:"input,omitempty"`
+	Output   string `json:"output,omitempty"`
+	Web      bool   `json:"web,omitempty"`
+	Template string `json:"template,omitempty"`
+	Version  string `json:"version,omitempty"`
+	L        logger `json:"-"`
+
+	err   error
+	files []string
 }
 
-func (g *Markdown) ior(path string) string {
-	return strings.TrimSuffix(strings.Replace(path, g.Input, g.Output, 1), filepath.Ext(path)) + ".html"
+// This function helps us handle any errors encountered during processing
+// without forcing us to immediately terminate.
+//
+// It also is responsible for logging every error encountered.
+//
+// If the error is nil it ignores it, thus the last non-nil error will be
+// returned, so that the caller knows at least one failure has occurred.
+func (m *Markdown) errors(err error) {
+	if err == nil {
+		return
+	}
+	m.L.Error(err.Error())
+	m.err = err
 }
 
-func (g *Markdown) depth(path string) string {
-	if g.Relative {
-		if rel, err := filepath.Rel(filepath.Dir(path), g.Output); err == nil {
-			return rel + string(os.PathSeparator)
+// If the absolute path minus the file extension already exist then we want to
+// know so we can avoid redundant processing, overwriting files, and potential
+// race conditions.
+func (m *Markdown) matches(file string) bool {
+	for i := range m.files {
+		if strings.TrimSuffix(file, filepath.Ext(file)) == strings.TrimSuffix(m.files[i], filepath.Ext(m.files[i])) {
+			return true
 		}
 	}
-	return ""
+	return false
 }
 
-func (g *Markdown) walk(path string, file os.FileInfo, err error) error {
-	if file != nil && file.Mode().IsRegular() && file.Size() > 0 && isMarkdown(path) {
-		g.pages = append(g.pages, path)
+// This checks the extension against a list of supported extensions.
+func (m *Markdown) valid(file string) bool {
+	for i := range extensions {
+		if filepath.Ext(file) == extensions[i] {
+			return true
+		}
 	}
-	return err
+	return false
 }
 
-func (g *Markdown) multi(run operation) error {
-	navi := make(map[string][]navigation)
-	var err error
-
-	for i, _ := range g.pages {
-		out := g.ior(g.pages[i])
-		dir := filepath.Dir(g.ior(out))
-		nav := navigation{}
-
-		if filepath.Dir(out) != g.Output && strings.ToLower(basename(out)) == "index" {
-			nav.Title = basename(dir)
-			if g.Relative {
-				nav.Link = filepath.Join(strings.TrimPrefix(dir, filepath.Dir(dir)+string(os.PathSeparator)), filepath.Base(out))
-			} else {
-				nav.Link = strings.TrimPrefix(dir, g.Output) + string(os.PathSeparator)
-			}
-			dir = filepath.Dir(dir)
-		} else {
-			nav.Title = basename(out)
-			if g.Relative {
-				nav.Link = strings.TrimPrefix(out, filepath.Dir(out)+string(os.PathSeparator))
-			} else {
-				nav.Link = strings.TrimPrefix(out, g.Output)
-			}
-		}
-
-		if _, ok := navi[dir]; !ok {
-			navi[dir] = make([]navigation, 0)
-			if ok, _ := exists(dir); !ok {
-				if err = mkdirall(dir, 0770); err != nil {
-					g.L.Error("failed to create path: %s, %s", dir, err)
-				}
-			}
-		}
-
-		navi[dir] = append(navi[dir], nav)
+// When walking through files we collect errors but do not return them, so that
+// the entire operation is not canceled due to a single failure.
+//
+// If there is an error, the file is a directory, the file is irregular, the
+// file does not have a markdown extension, or the file name minus its
+// extention is already in our list, then we skip that file.
+//
+// Thus the first file matched is the only file processed, which is to deal
+// with multiple valid markdown extensions for the same file basename.
+//
+// Each verified file is added to the list of files, which we will process
+// after we finish iterating all files.
+func (m *Markdown) walk(file string, f os.FileInfo, e error) error {
+	m.errors(e)
+	if e != nil || f.IsDir() || !f.Mode().IsRegular() || f.Size() == 0 || !m.valid(file) || m.matches(file) {
+		return nil
 	}
+	m.files = append(m.files, file)
+	return nil
+}
 
-	for _, p := range g.pages {
-		var markdown []byte
-		if markdown, err = readfile(p); err != nil {
-			g.L.Error("failed to read file: %s, %s", p, err)
-			return err
-		}
+// A way to abstract the process of getting a template
+func (m *Markdown) template() (*template.Template, error) {
+	if m.Template != "" {
+		return template.ParseFiles(m.Template)
+	}
+	var assetFile = "templates/book.tmpl"
+	if m.Web {
+		assetFile = "templates/web.tmpl"
+	}
+	d, e := Asset(assetFile)
+	if e != nil {
+		return nil, e
+	}
+	t := template.New("markdown")
+	return t.Parse(string(d))
+}
 
-		out := g.ior(p)
-		dir := filepath.Dir(out)
-		page := page{
-			Name:    basename(p),
-			Version: g.version,
-			Nav:     navi[g.Output],
-			Depth:   g.depth(out),
+// This operation processes each file independently, which includes passing to
+// each its own page structure.
+//
+// In the future, when buffered markdown parsers exist, this should leverage
+// concurrency, but the current implementation is bottlenecked at disk IO.
+//
+// The template is created first, using the compiled bindata by default, or the
+// supplied template file if able.
+func (m *Markdown) web(o operation) error {
+	t, e := m.template()
+	if e != nil {
+		return e
+	}
+	for i := range m.files {
+		in, e := open(m.files[i])
+		if e != nil {
+			m.errors(e)
+			continue
 		}
-
-		if dir != g.Output && strings.ToLower(basename(p)) == "index" {
-			toc := "\n## Table of Contents:\n\n"
-			for i, _ := range navi[dir] {
-				toc = toc + "- [" + navi[dir][i].Title + "](" + navi[dir][i].Link + ")\n"
-			}
-			g.L.Debug("table of contents for %s, %s", out, toc)
-			markdown = append([]byte(toc), markdown...)
+		b, e := readall(in)
+		m.errors(in.Close())
+		if e != nil {
+			m.errors(e)
+			continue
 		}
-
-		page.Content = template.HTML(run(markdown))
-
-		var f *os.File
-		if f, err = create(out); err != nil {
-			g.L.Error("%s\n", err)
-			return err
+		d := o(b)
+		m.errors(mkdirall(filepath.Dir(filepath.Join(m.Output, strings.TrimSuffix(strings.TrimPrefix(m.files[i], m.Input), filepath.Ext(m.files[i]))+".html")), os.ModePerm))
+		out, e := create(filepath.Join(m.Output, strings.TrimSuffix(strings.TrimPrefix(m.files[i], m.Input), filepath.Ext(m.files[i]))+".html"))
+		if e != nil {
+			m.errors(e)
+			continue
 		}
-		defer f.Close()
 
-		fb := bufio.NewWriter(f)
-		defer fb.Flush()
-
-		if err = g.template.Execute(fb, page); err != nil {
-			g.L.Error("%s\n", err)
+		if e := t.Execute(out, struct {
+			Title   string
+			Name    string
+			Content template.HTML
+			Version string
+		}{
+			Content: template.HTML(string(d)),
+			Title:   m.Title,
+			Name:    strings.TrimSuffix(filepath.Base(m.files[i]), filepath.Ext(m.files[i])),
+			Version: m.Version,
+		}); e != nil {
+			m.errors(e)
 		}
+		m.errors(out.Close())
 	}
-
-	return err
+	return nil
 }
 
-func (g *Markdown) single(run operation) error {
-	content := make([]byte, 0)
-	toc := "\n"
-	previous_depth := 0
-	var err error
-
-	for _, p := range g.pages {
-		shorthand := strings.TrimPrefix(p, g.Input+string(os.PathSeparator))
-		depth := strings.Count(shorthand, string(os.PathSeparator))
-		if depth > previous_depth {
-			toc = toc + strings.Repeat("\t", depth-1) + "- " + basename(filepath.Dir(p)) + "\n"
-		}
-		anchor := strings.Replace(shorthand, string(os.PathSeparator), "-", -1)
-		toc = toc + strings.Repeat("\t", depth) + "- [" + basename(p) + "](#" + anchor + ")\n"
-
-		var markdown []byte
-		if markdown, err = readfile(p); err != nil {
-			g.L.Error("failed to read file: %s (%s)", p, err)
+// This operation processes each file sequentially, and keeps the bytes for all
+// files in memory so it can write the output to a single file.
+//
+// In the future, it would be best if each file were loaded into a buffered
+// markdown parser and piped to a buffered template so that the system could
+// avoid storing all bytes in memory.
+//
+// Once every file has been loaded into a single byte array, we run it through
+// the markdown processor `operation`, and pass that into a template which then
+// pushes the output to a single file.
+func (m *Markdown) book(o operation) error {
+	t, e := m.template()
+	if e != nil {
+		return e
+	}
+	var b []byte
+	for i := range m.files {
+		in, e := open(m.files[i])
+		if e != nil {
+			m.errors(e)
 			continue
 		}
-
-		markdown = append([]byte("\n<a id='"+anchor+"'/>\n\n"), markdown...)
-		markdown = append(markdown, []byte("\n[back to top](#top)\n\n")...)
-		content = append(content, markdown...)
-		previous_depth = depth
-	}
-
-	content = append([]byte(toc), content...)
-
-	if ok, _ := exists(g.Output); !ok {
-		if err = mkdirall(g.Output, 0770); err != nil {
-			g.L.Error("failed to create path: %s (%s)", g.Output, err)
-			return err
+		d, e := readall(in)
+		m.errors(in.Close())
+		if e != nil {
+			m.errors(e)
+			continue
 		}
+		b = append(b, d...)
 	}
-
-	page := page{
-		Version: g.version,
-		Content: template.HTML(run(content)),
+	d := o(b)
+	m.errors(mkdirall(filepath.Dir(m.Output), os.ModePerm))
+	out, e := create(m.Output)
+	if e != nil {
+		return e
 	}
-	out := filepath.Join(g.Output, "index.html")
-
-	var f *os.File
-	if f, err = create(out); err != nil {
-		g.L.Error("%s\n", err)
-		return err
-	}
-	defer f.Close()
-
-	fb := bufio.NewWriter(f)
-	defer fb.Flush()
-
-	if err = g.template.Execute(fb, page); err != nil {
-		g.L.Error("%s\n", err)
-	}
-
-	return err
+	defer out.Close()
+	return t.Execute(out, struct {
+		Title   string
+		Content template.HTML
+		Version string
+	}{
+		Content: template.HTML(string(d)),
+		Title:   m.Title,
+		Version: m.Version,
+	})
 }
 
-func (g *Markdown) Generate(run operation) error {
-	var err error
-	if g.template, err = parseFiles(g.TemplateFile); err != nil {
-		g.L.Error("%s\n", err)
-		return err
+// The primary function, which accepts the operation used to convert markdown
+// into html.  Unfortunately there are currently no markdown parsers that
+// operate on a stream, but in the future I would like to switch to an
+// io.Reader interface.
+//
+// The operation begins by capturing the input path so that we can translate
+// the output path when creating files from the input path, including matching
+// directories.
+//
+// If no title has been supplied it will default to the parent directories
+// name, but this might be better placed in package main.
+//
+// The default output for web is `public/`, otherwise when in book mode the
+// default is the title.
+//
+// We walk the input path, which assembles the list of markdown files and then
+// we gather any errors returned.
+//
+// Finally we process the files according to the desired output mode.
+func (m *Markdown) Run(o operation) error {
+	var e error
+	if m.Input == "" {
+		if m.Input, e = os.Getwd(); e != nil {
+			m.errors(e)
+			return e
+		}
 	}
-
-	g.version = version(g.Input)
-	g.Input, _ = filepath.Abs(g.Input)
-	g.Output, _ = filepath.Abs(g.Output)
-	g.Input = filepath.Clean(g.Input)
-	g.Output = filepath.Clean(g.Output)
-
-	if err := walk(g.Input, g.walk); err != nil {
-		g.L.Error("%s\n", err)
-		return err
+	if m.Title == "" {
+		m.Title = filepath.Base(filepath.Dir(m.Input))
 	}
-	g.L.Debug("Markdown state: %+v", g)
-
-	if g.Book {
-		return g.single(run)
+	if m.Web && m.Output == "" {
+		m.Output = filepath.Join(m.Input, "public")
+	} else if m.Output == "" {
+		m.Output = filepath.Join(m.Input, m.Title+".html")
+	}
+	m.errors(filepath.Walk(m.Input, m.walk))
+	m.L.Debug("Status: %#v", m)
+	if m.Web {
+		m.errors(m.web(o))
+	} else {
+		m.errors(m.book(o))
 	}
-	return g.multi(run)
+	return m.err
 }

+ 29 - 186
markdown_test.go

@@ -1,207 +1,50 @@
 package static
 
 import (
-	"html/template"
 	"io"
+	"io/ioutil"
 	"os"
-	"path/filepath"
 	"testing"
-	"time"
 )
 
-var parseTemplate *template.Template
-var readfileError, templateError, createError, mkdirallError, parseError, walkError error
-var mockOperation = func(b []byte) []byte { return b }
-
-func init() {
-	readfile = func(_ string) ([]byte, error) { return nil, readfileError }
-	create = func(_ string) (*os.File, error) { return nil, createError }
-	mkdirall = func(_ string, _ os.FileMode) error { return mkdirallError }
-	parseFiles = func(...string) (*template.Template, error) { return parseTemplate, parseError }
-	walk = func(_ string, _ filepath.WalkFunc) error { return walkError }
-}
-
 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 templateError }
-
-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 := Markdown{}
-	if s := g.ior("some.md"); len(s) == 0 {
-		t.FailNow()
-	}
-}
-
-func TestDepth(t *testing.T) {
-	t.Parallel()
-	absp := "/abs/path/"
-	g := Markdown{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 := Markdown{}
-
-	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 := Markdown{L: &mockLogger{}, template: &mockTemplate{}, pages: []string{"fuck.md", "deeper/than/index.md", "deeper/than/data.md"}}
-
-	// set expected defaults
-	notExist = false
-	statError = nil
-
-	// no pages
-	if e := g.multi(mockOperation); e != nil {
-		t.FailNow()
-	}
-
-	// test full pass
-	if e := g.multi(mockOperation); e != nil {
-		t.FailNow()
-	}
-
-	// test full pass relative
-	g.Relative = true
-	if e := g.multi(mockOperation); e != nil {
-		t.FailNow()
-	}
-
-	// test failing execute
-	templateError = mockError
-	if e := g.multi(mockOperation); e == nil {
-		t.FailNow()
-	}
-
-	// test failing file creation
-	createError = mockError
-	if e := g.multi(mockOperation); e == nil {
-		t.FailNow()
-	}
-
-	// test failing to read the file
-	readfileError = mockError
-	if e := g.multi(mockOperation); e == nil {
-		t.FailNow()
-	}
-
-	// test dir creation failure
-	mkdirallError = mockError
-	statError = mockError
-	if e := g.multi(mockOperation); e == nil {
-		t.FailNow()
-	}
-}
-
-func TestSingle(t *testing.T) {
-	g := Markdown{L: &mockLogger{}, template: &mockTemplate{}, pages: []string{"fuck.md", "deeper/than/index.md", "deeper/than/data.md"}}
-
-	// reset expected defaults
-	statError = nil
-	readfileError = nil
-	createError = nil
-	templateError = nil
-
-	// test full pass
-	if e := g.single(mockOperation); e != nil {
-		t.FailNow()
-	}
+func (l *mockLogger) Debug(string, ...interface{}) {}
+func (l *mockLogger) Info(string, ...interface{})  {}
+func (l *mockLogger) Error(string, ...interface{}) {}
 
-	// test failing execute
-	templateError = mockError
-	if e := g.single(mockOperation); e == nil {
-		t.FailNow()
-	}
-
-	// test create error
-	createError = mockError
-	if e := g.single(mockOperation); e == nil {
-		t.FailNow()
-	}
+func TestMarkdown(t *testing.T) {
+	var files []*os.File
 
-	// test fail mkdirall
-	mkdirallError = mockError
-	statError = mockError
-	if e := g.single(mockOperation); e == nil {
-		t.FailNow()
+	// abstract behaviors
+	o := func(b []byte) []byte { return b }
+	readall = func(f io.Reader) ([]byte, error) { return []byte{}, nil }
+	mkdirall = func(d string, f os.FileMode) error { return nil }
+	create = func(n string) (*os.File, error) {
+		tfo, e := ioutil.TempFile(os.TempDir(), "static-out")
+		files = append(files, tfo)
+		return tfo, e
 	}
-
-	// test fail readfile
-	readfileError = mockError
-	if e := g.single(mockOperation); e == nil {
-		t.FailNow()
-	}
-}
-
-func TestGenerate(t *testing.T) {
-	g := Markdown{L: &mockLogger{}}
-
-	// set template for stand-alone execution
-	parseTemplate = template.New("test")
-
-	// test full pass
-	if e := g.Generate(mockOperation); e != nil {
-		t.FailNow()
+	open = func(n string) (*os.File, error) {
+		tfi, e := ioutil.TempFile(os.TempDir(), "static-in")
+		files = append(files, tfi)
+		return tfi, e
 	}
 
-	// test book mode full pass
-	g.Book = true
-	if e := g.Generate(mockOperation); e == nil {
-		t.FailNow()
+	// execute operation book mode
+	m := &Markdown{L: &mockLogger{}}
+	if e := m.Run(o); e != nil {
+		t.Error(e)
 	}
 
-	// test walk error
-	walkError = mockError
-	if e := g.Generate(mockOperation); e == nil {
-		t.FailNow()
+	// execute operation web mode
+	m.Web = true
+	if e := m.Run(o); e != nil {
+		t.Error(e)
 	}
 
-	// test template error
-	parseError = mockError
-	if e := g.Generate(mockOperation); e == nil {
-		t.FailNow()
+	// remove all temporary files
+	for i := range files {
+		os.Remove(files[i].Name())
 	}
 }

+ 0 - 6
navigation.go

@@ -1,6 +0,0 @@
-package static
-
-type navigation struct {
-	Link  string
-	Title string
-}

+ 0 - 11
page.go

@@ -1,11 +0,0 @@
-package static
-
-import "html/template"
-
-type page struct {
-	Name    string
-	Version string
-	Nav     []navigation
-	Depth   string
-	Content template.HTML
-}

+ 12 - 66
static.go

@@ -1,68 +1,14 @@
+// This package provides a utility to parse markdown files in lexical order
+// as a single document or individual pages, with no additional requirements.
+//
+// It includes default templates that are embedded, but can be directed to a
+// separate file granting better control for more complex use-cases.
+//
+// Template parameters are simple, and include Title, Content, and Version;
+// both the Version and Title can be changed.  If in web mode, an additional
+// property called Name will be set to the basename of the file.
 package static
 
-import (
-	"os"
-	"os/exec"
-	"path/filepath"
-	"strconv"
-	"strings"
-	"time"
-)
-
-func init() {
-	runnable = cmd{}
-}
-
-type logger interface {
-	Debug(string, ...interface{})
-	Error(string, ...interface{})
-	Info(string, ...interface{})
-}
-
-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{}
-
-func (self cmd) Run(command string, args ...string) ([]byte, error) {
-	return exec.Command(command, args...).Output()
-}
-
-func exists(path string) (bool, error) {
-	_, err := stat(path)
-	if err == nil {
-		return true, nil
-	}
-	if isNotExist(err) {
-		return false, nil
-	}
-	return false, err
-}
-
-func version(dir string) string {
-	version := strconv.FormatInt(time.Now().Unix(), 10)
-	out, err := runnable.Run("sh", "-c", "git", "-C", dir, "rev-parse", "--short", "HEAD")
-	if err == nil {
-		version = strings.Trim(string(out), "\n")
-	}
-	return version
-}
-
-func basename(name string) string {
-	return filepath.Base(strings.TrimSuffix(name, filepath.Ext(name)))
-}
-
-func isMarkdown(path string) bool {
-	for i := range extensions {
-		if strings.HasSuffix(path, extensions[i]) {
-			return true
-		}
-	}
-	return false
-}
+// List of extensions matching the github parser, but with an inversed order
+// so that we pick the shortest extension first.
+var extensions = []string{".md", ".mkd", ".mkdn", ".mdown", ".markdown"}

+ 1 - 104
static_test.go

@@ -1,108 +1,5 @@
 package static
 
-import (
-	"errors"
-	"os"
-	"testing"
-)
-
-func init() {
-	runnable = mockCmd{}
-	stat = func(_ string) (os.FileInfo, error) { return nil, statError }
-	isNotExist = func(_ error) bool { return notExist }
-}
-
-var mockError = errors.New("mock error")
-
-var mockCmdByteArray []byte
-var mockCmdError error
-var statError error
-var notExist bool
-
-type mockCmd struct{}
-
-func (self mockCmd) Run(command string, args ...string) ([]byte, error) {
-	return mockCmdByteArray, mockCmdError
-}
+import "testing"
 
 func TestPlacebo(_ *testing.T) {}
-
-func TestCmd(t *testing.T) {
-	t.Parallel()
-	c := cmd{}
-	if _, e := c.Run(""); e == nil {
-		t.FailNow()
-	}
-}
-
-func TestExists(t *testing.T) {
-
-	// set expected defaults
-	notExist = false
-	statError = nil
-
-	// test stat success exists fail
-	if _, e := exists(""); e != nil {
-		t.FailNow()
-	}
-
-	// test stat success exists success
-	notExist = true
-	if _, e := exists(""); e != nil {
-		t.FailNow()
-	}
-
-	// test stat fail
-	statError = mockError
-	if _, e := exists(""); e != nil {
-		t.FailNow()
-	}
-
-	// test stat fail exists success
-	notExist = false
-	if _, e := exists(""); e == nil {
-		t.FailNow()
-	}
-}
-
-func TestVersion(t *testing.T) {
-	t.Parallel()
-
-	// test with byte array
-	compare := "newp"
-	mockCmdByteArray = []byte(compare)
-	if v := version(""); v != compare {
-		t.FailNow()
-	}
-
-	// test with error
-	mockCmdError = mockError
-	if v := version(""); v == compare || len(v) == 0 {
-		t.FailNow()
-	}
-}
-
-func TestBasename(t *testing.T) {
-	t.Parallel()
-
-	f := "/some/long/path"
-	if o := basename(f); len(o) == 0 {
-		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()
-	}
-}

Різницю між файлами не показано, бо вона завелика
+ 71 - 0
templates.go


+ 1 - 1
cmd/smd/templates/book.tmpl → templates/book.tmpl

@@ -237,7 +237,7 @@
 		<div class="content">{{.Content}}</div>
 
 		<footer>
-			<p>Created on {{.Date}}{{if .Version}} ({{.Version}}){{end}}</p>
+			<p>{{if .Version}}{{.Version}}{{end}}</p>
 			<div class="scripts">
 				<script>
 					// remove absolute path links

+ 1 - 5
cmd/smd/templates/web.tmpl → templates/web.tmpl

@@ -2,7 +2,7 @@
 <html lang="en">
 	<head>
 		<meta charset="utf-8">
-		<title>{{ if .Name}}{{.Name}} | {{End}}{{.Title}}</title>
+		<title>{{if .Name}}{{.Name}} | {{end}}{{.Title}}</title>
 		<style>
 
 			/**
@@ -233,9 +233,5 @@
 		</header>
 
 		<div class="content">{{.Content}}</div>
-
-		<footer>
-			<p>Created on {{.Date}}{{if .Version}} ({{.Version}}){{end}}</p>
-		</footer>
 	</body>
 </html>

Деякі файли не було показано, через те що забагато файлів було змінено