166 lines
4.7 KiB
Go
166 lines
4.7 KiB
Go
package builder
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/ficcdaf/zona/internal/util"
|
|
"github.com/gomarkdown/markdown"
|
|
"github.com/gomarkdown/markdown/ast"
|
|
"github.com/gomarkdown/markdown/html"
|
|
"github.com/gomarkdown/markdown/parser"
|
|
)
|
|
|
|
// This function takes a Markdown document and returns an HTML document.
|
|
func MdToHTML(md []byte) []byte {
|
|
// create parser with extensions
|
|
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
|
|
p := parser.NewWithExtensions(extensions)
|
|
doc := p.Parse(md)
|
|
|
|
// build HTML renderer
|
|
htmlFlags := html.CommonFlags | html.HrefTargetBlank
|
|
opts := html.RendererOptions{Flags: htmlFlags}
|
|
renderer := newZonaRenderer(opts)
|
|
|
|
return markdown.Render(doc, renderer)
|
|
}
|
|
|
|
// PathIsValid checks if a path is valid.
|
|
// If requireFile is set, directories are not considered valid.
|
|
func PathIsValid(path string, requireFile bool) bool {
|
|
s, err := os.Stat(path)
|
|
if os.IsNotExist(err) {
|
|
return false
|
|
} else if requireFile {
|
|
// fmt.Printf("Directory status: %s\n", strconv.FormatBool(s.IsDir()))
|
|
return !s.IsDir()
|
|
}
|
|
return err == nil
|
|
}
|
|
|
|
func processLink(p string) string {
|
|
// fmt.Println("Processing link...")
|
|
ext := filepath.Ext(p)
|
|
// Only process if it points to an existing, local markdown file
|
|
if ext == ".md" && filepath.IsLocal(p) {
|
|
// fmt.Println("Markdown link detected...")
|
|
return util.ChangeExtension(p, ".html")
|
|
} else {
|
|
return p
|
|
}
|
|
}
|
|
|
|
// renderImage outputs an ast Image node as HTML string.
|
|
func renderImage(w io.Writer, node *ast.Image, entering bool, next *ast.Text) {
|
|
// we add image-container div tag
|
|
// here before the opening img tag
|
|
if entering {
|
|
fmt.Fprintf(w, "<div class=\"image-container\">\n")
|
|
fmt.Fprintf(w, `<img src="%s" title="%s"`, node.Destination, node.Title)
|
|
if next != nil && len(next.Literal) > 0 {
|
|
md := []byte(next.Literal)
|
|
html, doc := convertEmbedded(md)
|
|
altText := extractPlainText(md, doc)
|
|
fmt.Fprintf(w, ` alt="%s">`, altText)
|
|
// TODO: render inside a special div?
|
|
// is this necessary since this is all inside image-container anyways?
|
|
fmt.Fprintf(w, `<small>%s</small>`, html)
|
|
} else {
|
|
//
|
|
io.WriteString(w, ">")
|
|
}
|
|
} else {
|
|
// if it's the closing img tag
|
|
// we close the div tag *after*
|
|
fmt.Fprintf(w, `</div>`)
|
|
fmt.Println("Image node not entering??")
|
|
}
|
|
}
|
|
|
|
func renderLink(w io.Writer, l *ast.Link, entering bool) {
|
|
if entering {
|
|
destPath := processLink(string(l.Destination))
|
|
fmt.Fprintf(w, `<a href="%s"`, destPath)
|
|
for _, attr := range html.BlockAttrs(l) {
|
|
fmt.Fprintf(w, ` %s`, attr)
|
|
}
|
|
io.WriteString(w, ">")
|
|
} else {
|
|
io.WriteString(w, "</a>")
|
|
}
|
|
}
|
|
|
|
func htmlRenderHookNoImage(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
|
|
if link, ok := node.(*ast.Link); ok {
|
|
renderLink(w, link, entering)
|
|
return ast.GoToNext, true
|
|
} else if _, ok := node.(*ast.Image); ok {
|
|
// we do not render images
|
|
return ast.GoToNext, true
|
|
}
|
|
return ast.GoToNext, false
|
|
}
|
|
|
|
// htmlRenderHook hooks the HTML renderer and overrides the rendering of certain nodes.
|
|
func htmlRenderHook(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
|
|
if link, ok := node.(*ast.Link); ok {
|
|
renderLink(w, link, entering)
|
|
return ast.GoToNext, true
|
|
} else if image, ok := node.(*ast.Image); ok {
|
|
var nextNode *ast.Text
|
|
if entering {
|
|
nextNodes := node.GetChildren()
|
|
fmt.Println("img next node len:", len(nextNodes))
|
|
if len(nextNodes) == 1 {
|
|
if textNode, ok := nextNodes[0].(*ast.Text); ok {
|
|
nextNode = textNode
|
|
}
|
|
}
|
|
}
|
|
renderImage(w, image, entering, nextNode)
|
|
// Skip rendering of `nextNode` explicitly
|
|
if nextNode != nil {
|
|
return ast.SkipChildren, true
|
|
}
|
|
return ast.GoToNext, true
|
|
}
|
|
return ast.GoToNext, false
|
|
}
|
|
|
|
// convertEmbedded renders markdown as HTML
|
|
// but does NOT render images
|
|
// also returns document AST
|
|
func convertEmbedded(md []byte) ([]byte, *ast.Node) {
|
|
p := parser.NewWithExtensions(parser.CommonExtensions)
|
|
doc := p.Parse(md)
|
|
htmlFlags := html.CommonFlags | html.HrefTargetBlank
|
|
opts := html.RendererOptions{Flags: htmlFlags}
|
|
opts.RenderNodeHook = htmlRenderHookNoImage
|
|
r := html.NewRenderer(opts)
|
|
html := markdown.Render(doc, r)
|
|
return html, &doc
|
|
}
|
|
|
|
func newZonaRenderer(opts html.RendererOptions) *html.Renderer {
|
|
opts.RenderNodeHook = htmlRenderHook
|
|
return html.NewRenderer(opts)
|
|
}
|
|
|
|
// ExtractPlainText walks the AST and extracts plain text from the Markdown input.
|
|
func extractPlainText(md []byte, doc *ast.Node) string {
|
|
var buffer bytes.Buffer
|
|
|
|
// Walk the AST and extract text nodes
|
|
ast.WalkFunc(*doc, func(node ast.Node, entering bool) ast.WalkStatus {
|
|
if textNode, ok := node.(*ast.Text); ok && entering {
|
|
buffer.Write(textNode.Literal) // Append the text content
|
|
}
|
|
return ast.GoToNext
|
|
})
|
|
|
|
return buffer.String()
|
|
}
|