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, "
\n") fmt.Fprintf(w, ` 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, `%s`, html) } else { // io.WriteString(w, ">") } } else { // if it's the closing img tag // we close the div tag *after* fmt.Fprintf(w, `
`) 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, `") } else { io.WriteString(w, "") } } 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() }