add parser for blog articles

Signed-off-by: Tobias Erbshäußer <tobias@tesoft.dev>
This commit is contained in:
2026-05-24 09:22:16 +02:00
parent 5147b61c9d
commit 333211d4d0
5 changed files with 427 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
package md
import (
"bytes"
"errors"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
)
var PropertyNotFoundError = errors.New("property not found")
func Parse(transformer UrlTransformer, source []byte) (parser.Context, []byte, error) {
md := goldmark.New(
goldmark.WithExtensions(
extension.Footnote, extension.Strikethrough,
extension.Table,
&propertiesExtension{},
&urlTransformerExtension{transformer},
),
goldmark.WithRendererOptions(),
)
pc := parser.NewContext()
var readme bytes.Buffer
err := md.Convert(source, &readme, parser.WithContext(pc))
if err != nil {
return nil, nil, err
}
return pc, readme.Bytes(), nil
}
func GetProperty(pc parser.Context, key string) (string, error) {
data := pc.Get(propertiesContextKey)
if data != nil {
pData := data.(*propertiesData)
if pData.Error != nil {
return "", pData.Error
}
value, ok := pData.properties[key]
if ok {
return value, nil
}
}
return "", PropertyNotFoundError
}
+109
View File
@@ -0,0 +1,109 @@
package md
import (
"errors"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
var propertiesContextKey = parser.NewContextKey()
type propertiesData struct {
properties map[string]string
Error error
}
type propertiesExtension struct {
}
func (e *propertiesExtension) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(
parser.WithBlockParsers(
util.Prioritized(&propertiesParser{}, 0),
),
)
}
type propertiesParser struct {
}
func (p *propertiesParser) Trigger() []byte {
return []byte{'-'}
}
func (p *propertiesParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) {
lineNumber, _ := reader.Position()
if lineNumber != 0 {
return nil, parser.NoChildren
}
line, _ := reader.PeekLine()
if isSeparator(line) {
return ast.NewTextBlock(), parser.NoChildren
}
return nil, parser.NoChildren
}
func (p *propertiesParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State {
line, segment := reader.PeekLine()
if isSeparator(line) && !util.IsBlank(line) {
reader.Advance(segment.Len())
return parser.Close
}
node.Lines().Append(segment)
return parser.Continue | parser.NoChildren
}
func (p *propertiesParser) Close(node ast.Node, reader text.Reader, pc parser.Context) {
lines := node.Lines()
data := &propertiesData{
make(map[string]string),
nil,
}
pc.Set(propertiesContextKey, data)
for i := 0; i < lines.Len(); i++ {
segment := lines.At(i)
line := string(segment.Value(reader.Source()))
index := strings.IndexRune(line, ':')
if index == -1 {
data.Error = errors.New("invalid property line")
break
}
key := strings.TrimSpace(line[:index])
value := strings.TrimSpace(line[index+1:])
data.properties[key] = value
}
node.Parent().RemoveChild(node.Parent(), node)
}
func (p *propertiesParser) CanInterruptParagraph() bool {
return false
}
func (p *propertiesParser) CanAcceptIndentedLine() bool {
return false
}
func isSeparator(line []byte) bool {
line = util.TrimRightSpace(util.TrimLeftSpace(line))
for i := 0; i < len(line); i++ {
if line[i] != '-' {
return false
}
}
return true
}
+110
View File
@@ -0,0 +1,110 @@
package md
import (
"bytes"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type UrlTransformer = func(string) (string, error)
type urlTransformerExtension struct {
transformer UrlTransformer
}
func (e *urlTransformerExtension) Extend(m goldmark.Markdown) {
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(&urlRenderer{e.transformer}, 500),
))
}
type urlRenderer struct {
transformer UrlTransformer
}
func (r *urlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindImage, r.renderImage)
reg.Register(ast.KindLink, r.renderLink)
}
func (r *urlRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.Image)
url, err := r.resolveUrl(string(n.Destination))
if err != nil {
return ast.WalkStop, err
}
_, _ = w.WriteString(`<img src="`)
_, _ = w.WriteString(url)
_, _ = w.WriteString(`" alt="`)
_, _ = w.Write(nodeToHTMLText(n, source))
_ = w.WriteByte('"')
if n.Title != nil {
_, _ = w.WriteString(` title="`)
_, _ = w.Write(n.Title)
_ = w.WriteByte('"')
}
if n.Attributes() != nil {
html.RenderAttributes(w, n, html.ImageAttributeFilter)
}
_, _ = w.WriteString(">")
return ast.WalkSkipChildren, nil
}
func (r *urlRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.Link)
url, err := r.resolveUrl(string(n.Destination))
if err != nil {
return ast.WalkStop, err
}
_, _ = w.WriteString(`<a href="`)
_, _ = w.WriteString(url)
_, _ = w.WriteString(`"`)
if n.Attributes() != nil {
html.RenderAttributes(w, n, html.ImageAttributeFilter)
}
_, _ = w.WriteString(`>`)
if n.Title != nil {
_, _ = w.Write(n.Title)
}
_, _ = w.WriteString("</a>")
return ast.WalkSkipChildren, nil
}
func (r *urlRenderer) resolveUrl(url string) (string, error) {
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return r.transformer(url)
}
return url, nil
}
func nodeToHTMLText(n ast.Node, source []byte) []byte {
var buf bytes.Buffer
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
if s, ok := c.(*ast.String); ok && s.IsCode() {
buf.Write(s.Text(source))
} else if !c.HasChildren() {
buf.Write(util.EscapeHTML(c.Text(source)))
} else {
buf.Write(nodeToHTMLText(c, source))
}
}
return buf.Bytes()
}