generator.go 8.3 KB

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