123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- //go:generate go-bindata -pkg static -o templates.go templates/
- package static
- import (
- "html/template"
- "io/ioutil"
- "os"
- "path/filepath"
- "strings"
- )
- var readall = ioutil.ReadAll
- var open = os.Open
- var create = os.Create
- var mkdirall = os.MkdirAll
- 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 {
- 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
- }
- // 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
- }
- // 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 false
- }
- // 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 false
- }
- // 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
- }
- // 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))
- }
- // 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
- }
- b, e := readall(in)
- m.errors(in.Close())
- if e != nil {
- m.errors(e)
- continue
- }
- 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
- }
- 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 nil
- }
- // 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
- }
- d, e := readall(in)
- m.errors(in.Close())
- if e != nil {
- m.errors(e)
- continue
- }
- b = append(b, d...)
- }
- d := o(b)
- m.errors(mkdirall(filepath.Dir(m.Output), os.ModePerm))
- out, e := create(m.Output)
- if e != nil {
- return e
- }
- 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,
- })
- }
- // 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
- }
- }
- if m.Title == "" {
- m.Title = filepath.Base(filepath.Dir(m.Input))
- }
- 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 m.err
- }
|