staticmd.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. package main
  2. import (
  3. "bufio"
  4. "html/template"
  5. "io/ioutil"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "sync"
  10. "github.com/russross/blackfriday"
  11. "github.com/cdelorme/go-log"
  12. )
  13. type Staticmd struct {
  14. Logger log.Logger
  15. Input string
  16. Output string
  17. Template template.Template
  18. Book bool
  19. Relative bool
  20. MaxParallelism int
  21. Version string
  22. Pages []string
  23. }
  24. // convert markdown input path to html output path
  25. // there is no reverse (because we support `.md`, `.mkd`, and `.markdown`)
  26. func (staticmd *Staticmd) ior(path string) string {
  27. return strings.TrimSuffix(strings.Replace(path, staticmd.Input, staticmd.Output, 1), filepath.Ext(path)) + ".html"
  28. }
  29. // for relative rendering generate relative depth string, or fallback to empty
  30. func (staticmd *Staticmd) depth(path string) string {
  31. if staticmd.Relative {
  32. if rel, err := filepath.Rel(filepath.Dir(path), staticmd.Output); err == nil {
  33. return rel+string(os.PathSeparator)
  34. }
  35. }
  36. return ""
  37. }
  38. // walk the directories and build a list of pages
  39. func (staticmd *Staticmd) Walk(path string, file os.FileInfo, err error) error {
  40. // only pay attention to files with a size greater than 0
  41. if file == nil {
  42. } else if file.Mode().IsRegular() && file.Size() > 0 {
  43. // only add markdown files to our pages array (.md, .mkd, .markdown)
  44. if strings.HasSuffix(path, ".md") || strings.HasSuffix(path, ".mkd") || strings.HasSuffix(path, ".markdown") {
  45. staticmd.Pages = append(staticmd.Pages, path)
  46. }
  47. }
  48. return err
  49. }
  50. // build output concurrently to many pages
  51. func (staticmd *Staticmd) Multi() {
  52. // prepare navigation storage
  53. navigation := make(map[string][]Navigation)
  54. // loop pages to build table of contents
  55. for i, _ := range staticmd.Pages {
  56. // build output directory
  57. out := staticmd.ior(staticmd.Pages[i])
  58. dir := filepath.Dir(staticmd.ior(out))
  59. // create navigation object
  60. nav := Navigation{}
  61. // sub-index condition changes name, dir, and link
  62. if filepath.Dir(out) != staticmd.Output && strings.ToLower(basename(out)) == "index" {
  63. // set name to containing folder
  64. nav.Name = basename(dir)
  65. // set relative or absolute link
  66. if staticmd.Relative {
  67. nav.Link = filepath.Join(strings.TrimPrefix(dir, filepath.Dir(dir)+string(os.PathSeparator)), filepath.Base(out))
  68. } else {
  69. nav.Link = strings.TrimPrefix(dir, staticmd.Output)+string(os.PathSeparator)
  70. }
  71. // update dir to dir of dir
  72. dir = filepath.Dir(dir)
  73. } else {
  74. // set name to files name
  75. nav.Name = basename(out)
  76. // set relative or absolute link
  77. if staticmd.Relative {
  78. nav.Link = strings.TrimPrefix(out, filepath.Dir(out)+string(os.PathSeparator))
  79. } else {
  80. nav.Link = strings.TrimPrefix(out, staticmd.Output)
  81. }
  82. }
  83. // build indexes first-match
  84. if _, ok := navigation[dir]; !ok {
  85. navigation[dir] = make([]Navigation, 0)
  86. // create output directory for when we create files
  87. if ok, _ := exists(dir); !ok {
  88. if err := os.MkdirAll(dir, 0770); err != nil {
  89. staticmd.Logger.Error("Failed to create path: %s, %s", dir, err)
  90. }
  91. }
  92. }
  93. // append to navigational list
  94. navigation[dir] = append(navigation[dir], nav)
  95. }
  96. // debug navigation output
  97. staticmd.Logger.Debug("navigation: %+v", navigation)
  98. // prepare waitgroup, bufferer channel, and add number of async handlers to wg
  99. var wg sync.WaitGroup
  100. pages := make(chan string, staticmd.MaxParallelism)
  101. wg.Add(staticmd.MaxParallelism)
  102. // prepare workers
  103. for i := 0; i < staticmd.MaxParallelism; i++ {
  104. go func() {
  105. defer wg.Done()
  106. // iterate supplied pages
  107. for p := range pages {
  108. // acquire output filepath
  109. out := staticmd.ior(p)
  110. dir := filepath.Dir(out)
  111. // prepare a new page object for our template to render
  112. page := Page{
  113. Name: basename(p),
  114. Version: staticmd.Version,
  115. Nav: navigation[staticmd.Output],
  116. Depth: staticmd.depth(out),
  117. }
  118. // read in page text
  119. if markdown, err := ioutil.ReadFile(p); err == nil {
  120. // if this page happens to be a sub-index we can generate the table of contents
  121. if dir != staticmd.Output && strings.ToLower(basename(p)) == "index" {
  122. // iterate and build table of contents as basic markdown
  123. toc := "\n## Table of Contents:\n\n"
  124. for i, _ := range navigation[dir] {
  125. toc = toc + "- [" + navigation[dir][i].Name + "](" + navigation[dir][i].Link + ")\n"
  126. }
  127. // debug table of contents output
  128. staticmd.Logger.Debug("table of contents for %s, %s", out, toc)
  129. // prepend table of contents
  130. markdown = append([]byte(toc), markdown...)
  131. }
  132. page.Content = template.HTML(blackfriday.MarkdownCommon(markdown))
  133. } else {
  134. staticmd.Logger.Error("failed to read file: %s, %s", p, err)
  135. }
  136. // debug page output
  137. staticmd.Logger.Debug("page: %+v", page)
  138. // translate input path to output path & create a write context
  139. if f, err := os.Create(out); err == nil {
  140. defer f.Close()
  141. // prepare a writer /w buffer
  142. fb := bufio.NewWriter(f)
  143. defer fb.Flush()
  144. // attempt to use template to write output with page context
  145. if e := staticmd.Template.Execute(fb, page); e != nil {
  146. staticmd.Logger.Error("Failed to write template: %s, %s", out, e)
  147. }
  148. } else {
  149. staticmd.Logger.Error("failed to create new file: %s, %s", out, err)
  150. }
  151. }
  152. }()
  153. }
  154. // send pages to workers for async rendering
  155. for _, page := range staticmd.Pages {
  156. pages <- page
  157. }
  158. // close channel and wait for async to finish before continuing
  159. close(pages)
  160. wg.Wait()
  161. }
  162. // build output synchronously to a single page
  163. func (staticmd *Staticmd) Single() {
  164. // still determining strategy
  165. }