my CMS/Blog engine
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

gen.go 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. package main
  2. /*
  3. * This is freesofware under 2-clause BSD license, See LICENSE file
  4. * (C)opyright 2018,2019 juju
  5. */
  6. import (
  7. "bufio"
  8. "bytes"
  9. "fmt"
  10. "html/template"
  11. "io"
  12. "io/ioutil"
  13. "os"
  14. "path"
  15. "path/filepath"
  16. "regexp"
  17. "sort"
  18. "strings"
  19. "time"
  20. "git.universelle.science/juju/amber"
  21. "github.com/russross/blackfriday"
  22. )
  23. // Site structure : FOLDER
  24. type FOLDER struct {
  25. Site *Site
  26. Path string // part of path relatif to SRC and OUT
  27. Name string // navigation name
  28. outfiles []os.FileInfo // (extra) files in Out
  29. srcfiles []os.FileInfo // Source files
  30. // SiteMap links
  31. Subdirs []*FOLDER // subdirectories
  32. Pages PAGES // pages in this folder
  33. index *PAGE // index page
  34. }
  35. // Site structure : Page
  36. type PAGE struct {
  37. Root *FOLDER // shortcut to root folder
  38. Folder *FOLDER // contening folder
  39. SrcName string // source .md file name
  40. DstName string // destination (slug) name
  41. PubTime time.Time
  42. ModTime time.Time
  43. Prev *PAGE
  44. Next *PAGE
  45. Up *PAGE
  46. Meta TemplateData
  47. Content template.HTML
  48. buf *bytes.Buffer
  49. }
  50. type PAGES []*PAGE
  51. // flatten SiteMap structure: topic
  52. type UrlEntry struct {
  53. EIndent int // Entry indent level
  54. Url string // URL
  55. Display string // Display name
  56. }
  57. // Site data
  58. type Site struct {
  59. RootFOLDER *FOLDER // root FOLDER
  60. SiteMap []UrlEntry
  61. }
  62. // return the full Out path
  63. func (folder *FOLDER) GetOutDir() string {
  64. return filepath.Join(PublicDir, folder.Path)
  65. }
  66. // return the full Src path
  67. func (folder *FOLDER) GetSrcDir() string {
  68. return filepath.Join(PostsDir, folder.Path)
  69. }
  70. var (
  71. //postTpl *template.Template // The one and only compiled post template
  72. postTpls map[string]*template.Template // [templateName]=*compiledTemplate
  73. postTplNm = "post.amber" // The amber post template file name (native Go are compiled using ParseGlob)
  74. site = Site{}
  75. funcs = template.FuncMap{
  76. "fmttime": func(t time.Time, f string) string {
  77. return t.Format(f)
  78. },
  79. }
  80. )
  81. func init() {
  82. // Add the custom functions to Amber in the init(), since this is global
  83. // (package) state in my Amber fork.
  84. amber.AddFuncs(funcs)
  85. }
  86. // Sort pages in same folder
  87. func (p PAGES) Less(i, j int) bool { return p[i].PubTime.Before(p[j].PubTime) }
  88. func (p PAGES) Len() int { return len(p) }
  89. func (p PAGES) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
  90. // Compile the tempalte directory
  91. func compileTemplates() (err error) {
  92. tmptpl, err := amber.CompileDir(TemplatesDir, amber.DefaultDirOptions, amber.DefaultOptions)
  93. if err != nil {
  94. return
  95. }
  96. postTpls = tmptpl
  97. DEBUG("Directory compiled: %v", TemplatesDir)
  98. return nil
  99. }
  100. // scan a directory tree and generate outputs
  101. func genPath(dir string) {
  102. // copy all static assets first
  103. copyFolder(StaticDirs, PublicDir)
  104. site.RootFOLDER = FOLDERTree("/")
  105. site.RootFOLDER.BuildTree()
  106. site.BuildMap()
  107. }
  108. // Build a FOLDER tree from SRC directory tree
  109. func FOLDERTree(dir string) *FOLDER {
  110. folder := FOLDER{Site: &site, Path: dir, Name: filepath.Base(dir)}
  111. files, err := ioutil.ReadDir(folder.GetSrcDir())
  112. if err != nil {
  113. WARN(err.Error())
  114. return nil
  115. }
  116. // walk all files first
  117. for _, fi := range files {
  118. if fi.IsDir() {
  119. if subfolder := FOLDERTree(filepath.Join(folder.Path, fi.Name())); subfolder != nil {
  120. folder.Subdirs = append(folder.Subdirs, subfolder)
  121. }
  122. } else {
  123. folder.srcfiles = append(folder.srcfiles, fi)
  124. }
  125. }
  126. return &folder
  127. }
  128. // Build a simple navigation tree with ul/li
  129. func (site *Site) BuildMap() {
  130. site.SiteMap = []UrlEntry{}
  131. site.RootFOLDER.UrlEntries(0)
  132. }
  133. // apped TOC of current folder to flatten navigation TOC
  134. func (folder *FOLDER) UrlEntries(eident int) {
  135. // add pages first
  136. for _, pa := range folder.Pages {
  137. site.SiteMap = append(site.SiteMap, UrlEntry{EIndent: eident, Url: path.Join(folder.Path, pa.DstName), Display: pa.DstName})
  138. }
  139. // buil all page for current folder
  140. for _, fi := range folder.Subdirs {
  141. site.SiteMap = append(site.SiteMap, UrlEntry{EIndent: eident, Url: fi.Path + "/", Display: fi.Name + "/"})
  142. fi.UrlEntries(eident + 1)
  143. }
  144. }
  145. // Build the site from FOLDER
  146. func (folder *FOLDER) BuildTree() {
  147. // build all pages for current directory
  148. folder.PopulateOut()
  149. for _, fi := range folder.srcfiles {
  150. fname := fi.Name()
  151. if strings.HasPrefix(fi.Name(), ".") {
  152. // ignore hidden files
  153. continue
  154. }
  155. if matched, _ := regexp.MatchString(".*\\.md", fname); matched {
  156. folder.newPage(fname)
  157. } else {
  158. folder.copy(fname)
  159. }
  160. }
  161. // read metadata for all pages of current folder
  162. sort.Sort(PAGES(folder.Pages))
  163. // build sub-directories
  164. for _, fi := range folder.Subdirs {
  165. fi.BuildTree()
  166. }
  167. // buil all page for current folder
  168. for _, pa := range folder.Pages {
  169. folder.generateFile(pa, pa == folder.index)
  170. }
  171. // clean up
  172. folder.CleanOut()
  173. }
  174. // Copy a static file in Src to Out
  175. func (folder *FOLDER) copy(src string) {
  176. fsrc := filepath.Join(folder.GetSrcDir(), src)
  177. fdst := filepath.Join(folder.GetOutDir(), src)
  178. err := copyFile(fsrc, fdst)
  179. if err != nil {
  180. ERROR(err.Error())
  181. return
  182. }
  183. folder.legit(src)
  184. }
  185. // Copy a file from src to dst
  186. func copyFile(fsrc, fdst string) error {
  187. inf, err := os.Open(fsrc)
  188. if err != nil {
  189. return err
  190. }
  191. defer inf.Close()
  192. ouf, err := os.Create(fdst)
  193. if err != nil {
  194. return err
  195. }
  196. defer ouf.Close()
  197. _, err = io.Copy(ouf, inf)
  198. if err != nil {
  199. return err
  200. }
  201. return nil
  202. }
  203. // Copy a folder tree
  204. func copyFolder(fsrc, fdst string) error {
  205. // mkdir -p
  206. os.MkdirAll(fdst, 0755)
  207. // copy file first
  208. files, err := ioutil.ReadDir(fsrc)
  209. if err != nil {
  210. return err
  211. }
  212. // walk all files first
  213. for _, fi := range files {
  214. if !fi.IsDir() {
  215. fin := fi.Name()
  216. err := copyFile(filepath.Join(fsrc, fin), filepath.Join(fdst, fin))
  217. if err != nil {
  218. WARN(err.Error())
  219. }
  220. }
  221. }
  222. // walk all subfolder
  223. for _, fi := range files {
  224. if fi.IsDir() {
  225. fin := fi.Name()
  226. err := copyFolder(filepath.Join(fsrc, fin), filepath.Join(fdst, fin))
  227. if err != nil {
  228. WARN(err.Error())
  229. }
  230. }
  231. }
  232. return nil
  233. }
  234. // Mark a file in Out as legit from Src, by deleting it from outfiles
  235. func (folder *FOLDER) legit(src string) {
  236. nf := []os.FileInfo{}
  237. for _, f := range folder.outfiles {
  238. if f.Name() != src {
  239. nf = append(nf, f)
  240. }
  241. }
  242. folder.outfiles = nf
  243. }
  244. // Cleanup: delete any extra files in Pub not present in Post
  245. func (folder *FOLDER) CleanOut() {
  246. for _, f := range folder.outfiles {
  247. os.Remove(filepath.Join(folder.GetOutDir(), f.Name()))
  248. }
  249. folder.outfiles = nil
  250. }
  251. // Populate pubContent from a PostsDir's sub dir
  252. func (folder *FOLDER) PopulateOut() {
  253. outDir := folder.GetOutDir()
  254. os.MkdirAll(outDir, 0755)
  255. files, err := ioutil.ReadDir(outDir)
  256. if err != nil {
  257. WARN(err.Error())
  258. return
  259. }
  260. for _, f := range files {
  261. if !f.IsDir() {
  262. folder.outfiles = append(folder.outfiles, f)
  263. }
  264. }
  265. }
  266. // Clear the public directory, ignoring special files, subdirectories, and hidden (dot) files.
  267. func clearPublicDir() error {
  268. // do nothing for now
  269. return nil
  270. // Clear the public directory, except subdirs and special files (favicon.ico & co.)
  271. fis, err := ioutil.ReadDir(PublicDir)
  272. if err != nil {
  273. return fmt.Errorf("error getting public directory files: %s", err)
  274. }
  275. for _, fi := range fis {
  276. if !fi.IsDir() && !strings.HasPrefix(fi.Name(), ".") {
  277. }
  278. }
  279. return nil
  280. }
  281. // Generate the whole site.
  282. func generateSite() error {
  283. // First compile the template(s)
  284. if err := compileTemplates(); err != nil {
  285. DEBUG("template error %v", err)
  286. return err
  287. }
  288. genPath(PostsDir)
  289. return nil
  290. }
  291. // create newpage, fill with metadata, but don't render template yet
  292. func (folder *FOLDER) newPage(mdf string) {
  293. var p PAGE = PAGE{
  294. Root: site.RootFOLDER,
  295. Folder: folder,
  296. SrcName: mdf,
  297. Meta: make(TemplateData),
  298. }
  299. fpath := filepath.Join(folder.GetSrcDir(), mdf)
  300. f, err := os.Open(fpath)
  301. if err != nil {
  302. ERROR("Cannot open %v(%v)", fpath, err)
  303. return
  304. }
  305. defer f.Close()
  306. p.DstName = getSlug(mdf)
  307. p.Meta["Slug"] = p.DstName
  308. fi, _ := f.Stat()
  309. p.PubTime = fi.ModTime()
  310. p.ModTime = fi.ModTime()
  311. if dt, ok := p.Meta["Date"]; ok && len(dt) > 0 {
  312. pubdt, err := time.Parse(pubDtFmt[len(dt)], dt)
  313. if err == nil {
  314. p.PubTime = pubdt
  315. }
  316. }
  317. p.Meta["PubTime"] = p.PubTime.Format("2006-01-02")
  318. p.Meta["ModTime"] = p.ModTime.Format("15:04")
  319. s := bufio.NewScanner(f)
  320. meta, err := readFrontMatter(s)
  321. if err != nil {
  322. WARN("Cannot read meta from %v(%v)", fpath, err)
  323. return
  324. }
  325. for k, v := range meta {
  326. p.Meta[k] = v
  327. }
  328. p.DstName = p.Meta["Slug"]
  329. if t, err := time.Parse("2006-01-02", p.Meta["PubTime"]); err == nil {
  330. p.PubTime = t
  331. }
  332. if _, ok := p.Meta["Index"]; ok {
  333. folder.index = &p
  334. }
  335. // Read rest of file
  336. p.buf = bytes.NewBuffer(nil)
  337. for s.Scan() {
  338. p.buf.WriteString(s.Text() + "\n")
  339. }
  340. folder.Pages = append(folder.Pages, &p)
  341. }
  342. // Generate the static HTML file for the post identified by the index.
  343. func (folder *FOLDER) generateFile(p *PAGE, idx bool) {
  344. var w io.Writer
  345. // check if template exists
  346. tplName, ok := p.Meta["Template"]
  347. if !ok {
  348. tplName = "default"
  349. }
  350. var tpl *template.Template
  351. var ex bool
  352. if tpl, ex = postTpls[tplName]; !ex {
  353. ERROR("Template not found: %s", tplName)
  354. return
  355. }
  356. slug := p.Meta["Slug"]
  357. fw, err := os.Create(filepath.Join(folder.GetOutDir(), slug))
  358. if err != nil {
  359. ERROR("error creating output %s: %s", slug, err)
  360. return
  361. }
  362. defer fw.Close()
  363. // If this is the newest file, also save as index.html
  364. w = fw
  365. if idx {
  366. idxw, err := os.Create(filepath.Join(folder.GetOutDir(), "index.html"))
  367. if err != nil {
  368. ERROR("error creating static file index.html: %s", err)
  369. return
  370. }
  371. defer idxw.Close()
  372. w = io.MultiWriter(fw, idxw)
  373. }
  374. // format from mardown
  375. res := blackfriday.Markdown(p.buf.Bytes(), bfRender, bfExtensions)
  376. p.Content = template.HTML(res)
  377. tpl.ExecuteTemplate(w, tplName+".amber", p)
  378. folder.legit(slug)
  379. }