// Package page implements the website backed by a local filesystem. // // Reading the base template off the disk, then any markdown files which are // split into two sections by the DocumentSplit global variable. The first // section is parsed as yaml to populate the Page struct. The second portion is // markdown, first executed as part of the text/template then rendered by // blackfriday. // // This package is designed to only be run with ***TRUSTED INPUT*** // // Usage: // // import ( // "fmt" // "os" // site "riedstra.dev/mitch/go-website/page" // ) // // Where some/path.md exists // p := site.NewPage("/some/path") // // Dump the rendered HTML to stdout // err := p.Render(os.Stdout) // if err != nil { // fmt.Fprintln(os.Stderr, err) // } package page import ( "bufio" "bytes" "errors" "fmt" "io" "log" "os" "path/filepath" "text/template" "github.com/russross/blackfriday" "gopkg.in/yaml.v3" ) // Page should not be created directly as it will not set the interal path // properly. Use NewPage instead. // // The exported fields can be filled in the yaml at the top of a page and // utilized within. type Page struct { path string DefaultTitle string `yaml:"title"` DefaultDescription string `yaml:"description"` AuthorName string AuthorEmail string // Tags to apply to the page in question. Useful for Index() Tags map[string]interface{} Date *PageTime Published bool Vars map[string]interface{} // Keys of vars to be included when RenderJson is called, all vars are // omitted if empty. JsonVars []string markdown []byte } // Global is meant to be supplied by external users of this package to populate // globally accessible information across all of the templates accessiable via // .Global care must be taken when utilizing this functionality. var Global interface{} // Funcs accessible to the templates. var Funcs template.FuncMap // CacheIndex determines whether or not the index will be cached in memory // or rebuilt on each call. var CacheIndex = true // TemplateDirectory is the parent directory which templates are stored, // usually the BaseTemplate is stored here as well, though it does not // have to be. Currently this is used for the 4xx and 5xx pages. ( 4xx.md // and 5xx.md respectively. var TemplateDirectory = "tpl" // BaseTemplate can be adjusted to change the base template used in rendering. var BaseTemplate = "tpl/base.md" // Suffix is applied to all pages for reading off of the disk. var Suffix = ".md" // DocumentSplit is used to split the .md files into yaml and markdown. var DocumentSplit = "|---\n" // Logger is the default logger used throughout this package, feel // free to override. var Logger = log.New(os.Stderr, "PAGE: ", log.LstdFlags) // NewPage returns a page struct with the path populated. func NewPage(pth string) *Page { return &Page{path: filepath.FromSlash(filepath.Clean(pth))} } // NewPage Allow for the creation of a new page from the current page, does // not inlcude any information about the current page. func (p Page) NewPage(pth string) *Page { return NewPage(pth) } // Path gets the current path set on the struct for the page in question // Useful if you're say iterating across tags to print out a list of // relevant posts on a blog or so by topic. func (p Page) Path() string { return filepath.ToSlash(p.path) } // Title returns `DefaultTitle` unless `.Vars.Title` is // set, then it returns that instead. func (p Page) Title() string { if p.Vars == nil { return p.DefaultTitle } t, ok := p.Vars["Title"] if !ok { return p.DefaultTitle } nt, ok := t.(string) if !ok { return p.DefaultTitle } return nt } // Description returns `DefaultDescription` unless `.Vars.Description` is // set, then it returns that instead. func (p Page) Description() string { if p.Vars == nil { return p.DefaultDescription } t, ok := p.Vars["Description"] if !ok { return p.DefaultDescription } nt, ok := t.(string) if !ok { return p.DefaultDescription } return nt } // Global is specifically for use inside of a page markdown file or // in a base template. This simply returns the package Global variable. func (p *Page) Global() interface{} { return Global } // Render a page. func (p *Page) Render(wr io.Writer) error { if err := p.Read(); err != nil { return err } templateContent, err := os.ReadFile(BaseTemplate) if err != nil { return fmt.Errorf("reading base template: %w", err) } t, err := template.New("baseTemplate").Funcs(Funcs).Parse(string(templateContent)) if err != nil { return fmt.Errorf("parsing: %w", err) } t = t.Funcs(Funcs) return t.Execute(wr, p) } // Read in the special markdown file format for the website off of the disk. func (p *Page) Read() error { yamlBuf := bytes.NewBuffer(nil) markdownBuf := bytes.NewBuffer(nil) fh, err := os.Open(p.path + Suffix) if err != nil { return fmt.Errorf("opening markdown: %w", err) } defer func() { err := fh.Close() if err != nil { Logger.Println(err) } }() rdr := bufio.NewReader(fh) // Read in the file and split between markdown and yaml buffers yamlDone := false for { bytes, err := rdr.ReadBytes('\n') if errors.Is(err, io.EOF) { break } else if err != nil { return fmt.Errorf("reading markdown: %w", err) } // Is this the line where we stop reading the yaml and start reading markdown? if DocumentSplit == string(bytes) && !yamlDone { yamlDone = true continue } if !yamlDone { yamlBuf.Write(bytes) } else { markdownBuf.Write(bytes) } } err = yaml.Unmarshal(yamlBuf.Bytes(), p) if err != nil { return fmt.Errorf("reading yaml: %w", err) } p.markdown = markdownBuf.Bytes() return nil } // RenderBody renders and executes a template from the body of the // markdown file, then runs it through the markdown parser. Typically // this is called in the base template. func (p *Page) RenderBody() (string, error) { s, err := p.GetMarkdown() return string(blackfriday.Run([]byte(s))), err } // GetMarkdown renders and executes a template from the body of the // markdown file, then simply returns the unrendered markdown. func (p *Page) GetMarkdown() (string, error) { buf := &bytes.Buffer{} t, err := template.New("Body").Funcs(Funcs).Parse(string(p.markdown)) if err != nil { return "", fmt.Errorf("render body: %w", err) } err = t.Execute(buf, p) if err != nil { return "", fmt.Errorf("template execute; %w", err) } return buf.String(), nil } func (p Page) String() string { return fmt.Sprintf("Page: %s", p.path) }