markdown.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. //go:generate go-bindata -pkg static -o templates.go templates/
  2. package static
  3. import (
  4. "html/template"
  5. "io/ioutil"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. )
  10. var readall = ioutil.ReadAll
  11. var open = os.Open
  12. var create = os.Create
  13. var mkdirall = os.MkdirAll
  14. type logger interface {
  15. Info(string, ...interface{})
  16. Debug(string, ...interface{})
  17. Error(string, ...interface{})
  18. }
  19. type operation func([]byte) []byte
  20. // This is the compiler that collects the list markdown files, a title, the
  21. // input and output paths, and whether to produce multiple files (web mode) or
  22. // to produce a single file (default, book mode).
  23. //
  24. // All public properties are not thread safe, so concurrent execution may yield
  25. // errors if those properties are being modified or accessed in parallel.
  26. type Markdown struct {
  27. Title string `json:"title,omitempty"`
  28. Input string `json:"input,omitempty"`
  29. Output string `json:"output,omitempty"`
  30. Web bool `json:"web,omitempty"`
  31. Template string `json:"template,omitempty"`
  32. Version string `json:"version,omitempty"`
  33. L logger `json:"-"`
  34. err error
  35. files []string
  36. }
  37. // This function helps us handle any errors encountered during processing
  38. // without forcing us to immediately terminate.
  39. //
  40. // It also is responsible for logging every error encountered.
  41. //
  42. // If the error is nil it ignores it, thus the last non-nil error will be
  43. // returned, so that the caller knows at least one failure has occurred.
  44. func (m *Markdown) errors(err error) {
  45. if err == nil {
  46. return
  47. }
  48. m.L.Error(err.Error())
  49. m.err = err
  50. }
  51. // If the absolute path minus the file extension already exist then we want to
  52. // know so we can avoid redundant processing, overwriting files, and potential
  53. // race conditions.
  54. func (m *Markdown) matches(file string) bool {
  55. for i := range m.files {
  56. if strings.TrimSuffix(file, filepath.Ext(file)) == strings.TrimSuffix(m.files[i], filepath.Ext(m.files[i])) {
  57. return true
  58. }
  59. }
  60. return false
  61. }
  62. // This checks the extension against a list of supported extensions.
  63. func (m *Markdown) valid(file string) bool {
  64. for i := range extensions {
  65. if filepath.Ext(file) == extensions[i] {
  66. return true
  67. }
  68. }
  69. return false
  70. }
  71. // When walking through files we collect errors but do not return them, so that
  72. // the entire operation is not canceled due to a single failure.
  73. //
  74. // If there is an error, the file is a directory, the file is irregular, the
  75. // file does not have a markdown extension, or the file name minus its
  76. // extention is already in our list, then we skip that file.
  77. //
  78. // Thus the first file matched is the only file processed, which is to deal
  79. // with multiple valid markdown extensions for the same file basename.
  80. //
  81. // Each verified file is added to the list of files, which we will process
  82. // after we finish iterating all files.
  83. func (m *Markdown) walk(file string, f os.FileInfo, e error) error {
  84. m.errors(e)
  85. if e != nil || f.IsDir() || !f.Mode().IsRegular() || f.Size() == 0 || !m.valid(file) || m.matches(file) {
  86. return nil
  87. }
  88. m.files = append(m.files, file)
  89. return nil
  90. }
  91. // A way to abstract the process of getting a template
  92. func (m *Markdown) template() (*template.Template, error) {
  93. if m.Template != "" {
  94. return template.ParseFiles(m.Template)
  95. }
  96. var assetFile = "templates/book.tmpl"
  97. if m.Web {
  98. assetFile = "templates/web.tmpl"
  99. }
  100. d, e := Asset(assetFile)
  101. if e != nil {
  102. return nil, e
  103. }
  104. t := template.New("markdown")
  105. return t.Parse(string(d))
  106. }
  107. // This operation processes each file independently, which includes passing to
  108. // each its own page structure.
  109. //
  110. // In the future, when buffered markdown parsers exist, this should leverage
  111. // concurrency, but the current implementation is bottlenecked at disk IO.
  112. //
  113. // The template is created first, using the compiled bindata by default, or the
  114. // supplied template file if able.
  115. func (m *Markdown) web(o operation) error {
  116. t, e := m.template()
  117. if e != nil {
  118. return e
  119. }
  120. for i := range m.files {
  121. in, e := open(m.files[i])
  122. if e != nil {
  123. m.errors(e)
  124. continue
  125. }
  126. b, e := readall(in)
  127. m.errors(in.Close())
  128. if e != nil {
  129. m.errors(e)
  130. continue
  131. }
  132. d := o(b)
  133. 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))
  134. out, e := create(filepath.Join(m.Output, strings.TrimSuffix(strings.TrimPrefix(m.files[i], m.Input), filepath.Ext(m.files[i]))+".html"))
  135. if e != nil {
  136. m.errors(e)
  137. continue
  138. }
  139. if e := t.Execute(out, struct {
  140. Title string
  141. Name string
  142. Content template.HTML
  143. Version string
  144. }{
  145. Content: template.HTML(string(d)),
  146. Title: m.Title,
  147. Name: strings.TrimSuffix(filepath.Base(m.files[i]), filepath.Ext(m.files[i])),
  148. Version: m.Version,
  149. }); e != nil {
  150. m.errors(e)
  151. }
  152. m.errors(out.Close())
  153. }
  154. return nil
  155. }
  156. // This operation processes each file sequentially, and keeps the bytes for all
  157. // files in memory so it can write the output to a single file.
  158. //
  159. // In the future, it would be best if each file were loaded into a buffered
  160. // markdown parser and piped to a buffered template so that the system could
  161. // avoid storing all bytes in memory.
  162. //
  163. // Once every file has been loaded into a single byte array, we run it through
  164. // the markdown processor `operation`, and pass that into a template which then
  165. // pushes the output to a single file.
  166. func (m *Markdown) book(o operation) error {
  167. t, e := m.template()
  168. if e != nil {
  169. return e
  170. }
  171. var b []byte
  172. for i := range m.files {
  173. in, e := open(m.files[i])
  174. if e != nil {
  175. m.errors(e)
  176. continue
  177. }
  178. d, e := readall(in)
  179. m.errors(in.Close())
  180. if e != nil {
  181. m.errors(e)
  182. continue
  183. }
  184. b = append(b, d...)
  185. }
  186. d := o(b)
  187. m.errors(mkdirall(filepath.Dir(m.Output), os.ModePerm))
  188. out, e := create(m.Output)
  189. if e != nil {
  190. return e
  191. }
  192. defer out.Close()
  193. return t.Execute(out, struct {
  194. Title string
  195. Content template.HTML
  196. Version string
  197. }{
  198. Content: template.HTML(string(d)),
  199. Title: m.Title,
  200. Version: m.Version,
  201. })
  202. }
  203. // The primary function, which accepts the operation used to convert markdown
  204. // into html. Unfortunately there are currently no markdown parsers that
  205. // operate on a stream, but in the future I would like to switch to an
  206. // io.Reader interface.
  207. //
  208. // The operation begins by capturing the input path so that we can translate
  209. // the output path when creating files from the input path, including matching
  210. // directories.
  211. //
  212. // If no title has been supplied it will default to the parent directories
  213. // name, but this might be better placed in package main.
  214. //
  215. // The default output for web is `public/`, otherwise when in book mode the
  216. // default is the title.
  217. //
  218. // We walk the input path, which assembles the list of markdown files and then
  219. // we gather any errors returned.
  220. //
  221. // Finally we process the files according to the desired output mode.
  222. func (m *Markdown) Run(o operation) error {
  223. var e error
  224. if m.Input == "" {
  225. if m.Input, e = os.Getwd(); e != nil {
  226. m.errors(e)
  227. return e
  228. }
  229. }
  230. if m.Title == "" {
  231. m.Title = filepath.Base(filepath.Dir(m.Input))
  232. }
  233. if m.Web && m.Output == "" {
  234. m.Output = filepath.Join(m.Input, "public")
  235. } else if m.Output == "" {
  236. m.Output = filepath.Join(m.Input, m.Title+".html")
  237. }
  238. m.errors(filepath.Walk(m.Input, m.walk))
  239. m.L.Debug("Status: %#v", m)
  240. if m.Web {
  241. m.errors(m.web(o))
  242. } else {
  243. m.errors(m.book(o))
  244. }
  245. return m.err
  246. }