223 lines
5.8 KiB
Go
223 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"github.com/yuin/goldmark"
|
|
"github.com/yuin/goldmark-meta"
|
|
"github.com/yuin/goldmark/ast"
|
|
"github.com/yuin/goldmark/extension"
|
|
extast "github.com/yuin/goldmark/extension/ast"
|
|
"github.com/yuin/goldmark/parser"
|
|
"github.com/yuin/goldmark/text"
|
|
"gopkg.in/yaml.v3"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
func ParseMarkdown(mdPath string) (Config, ast.Node, []byte, error) {
|
|
content, err := os.ReadFile(mdPath)
|
|
if err != nil {
|
|
return Config{}, nil, nil, err
|
|
}
|
|
|
|
md := goldmark.New(
|
|
goldmark.WithExtensions(meta.Meta, extension.Table),
|
|
)
|
|
|
|
context := parser.NewContext()
|
|
doc := md.Parser().Parse(text.NewReader(content), parser.WithContext(context))
|
|
|
|
metaData := meta.Get(context)
|
|
|
|
// Convert metaData map to Config struct
|
|
var config Config
|
|
yamlData, _ := yaml.Marshal(metaData)
|
|
err = yaml.Unmarshal(yamlData, &config)
|
|
if err != nil {
|
|
return Config{}, nil, nil, fmt.Errorf("error parsing metadata: %v", err)
|
|
}
|
|
|
|
return config, doc, content, nil
|
|
}
|
|
|
|
type parserState struct {
|
|
nextCodeIsAppendix bool
|
|
appendixTitle string
|
|
}
|
|
|
|
func RenderAST(doc ast.Node, content []byte, r *IHKRenderer) error {
|
|
r.StartFrontMatter()
|
|
state := &parserState{}
|
|
|
|
return ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
if !entering {
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
switch node := n.(type) {
|
|
case *ast.Heading:
|
|
if node.Level == 1 && r.numType == NumRoman {
|
|
title := extractText(node, content)
|
|
if title != "Vorwort" && title != "Abkürzungsverzeichnis" {
|
|
r.StartMainBody()
|
|
}
|
|
}
|
|
title := extractText(node, content)
|
|
r.RenderHeader(node.Level, title)
|
|
return ast.WalkSkipChildren, nil
|
|
case *ast.Paragraph:
|
|
text := extractText(node, content)
|
|
lines := strings.Split(text, "\n")
|
|
isMeta := false
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "@Quelle:") {
|
|
r.AddSource(strings.TrimPrefix(line, "@Quelle:"))
|
|
isMeta = true
|
|
} else if strings.HasPrefix(line, "@Anhang:") {
|
|
r.AddAppendix(strings.TrimPrefix(line, "@Anhang:"))
|
|
isMeta = true
|
|
} else if strings.HasPrefix(line, "@AnhangUML:") {
|
|
state.nextCodeIsAppendix = true
|
|
state.appendixTitle = strings.TrimSpace(strings.TrimPrefix(line, "@AnhangUML:"))
|
|
isMeta = true
|
|
}
|
|
}
|
|
if isMeta {
|
|
return ast.WalkSkipChildren, nil
|
|
}
|
|
r.RenderParagraph(text)
|
|
return ast.WalkSkipChildren, nil
|
|
case *ast.FencedCodeBlock:
|
|
lang := string(node.Language(content))
|
|
code := extractCode(node, content)
|
|
if lang == "mermaid" || lang == "plantuml" || lang == "puml" {
|
|
imgPath, err := RenderDiagramViaKroki(lang, code)
|
|
if err == nil {
|
|
if state.nextCodeIsAppendix {
|
|
r.AddAppendix(state.appendixTitle + " | " + imgPath)
|
|
state.nextCodeIsAppendix = false
|
|
} else {
|
|
r.RenderImage(imgPath, "Diagramm: "+lang)
|
|
}
|
|
return ast.WalkSkipChildren, nil
|
|
}
|
|
}
|
|
case *ast.Image:
|
|
imgPath := string(node.Destination)
|
|
title := string(node.Title)
|
|
r.RenderImage(imgPath, title)
|
|
return ast.WalkSkipChildren, nil
|
|
case *ast.Blockquote:
|
|
// Check if first paragraph starts with "Quelle:"
|
|
first := node.FirstChild()
|
|
if first != nil {
|
|
if para, ok := first.(*ast.Paragraph); ok {
|
|
pText := extractText(para, content)
|
|
if strings.HasPrefix(pText, "Quelle:") || strings.HasPrefix(pText, "Source:") {
|
|
sourceText := strings.TrimPrefix(pText, "Quelle:")
|
|
sourceText = strings.TrimPrefix(sourceText, "Source:")
|
|
r.AddSource(sourceText)
|
|
return ast.WalkSkipChildren, nil
|
|
}
|
|
}
|
|
}
|
|
case *ast.List:
|
|
// Items will be handled by ListItem
|
|
case *ast.ListItem:
|
|
text := extractText(node, content)
|
|
r.RenderListItem(text, true, 0) // Basic bullet point for now
|
|
return ast.WalkSkipChildren, nil
|
|
case *extast.Table:
|
|
var tableData [][]string
|
|
for row := node.FirstChild(); row != nil; row = row.NextSibling() {
|
|
var rowData []string
|
|
for cell := row.FirstChild(); cell != nil; cell = cell.NextSibling() {
|
|
rowData = append(rowData, extractText(cell, content))
|
|
}
|
|
tableData = append(tableData, rowData)
|
|
}
|
|
r.RenderTable(tableData)
|
|
return ast.WalkSkipChildren, nil
|
|
}
|
|
return ast.WalkContinue, nil
|
|
})
|
|
}
|
|
|
|
func extractText(n ast.Node, content []byte) string {
|
|
var textStr string
|
|
for child := n.FirstChild(); child != nil; child = child.NextSibling() {
|
|
if textNode, ok := child.(*ast.Text); ok {
|
|
textStr += string(textNode.Segment.Value(content))
|
|
if textNode.HardLineBreak() || textNode.SoftLineBreak() {
|
|
textStr += "\n"
|
|
}
|
|
} else {
|
|
textStr += extractText(child, content)
|
|
}
|
|
}
|
|
if textStr == "" {
|
|
// Fallback for simple nodes
|
|
return string(n.Text(content))
|
|
}
|
|
return textStr
|
|
}
|
|
|
|
func extractCode(n *ast.FencedCodeBlock, content []byte) string {
|
|
var buf bytes.Buffer
|
|
for i := 0; i < n.Lines().Len(); i++ {
|
|
line := n.Lines().At(i)
|
|
buf.Write(line.Value(content))
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func RenderDiagramViaKroki(lang string, code string) (string, error) {
|
|
if lang == "puml" {
|
|
lang = "plantuml"
|
|
}
|
|
|
|
// Kroki encoding: zlib + base64url
|
|
var b bytes.Buffer
|
|
w := zlib.NewWriter(&b)
|
|
w.Write([]byte(code))
|
|
w.Close()
|
|
|
|
encoded := base64.URLEncoding.EncodeToString(b.Bytes())
|
|
url := fmt.Sprintf("https://kroki.io/%s/png/%s", lang, encoded)
|
|
|
|
// Cache based on hash
|
|
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(code)))
|
|
cachePath := filepath.Join(os.TempDir(), "ihk_cache_"+hash+".png")
|
|
|
|
if _, err := os.Stat(cachePath); err == nil {
|
|
return cachePath, nil
|
|
}
|
|
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return "", fmt.Errorf("kroki error: %d", resp.StatusCode)
|
|
}
|
|
|
|
out, err := os.Create(cachePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer out.Close()
|
|
|
|
_, err = io.Copy(out, resp.Body)
|
|
return cachePath, err
|
|
}
|