staticmd.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  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. func (staticmd *Staticmd) ior(path string) string {
  26. return strings.TrimSuffix(strings.Replace(path, staticmd.Input, staticmd.Output, 1), filepath.Ext(path)) + ".html"
  27. }
  28. // for relative rendering generate relative depth string, or fallback to empty
  29. func (staticmd *Staticmd) depth(path string) string {
  30. if staticmd.Relative {
  31. if rel, err := filepath.Rel(filepath.Dir(path), staticmd.Output); err == nil {
  32. return rel
  33. }
  34. }
  35. return ""
  36. }
  37. // get link to file, with support for relative path linking
  38. func (staticmd *Staticmd) link(path string) string {
  39. if staticmd.Relative {
  40. return strings.TrimPrefix(path, filepath.Dir(path))
  41. }
  42. return strings.TrimPrefix(path, staticmd.Output)
  43. }
  44. // walk the directories and build a list of pages
  45. func (staticmd *Staticmd) Walk(path string, file os.FileInfo, err error) error {
  46. // only pay attention to files with a size greater than 0
  47. if file == nil {
  48. } else if file.Mode().IsRegular() && file.Size() > 0 {
  49. // only add markdown files to our pages array (.md, .mkd, .markdown)
  50. if strings.HasSuffix(path, ".md") || strings.HasSuffix(path, ".mkd") || strings.HasSuffix(path, ".markdown") {
  51. staticmd.Pages = append(staticmd.Pages, path)
  52. }
  53. }
  54. return err
  55. }
  56. // build output concurrently to many pages
  57. func (staticmd *Staticmd) Multi() {
  58. // prepare navigation storage
  59. navigation := make(map[string][]Navigation)
  60. // loop pages to build table of contents
  61. for i, _ := range staticmd.Pages {
  62. // build output directory
  63. out := staticmd.ior(staticmd.Pages[i])
  64. dir := filepath.Dir(staticmd.ior(out))
  65. // build indexes first-match
  66. if _, ok := navigation[dir]; !ok {
  67. navigation[dir] = make([]Navigation, 0)
  68. // create output directory for when we create files
  69. if ok, _ := exists(dir); !ok {
  70. if err := os.MkdirAll(dir, 0770); err != nil {
  71. staticmd.Logger.Error("Failed to create path: %s, %s", dir, err)
  72. }
  73. }
  74. }
  75. // create a new navigation object
  76. nav := Navigation{
  77. Name: basename(out),
  78. Link: staticmd.link(out),
  79. }
  80. // append files to their respective directories
  81. navigation[dir] = append(navigation[dir], nav)
  82. }
  83. // @todo second cycle to clarify whether index or readme for table-of-contents
  84. // debug output
  85. staticmd.Logger.Debug("Navigation: %+v", navigation)
  86. // prepare waitgroup, bufferer channel, and add number of async handlers to wg
  87. var wg sync.WaitGroup
  88. pages := make(chan string, staticmd.MaxParallelism)
  89. wg.Add(staticmd.MaxParallelism)
  90. // prepare workers
  91. for i := 0; i < staticmd.MaxParallelism; i++ {
  92. go func() {
  93. defer wg.Done()
  94. // iterate supplied pages
  95. for p := range pages {
  96. // acquire output filepath
  97. out := staticmd.ior(p)
  98. // dir := filepath.Dir(out)
  99. // prepare a new page object for our template to render
  100. page := Page{
  101. Name: basename(p),
  102. Version: staticmd.Version,
  103. Nav: navigation[staticmd.Output],
  104. Depth: staticmd.depth(p),
  105. }
  106. // read in page text
  107. if markdown, err := ioutil.ReadFile(p); err == nil {
  108. // conditionally prepend table of contents?
  109. page.Content = template.HTML(blackfriday.MarkdownCommon(markdown))
  110. } else {
  111. staticmd.Logger.Error("failed to read file: %s, %s", p, err)
  112. }
  113. // debug output
  114. staticmd.Logger.Debug("Page: %+v", page)
  115. // translate input path to output path & create a write context
  116. if f, err := os.Create(out); err == nil {
  117. defer f.Close()
  118. // prepare a writer /w buffer
  119. fb := bufio.NewWriter(f)
  120. defer fb.Flush()
  121. // attempt to use template to write output with page context
  122. if e := staticmd.Template.Execute(fb, page); e != nil {
  123. staticmd.Logger.Error("Failed to write template: %s, %s", out, e)
  124. }
  125. } else {
  126. staticmd.Logger.Error("failed to create new file: %s, %s", out, err)
  127. }
  128. }
  129. }()
  130. }
  131. // send pages to workers for async rendering
  132. for _, page := range staticmd.Pages {
  133. pages <- page
  134. }
  135. // close channel and wait for async to finish before continuing
  136. close(pages)
  137. wg.Wait()
  138. }
  139. // build output synchronously to a single page
  140. func (staticmd *Staticmd) Single() {
  141. // still determining strategy
  142. }