package internal import ( "embed" "fmt" "html/template" "net/http" "os" "path/filepath" "time" "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" "github.com/james4k/fmatter" "github.com/julienschmidt/httprouter" sync "github.com/sasha-s/go-deadlock" log "github.com/sirupsen/logrus" "golang.org/x/text/cases" "golang.org/x/text/language" ) const pagesDir = "pages" //go:embed pages/*.md var builtinPages embed.FS type FrontMatter struct { Title string Description string } type Page struct { Content string LastModified time.Time } // PageHandler ... func (s *Server) PageHandler(name string) httprouter.Handle { pagesBaseDir := filepath.Join(s.config.Data, pagesDir) pageMutex := &sync.RWMutex{} pageCache := make(map[string]*Page) getPage := func(name string) (*Page, error) { fn := filepath.Join(pagesBaseDir, fmt.Sprintf("%s.md", name)) pageMutex.RLock() page, isCached := pageCache[name] pageMutex.RUnlock() if isCached && FileExists(fn) { if fileInfo, err := os.Stat(fn); err == nil { if fileInfo.ModTime().After(page.LastModified) { data, err := os.ReadFile(fn) if err != nil { log.WithError(err).Warnf("error reading page %s", name) return page, nil } page.Content = string(data) page.LastModified = fileInfo.ModTime() pageMutex.Lock() pageCache[name] = page pageMutex.Unlock() return page, nil } } } page = &Page{} if FileExists(fn) { fileInfo, err := os.Stat(fn) if err != nil { log.WithError(err).Errorf("error getting page stats") return nil, err } page.LastModified = fileInfo.ModTime() data, err := os.ReadFile(fn) if err != nil { log.WithError(err).Errorf("error reading page %s", name) return nil, err } page.Content = string(data) } else { fn := filepath.Join(pagesDir, fmt.Sprintf("%s.md", name)) data, err := builtinPages.ReadFile(fn) if err != nil { log.WithError(err).Errorf("error reading custom page %s", name) return nil, err } page.Content = string(data) } pageMutex.Lock() pageCache[name] = page pageMutex.Unlock() return page, nil } caser := cases.Title(language.English) return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { ctx := NewContext(s.config, s.db, r) page, err := getPage(name) if err != nil { if os.IsNotExist(err) { ctx.Error = true ctx.Message = "Page Not Found" s.render("404", w, ctx) return } ctx.Error = true ctx.Message = "Error reading page" s.render("message", w, ctx) return } markdownContent, err := RenderHTML(page.Content, ctx) if err != nil { log.WithError(err).Errorf("error rendering page %s", name) ctx.Error = true ctx.Message = "Error rendering page" s.render("error", w, ctx) return } var frontmatter FrontMatter content, err := fmatter.Read([]byte(markdownContent), &frontmatter) if err != nil { log.WithError(err).Error("error parsing front matter") ctx.Error = true ctx.Message = "Error loading page" s.render("error", w, ctx) return } extensions := parser.CommonExtensions | parser.AutoHeadingIDs p := parser.NewWithExtensions(extensions) htmlFlags := html.CommonFlags opts := html.RendererOptions{ Flags: htmlFlags, Generator: "", } renderer := html.NewRenderer(opts) html := markdown.ToHTML(content, p, renderer) var title string if frontmatter.Title != "" { title = frontmatter.Title } else { title = caser.String(name) } ctx.Title = title ctx.Meta.Description = frontmatter.Description ctx.Page = name ctx.Content = template.HTML(html) s.render("page", w, ctx) } }