From cdb0a541554aa4a16fc71b6425225e8c603c83b8 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Thu, 2 Oct 2025 12:03:06 +0200 Subject: feat(markdown): support external link and image (without source) --- mardown/ast.go | 6 +- mardown/ast_external.go | 141 +++++++++++++++++++++++++++++++++++++++++++ mardown/ast_external_test.go | 31 ++++++++++ mardown/ast_modifier.go | 6 ++ mardown/ast_paragraph.go | 7 ++- 5 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 mardown/ast_external.go create mode 100644 mardown/ast_external_test.go diff --git a/mardown/ast.go b/mardown/ast.go index 3794ff1..ed4d1f8 100644 --- a/mardown/ast.go +++ b/mardown/ast.go @@ -37,7 +37,7 @@ func (t *tree) String() string { func ast(lxs *lexers) (*tree, error) { tr := new(tree) - newLine := false + newLine := true for lxs.Next() { b, err := getBlock(lxs, newLine) if err != nil { @@ -64,8 +64,8 @@ func getBlock(lxs *lexers, newLine bool) (block, error) { b, err = header(lxs) } case lexerExternal: - if newLine && lxs.Current().Value == "!" { - //TODO: handle + if newLine && lxs.Current().Value == "![" { + b, err = external(lxs) } else { b, err = paragraph(lxs, false) } diff --git a/mardown/ast_external.go b/mardown/ast_external.go new file mode 100644 index 0000000..87fb880 --- /dev/null +++ b/mardown/ast_external.go @@ -0,0 +1,141 @@ +package mardown + +import ( + "fmt" + "html/template" +) + +type astLink struct { + content block + href block +} + +func (a *astLink) Eval() (template.HTML, error) { + content, err := a.content.Eval() + if err != nil { + return "", err + } + href, err := a.href.Eval() + if err != nil { + return "", err + } + return template.HTML(fmt.Sprintf(`%s`, href, content)), nil +} + +type astImage struct { + alt block + src block + source *astParagraph +} + +func (a *astImage) Eval() (template.HTML, error) { + alt, err := a.alt.Eval() + if err != nil { + return "", err + } + src, err := a.src.Eval() + if err != nil { + return "", err + } + if a.source == nil { + return template.HTML(fmt.Sprintf(`
%s
`, alt, src)), nil + } + source, err := a.source.Eval() + if err != nil { + return "", err + } + return template.HTML(fmt.Sprintf(`
%s
%s
`, alt, src, source)), nil +} + +func external(lxs *lexers) (block, error) { + tp := lxs.Current().Value + if !lxs.Next() { + return astLiteral(tp), nil + } + lxs.Before() // because we call Next + var b block + var err error + switch tp { + case "![": + b, err = image(lxs) + case "[": + b, err = link(lxs) + default: + b = astLiteral(tp) + } + return b, err +} + +func link(lxs *lexers) (block, error) { + lk := new(astLink) + start := lxs.current + content, href, _, ok := parseExternal(lxs, 1) + if !ok { + return reset(lxs, start), nil + } + lk.content = astLiteral(content) + lk.href = astLiteral(href) + return lk, nil +} + +func image(lxs *lexers) (block, error) { + img := new(astImage) + start := lxs.current + alt, src, _, ok := parseExternal(lxs, 2) + if !ok { + return reset(lxs, start), nil + } + img.alt = astLiteral(alt) + img.src = astLiteral(src) + //img.source = astLiteral(source) + return img, nil +} + +func parseExternal(lxs *lexers, maxBreak int) (string, string, string, bool) { + next := false + var s string + var first string + var end string + n := 0 + for lxs.Next() && n < maxBreak { + switch lxs.Current().Type { + case lexerBreak: + n++ + case lexerExternal: + if n > 0 && (first == "" || end == "") { + return "", "", "", false + } + n = 0 + if !next { + if lxs.Current().Value != "](" || !lxs.Next() { + return "", "", "", false + } + lxs.Before() // because we called Next + first = s + s = "" + next = true + } else { + if lxs.Current().Value != ")" { + return "", "", "", false + } + if maxBreak == 1 { + return first, s, "", true + } + end = s + s = "" + } + default: + n = 0 + s += lxs.Current().Value + } + } + if maxBreak == 1 { + return "", "", "", false + } + return first, end, s, true +} + +func reset(lxs *lexers, start int) block { + lxs.current = start + return astLiteral(lxs.Current().Value) +} diff --git a/mardown/ast_external_test.go b/mardown/ast_external_test.go new file mode 100644 index 0000000..fc0597d --- /dev/null +++ b/mardown/ast_external_test.go @@ -0,0 +1,31 @@ +package mardown + +import "testing" + +func TestExternal(t *testing.T) { + lxs := lex("[content](href)") + tree, err := ast(lxs) + if err != nil { + t.Fatal(err) + } + got, err := tree.Eval() + if err != nil { + t.Fatal(err) + } + if string(got) != `

content

` { + t.Errorf("invalid value, got %s", got) + } + + lxs = lex("![image alt](image src)") + tree, err = ast(lxs) + if err != nil { + t.Fatal(err) + } + got, err = tree.Eval() + if err != nil { + t.Fatal(err) + } + if string(got) != `
image alt
` { + t.Errorf("invalid value, got %s", got) + } +} diff --git a/mardown/ast_modifier.go b/mardown/ast_modifier.go index 13cd91d..bd04b28 100644 --- a/mardown/ast_modifier.go +++ b/mardown/ast_modifier.go @@ -117,6 +117,12 @@ func modifier(lxs *lexers) (*astModifier, error) { case lexerExternal: if lxs.Current().Value == "!" { s += lxs.Current().Value + } else { + ext, err := external(lxs) + if err != nil { + return nil, err + } + mod.content = append(mod.content, ext) } default: return nil, ErrInvalidTypeInModifier diff --git a/mardown/ast_paragraph.go b/mardown/ast_paragraph.go index ee71794..44b5dd9 100644 --- a/mardown/ast_paragraph.go +++ b/mardown/ast_paragraph.go @@ -68,8 +68,13 @@ func paragraph(lxs *lexers, oneLine bool) (*astParagraph, error) { n = 0 if lxs.Current().Value == "!" { tree.content = append(tree.content, astLiteral(lxs.Current().Value)) + } else { + ext, err := external(lxs) + if err != nil { + return nil, err + } + tree.content = append(tree.content, ext) } - //TODO: handle case lexerCode: n = 0 b, err := code(lxs) -- cgit v1.2.3