From d6e75145e54854484f2114398ff89787d243608a Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Thu, 5 Feb 2026 10:50:12 +0100 Subject: feat(markdown): interprate callout --- markdown/ast.go | 13 +++++++ markdown/ast_callout.go | 92 ++++++++++++++++++++++++++++++++++++++++++++ markdown/ast_callout_test.go | 16 ++++++++ markdown/ast_quote.go | 39 ++++++++----------- markdown/ast_test.go | 6 +++ markdown/lexer.go | 19 +++++---- markdown/lexer_test.go | 2 +- 7 files changed, 157 insertions(+), 30 deletions(-) create mode 100644 markdown/ast_callout.go create mode 100644 markdown/ast_callout_test.go diff --git a/markdown/ast.go b/markdown/ast.go index b78104c..f977228 100644 --- a/markdown/ast.go +++ b/markdown/ast.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "html/template" + "strings" ) var ErrUnkownLexType = errors.New("unkown lex type") @@ -106,3 +107,15 @@ func getBlock(lxs *lexers, newLine bool) (block, *ParseError) { } return b, err } + +func evalBlock(bs []*astParagraph, opt *Option) (template.HTML, *ParseError) { + var sb strings.Builder + for _, c := range bs { + ct, err := c.Eval(opt) + if err != nil { + return "", err + } + sb.WriteString(string(ct)) + } + return template.HTML(strings.TrimSpace(sb.String())), nil +} diff --git a/markdown/ast_callout.go b/markdown/ast_callout.go new file mode 100644 index 0000000..769d714 --- /dev/null +++ b/markdown/ast_callout.go @@ -0,0 +1,92 @@ +package markdown + +import ( + "errors" + "html/template" + "strings" + + "git.anhgelus.world/anhgelus/small-web/dom" +) + +var ( + ErrInvalidCallout = errors.New("invalid callout") +) + +type astCallout struct { + kind string + title *astParagraph + content []*astParagraph +} + +func (a *astCallout) Eval(opt *Option) (template.HTML, *ParseError) { + inner := dom.NewContentElement("div", make([]dom.Element, 0)) + for _, c := range a.content { + ct, err := c.Eval(opt) + if err != nil { + return "", err + } + inner.Contents = append(inner.Contents, dom.NewParagraph( + template.HTML(strings.TrimSpace(string(ct))), + )) + } + + titleContent, err := a.title.Eval(opt) + if err != nil { + return "", err + } + titleContent = template.HTML(strings.TrimSpace(string(titleContent))) + if len(titleContent) == 0 { + titleContent = template.HTML(a.kind) + } + title := dom.NewLiteralContentElement("h4", titleContent) + + callout := dom.NewContentElement("div", make([]dom.Element, 2)) + callout.Contents[0] = title + callout.Contents[1] = inner + callout.SetAttribute("data-kind", a.kind) + callout.ClassList().Add("callout") + return callout.Render(), nil +} + +func callout(lxs *lexers) (block, *ParseError) { + callout := new(astCallout) + if lxs.Current().Value != "[!" { + return paragraph(lxs, false) + } + if !lxs.Next() { + return nil, &ParseError{lxs: *lxs, internal: ErrInvalidCallout} + } + callout.kind = strings.ToLower(lxs.Current().Value) + if !lxs.Next() || lxs.Current().Type != lexerCallout || lxs.Current().Value != "]" { + return nil, &ParseError{lxs: *lxs, internal: ErrInvalidCallout} + } + var err *ParseError + callout.title, err = paragraph(lxs, true) + if err != nil { + return nil, err + } + n := 0 + for lxs.Next() && n < 2 { + current := lxs.Current() + n = 0 + switch current.Type { + case lexerBreak: + n = len(current.Value) + case lexerQuote: + case lexerLiteral, lexerModifier, lexerCode, lexerExternal: + p, err := paragraph(lxs, true) + if err != nil { + return nil, err + } + lxs.Before() + callout.content = append(callout.content, p) + n++ + default: + // because the code did not use it + lxs.Before() + return callout, nil + } + } + lxs.Before() + return callout, nil +} diff --git a/markdown/ast_callout_test.go b/markdown/ast_callout_test.go new file mode 100644 index 0000000..75140d4 --- /dev/null +++ b/markdown/ast_callout_test.go @@ -0,0 +1,16 @@ +package markdown + +import "testing" + +func TestCallout(t *testing.T) { + t.Run("callout", func(t *testing.T) { + t.Run("simple", test(` +> [!NOTE] +`, `

note

`)) + t.Run("multiline", test(` +> [!NOTE] Hey :3 +> content 1 +> content 2 +`, `

Hey :3

content 1

content 2

`)) + }) +} diff --git a/markdown/ast_quote.go b/markdown/ast_quote.go index 96669c5..72a43e6 100644 --- a/markdown/ast_quote.go +++ b/markdown/ast_quote.go @@ -2,7 +2,6 @@ package markdown import ( "html/template" - "strings" "git.anhgelus.world/anhgelus/small-web/dom" ) @@ -13,27 +12,18 @@ type astQuote struct { } func (a *astQuote) Eval(opt *Option) (template.HTML, *ParseError) { - var quoteContent template.HTML - for _, c := range a.quote { - ct, err := c.Eval(opt) - if err != nil { - return "", err - } - quoteContent += ct + quoteContent, err := evalBlock(a.quote, opt) + if err != nil { + return "", err } blockquote := dom.NewLiteralContentElement( "blockquote", - template.HTML(strings.TrimSpace(string(quoteContent))), + template.HTML(quoteContent), ) - var source template.HTML - for _, c := range a.source { - ct, err := c.Eval(opt) - if err != nil { - return "", err - } - source += ct + source, err := evalBlock(a.source, opt) + if err != nil { + return "", err } - source = template.HTML(strings.TrimSpace(string(source))) quote := dom.NewContentElement("div", make([]dom.Element, 0)) quote.ClassList().Add("quote") quote.Contents = append(quote.Contents, blockquote) @@ -43,26 +33,31 @@ func (a *astQuote) Eval(opt *Option) (template.HTML, *ParseError) { return quote.Render(), nil } -func quote(lxs *lexers) (*astQuote, *ParseError) { +func quote(lxs *lexers) (block, *ParseError) { tree := new(astQuote) n := 0 quoteContinue := true source := false for lxs.Next() && n < 2 { - switch lxs.Current().Type { + current := lxs.Current() + n = 0 + switch current.Type { case lexerBreak: - n = len(lxs.Current().Value) + n = len(current.Value) quoteContinue = false case lexerQuote: - n = 0 if source { // because the code did not use it lxs.Before() return tree, nil } quoteContinue = true + case lexerCallout: + if len(tree.quote) == 0 { + return callout(lxs) + } + fallthrough case lexerLiteral, lexerModifier, lexerCode, lexerExternal: - n = 0 if !quoteContinue { source = true } diff --git a/markdown/ast_test.go b/markdown/ast_test.go index 034eeb8..8205b4e 100644 --- a/markdown/ast_test.go +++ b/markdown/ast_test.go @@ -19,6 +19,10 @@ avec une source > qui recommence après ! qui a elle aussi une source :D +> [!NOTE] Hey :3 +> Hehe +> That's cool + - Ceci est une liste - pas ordonnée 1. et maintenant @@ -36,6 +40,7 @@ var parsed = `

Et je peux mettre du texte en gras, en italique et les deux en même temps !

Je suis une magnifique citation sur plusieurs lignes

avec une source

qui recommence après !

qui a elle aussi une source :D

+

Hey :3

Hehe

That's cool

  1. et maintenant
  2. elle l'est
@@ -51,6 +56,7 @@ var parsedPoem = `

Et je peux mettre du texte en gras,
en italique et les deux en même temps !

Je suis une magnifique citation sur plusieurs lignes

avec une source

qui recommence après !

qui a elle aussi une source :D

+

Hey :3

Hehe

That's cool

  1. et maintenant
  2. elle l'est
diff --git a/markdown/lexer.go b/markdown/lexer.go index f79b2ec..7386844 100644 --- a/markdown/lexer.go +++ b/markdown/lexer.go @@ -139,13 +139,18 @@ func lex(s string, opt *Option) *lexers { if !newLine && i < len(runes)-1 { next := runes[i+1] runes := []rune(previous) - if c == '[' && next == '!' { - fn(c, lexerCallout, nil) - continue - } else if c == '!' && len(runes) > 0 && previous[len(previous)-1] == '[' { - fn(c, lexerCallout, nil) - continue - } else if c == ']' && next != '(' { + if (c == '[' && next == '!') || + (c == '!' && len(runes) > 0 && previous[len(previous)-1] == '[') || + (c == ']' && next != '(') { + allSpace := true + for i := 0; allSpace && i < len(runes); i++ { + if runes[i] != ' ' { + allSpace = false + } + } + if allSpace { + previous = "" + } fn(c, lexerCallout, nil) continue } diff --git a/markdown/lexer_test.go b/markdown/lexer_test.go index 5736b2a..57f8606 100644 --- a/markdown/lexer_test.go +++ b/markdown/lexer_test.go @@ -30,7 +30,7 @@ func TestLex(t *testing.T) { } lxs = lex(`> [!NOTE] title > hey`, opt) - if lxs.String() != `Lexers[quote(>) literal( ) callout([!) literal(NOTE) callout(]) literal( title) break({\n}) quote(>) literal( hey) ]` { + if lxs.String() != `Lexers[quote(>) callout([!) literal(NOTE) callout(]) literal( title) break({\n}) quote(>) literal( hey) ]` { t.Errorf("invalid lex, got %s", lxs) } } -- cgit v1.2.3