generator.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. package staticmd
  2. import (
  3. "bufio"
  4. "html/template"
  5. "io"
  6. "io/ioutil"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "github.com/russross/blackfriday"
  11. )
  12. var readfile = ioutil.ReadFile
  13. var create = os.Create
  14. var mkdirall = os.MkdirAll
  15. var parseFiles = template.ParseFiles
  16. var walk = filepath.Walk
  17. type ht interface {
  18. Execute(io.Writer, interface{}) error
  19. }
  20. type Generator struct {
  21. Input string
  22. Output string
  23. TemplateFile string
  24. Book bool
  25. Relative bool
  26. Logger logger
  27. version string
  28. pages []string
  29. template ht
  30. }
  31. // convert markdown input path to html output path
  32. // @note: we cannot reverse since we do not track the extension
  33. // we support `.md`, `.mkd`, and `.markdown`
  34. func (self *Generator) ior(path string) string {
  35. return strings.TrimSuffix(strings.Replace(path, self.Input, self.Output, 1), filepath.Ext(path)) + ".html"
  36. }
  37. // for relative rendering generate relative depth string, or fallback to empty
  38. func (self *Generator) depth(path string) string {
  39. if self.Relative {
  40. if rel, err := filepath.Rel(filepath.Dir(path), self.Output); err == nil {
  41. return rel + string(os.PathSeparator)
  42. }
  43. }
  44. return ""
  45. }
  46. // walk the directories and build a list of markdown files with content
  47. func (self *Generator) walk(path string, file os.FileInfo, err error) error {
  48. if file != nil && file.Mode().IsRegular() && file.Size() > 0 && isMarkdown(path) {
  49. self.pages = append(self.pages, path)
  50. }
  51. return err
  52. }
  53. // build output concurrently to many pages
  54. func (self *Generator) multi() (err error) {
  55. // prepare navigation storage
  56. navi := make(map[string][]navigation)
  57. // loop pages to build table of contents
  58. for i, _ := range self.pages {
  59. // build output directory
  60. out := self.ior(self.pages[i])
  61. dir := filepath.Dir(self.ior(out))
  62. // create navigation object
  63. nav := navigation{}
  64. // sub-index condition changes name, dir, and link
  65. if filepath.Dir(out) != self.Output && strings.ToLower(basename(out)) == "index" {
  66. // set name to containing folder
  67. nav.Title = basename(dir)
  68. // set relative or absolute link
  69. if self.Relative {
  70. nav.Link = filepath.Join(strings.TrimPrefix(dir, filepath.Dir(dir)+string(os.PathSeparator)), filepath.Base(out))
  71. } else {
  72. nav.Link = strings.TrimPrefix(dir, self.Output) + string(os.PathSeparator)
  73. }
  74. // update dir to dir of dir
  75. dir = filepath.Dir(dir)
  76. } else {
  77. // set name to files name
  78. nav.Title = basename(out)
  79. // set relative or absolute link
  80. if self.Relative {
  81. nav.Link = strings.TrimPrefix(out, filepath.Dir(out)+string(os.PathSeparator))
  82. } else {
  83. nav.Link = strings.TrimPrefix(out, self.Output)
  84. }
  85. }
  86. // build indexes first-match
  87. if _, ok := navi[dir]; !ok {
  88. navi[dir] = make([]navigation, 0)
  89. // create output directory for when we create files
  90. if ok, _ := exists(dir); !ok {
  91. if err = mkdirall(dir, 0770); err != nil {
  92. self.Logger.Error("Failed to create path: %s, %s", dir, err)
  93. }
  94. }
  95. }
  96. // append to navigational list
  97. navi[dir] = append(navi[dir], nav)
  98. }
  99. // process all pages
  100. for _, p := range self.pages {
  101. // attempt to read entire document
  102. var markdown []byte
  103. if markdown, err = readfile(p); err != nil {
  104. self.Logger.Error("failed to read file: %s, %s", p, err)
  105. return
  106. }
  107. // acquire output filepath
  108. out := self.ior(p)
  109. dir := filepath.Dir(out)
  110. // prepare a new page object for our template to render
  111. page := page{
  112. Name: basename(p),
  113. Version: self.version,
  114. Nav: navi[self.Output],
  115. Depth: self.depth(out),
  116. }
  117. // if this page happens to be a sub-index we can generate the table of contents
  118. if dir != self.Output && strings.ToLower(basename(p)) == "index" {
  119. // iterate and build table of contents as basic markdown
  120. toc := "\n## Table of Contents:\n\n"
  121. for i, _ := range navi[dir] {
  122. toc = toc + "- [" + navi[dir][i].Title + "](" + navi[dir][i].Link + ")\n"
  123. }
  124. // debug: table of contents output
  125. self.Logger.Debug("table of contents for %s, %s", out, toc)
  126. // prepend table of contents
  127. markdown = append([]byte(toc), markdown...)
  128. }
  129. // convert to html, and accept as part of the template
  130. page.Content = template.HTML(blackfriday.MarkdownCommon(markdown))
  131. // attempt to open file for output
  132. var f *os.File
  133. if f, err = create(out); err != nil {
  134. return err
  135. }
  136. defer f.Close()
  137. // prepare a writer /w buffer
  138. fb := bufio.NewWriter(f)
  139. defer fb.Flush()
  140. // attempt to use template to write output with page context
  141. err = self.template.Execute(fb, page)
  142. }
  143. return
  144. }
  145. // build output synchronously to a single page
  146. func (self *Generator) single() (err error) {
  147. // prepare []byte array to store all files markdown
  148. content := make([]byte, 0)
  149. // prepare a table-of-contents
  150. toc := "\n"
  151. previous_depth := 0
  152. // iterate and append all files contents
  153. for _, p := range self.pages {
  154. // shorthand
  155. shorthand := strings.TrimPrefix(p, self.Input+string(os.PathSeparator))
  156. // acquire depth
  157. depth := strings.Count(shorthand, string(os.PathSeparator))
  158. // if depth > previous depth then prepend with basename of dir for sub-section-headings
  159. if depth > previous_depth {
  160. toc = toc + strings.Repeat("\t", depth-1) + "- " + basename(filepath.Dir(p)) + "\n"
  161. }
  162. // prepare anchor text
  163. anchor := strings.Replace(shorthand, string(os.PathSeparator), "-", -1)
  164. // create new toc record
  165. toc = toc + strings.Repeat("\t", depth) + "- [" + basename(p) + "](#" + anchor + ")\n"
  166. // read markdown from file or skip to next file
  167. var markdown []byte
  168. markdown, err = readfile(p)
  169. if err != nil {
  170. self.Logger.Error("failed to read file: %s (%s)", p, err)
  171. continue
  172. }
  173. // prepend anchor to content
  174. markdown = append([]byte("\n<a id='"+anchor+"'/>\n\n"), markdown...)
  175. // append a "back-to-top" anchor
  176. markdown = append(markdown, []byte("\n[back to top](#top)\n\n")...)
  177. // append to content
  178. content = append(content, markdown...)
  179. // update depth
  180. previous_depth = depth
  181. }
  182. // prepend toc
  183. content = append([]byte(toc), content...)
  184. // prepare output directory
  185. if ok, _ := exists(self.Output); !ok {
  186. if err = mkdirall(self.Output, 0770); err != nil {
  187. return err
  188. }
  189. }
  190. // create page object with version & content
  191. page := page{
  192. Version: self.version,
  193. Content: template.HTML(blackfriday.MarkdownCommon(content)),
  194. }
  195. // prepare output file path
  196. out := filepath.Join(self.Output, "index.html")
  197. // attempt to open file for output
  198. var f *os.File
  199. if f, err = create(out); err != nil {
  200. return err
  201. }
  202. defer f.Close()
  203. // prepare a writer /w buffer
  204. fb := bufio.NewWriter(f)
  205. defer fb.Flush()
  206. // attempt to use template to write output with page context
  207. err = self.template.Execute(fb, page)
  208. return
  209. }
  210. func (self *Generator) Generate() error {
  211. // process template
  212. var err error
  213. if self.template, err = parseFiles(self.TemplateFile); err != nil {
  214. return err
  215. }
  216. // acquire version
  217. self.version = version(self.Input)
  218. // sanitize input & output
  219. self.Input, _ = filepath.Abs(self.Input)
  220. self.Output, _ = filepath.Abs(self.Output)
  221. // sanitize & validate properties
  222. self.Input = filepath.Clean(self.Input)
  223. self.Output = filepath.Clean(self.Output)
  224. // walk the file system
  225. if err := walk(self.Input, self.walk); err != nil {
  226. return err
  227. }
  228. // debug: print state
  229. self.Logger.Debug("generator state: %+v", self)
  230. // determine assembly method
  231. if self.Book {
  232. return self.single()
  233. }
  234. return self.multi()
  235. }