staticmd.go 8.8 KB

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