From a65b9cb6b2c7dc7b48b8076d5e463776f1de0cf1 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Sun, 14 Dec 2025 18:05:35 +0100 Subject: feat(markdown): custom replacer --- markdown/ast.go | 2 +- markdown/ast_list_test.go | 2 +- markdown/ast_modifier_test.go | 2 +- markdown/ast_paragraph.go | 47 +++++++++++++++++++++++++++--------------- markdown/ast_paragraph_test.go | 20 ++++++++++++++++++ markdown/ast_quote_test.go | 2 +- markdown/eval.go | 18 ++++++++++------ markdown/lexer.go | 9 ++++++-- markdown/lexer_test.go | 27 ++++++++++++++++++------ 9 files changed, 94 insertions(+), 35 deletions(-) diff --git a/markdown/ast.go b/markdown/ast.go index f409ccb..b78104c 100644 --- a/markdown/ast.go +++ b/markdown/ast.go @@ -95,7 +95,7 @@ func getBlock(lxs *lexers, newLine bool) (block, *ParseError) { } else { b, err = code(lxs) } - case lexerLiteral, lexerModifier: + case lexerLiteral, lexerModifier, lexerReplace: b, err = paragraph(lxs, false) case lexerBreak: // do nothing default: diff --git a/markdown/ast_list_test.go b/markdown/ast_list_test.go index f33a229..210a83a 100644 --- a/markdown/ast_list_test.go +++ b/markdown/ast_list_test.go @@ -27,7 +27,7 @@ var expected = ` ` func TestList(t *testing.T) { - lxs := lex(rw) + lxs := lex(rw, new(Option)) tree, err := ast(lxs) if err != nil { t.Fatal(err) diff --git a/markdown/ast_modifier_test.go b/markdown/ast_modifier_test.go index 8ccc860..3689657 100644 --- a/markdown/ast_modifier_test.go +++ b/markdown/ast_modifier_test.go @@ -6,7 +6,7 @@ func TestModifier(t *testing.T) { content := ` **bo*n*soir**, ça ***va* bien** ? ` - lxs := lex(content) + lxs := lex(content, new(Option)) tree, err := ast(lxs) if err != nil { t.Fatal(err) diff --git a/markdown/ast_paragraph.go b/markdown/ast_paragraph.go index eddba0a..70d3414 100644 --- a/markdown/ast_paragraph.go +++ b/markdown/ast_paragraph.go @@ -42,6 +42,14 @@ func paragraph(lxs *lexers, oneLine bool) (*astParagraph, *ParseError) { maxBreak = 1 } n := 0 + asLiteral := func(conv func(s string) block) { + s := lxs.Current().Value + // replace line break by space + if n > 0 && len(tree.content) != 0 { + s = " " + s + } + tree.content = append(tree.content, conv(s)) + } lxs.current-- // because we do not use it before the next for lxs.Next() && n < maxBreak { switch lxs.Current().Type { @@ -52,21 +60,16 @@ func paragraph(lxs *lexers, oneLine bool) (*astParagraph, *ParseError) { lxs.Before() // because we did not use it return tree, nil } - tree.content = append(tree.content, astLiteral(lxs.Current().Value)) + asLiteral(toAstLiteral) case lexerLiteral, lexerHeading: - s := lxs.Current().Value - // replace line break by space - if n > 0 && len(tree.content) != 0 { - s = " " + s - } - n = 0 - tree.content = append(tree.content, astLiteral(s)) + asLiteral(toAstLiteral) + case lexerReplace: + asLiteral(toAstReplacer) case lexerModifier: // replace line break by space if n > 0 { tree.content = append(tree.content, astLiteral(" ")) } - n = 0 mod, err := modifier(lxs) if err != nil { return nil, &ParseError{lxs: *lxs, internal: err} @@ -78,12 +81,7 @@ func paragraph(lxs *lexers, oneLine bool) (*astParagraph, *ParseError) { return tree, nil } if lxs.Current().Value != "[" { - //if lxs.Current().Value == "!" { - s := lxs.Current().Value - if n > 0 { - s = " " + s - } - tree.content = append(tree.content, astLiteral(s)) + asLiteral(toAstLiteral) } else { ext, err := external(lxs) if err != nil { @@ -91,18 +89,19 @@ func paragraph(lxs *lexers, oneLine bool) (*astParagraph, *ParseError) { } tree.content = append(tree.content, ext) } - n = 0 case lexerCode: if len(lxs.Current().Value) > 1 { return nil, &ParseError{lxs: *lxs, internal: ErrInvalidCodeBlockPosition} } - n = 0 b, err := code(lxs) if err != nil { return nil, err } tree.content = append(tree.content, b) } + if lxs.Current().Type != lexerBreak { + n = 0 + } } lxs.Before() // because we never handle the last item return tree, nil @@ -113,3 +112,17 @@ type astLiteral string func (a astLiteral) Eval(_ *Option) (template.HTML, *ParseError) { return template.HTML(template.HTMLEscapeString(string(a))), nil } + +func toAstLiteral(s string) block { + return astLiteral(s) +} + +type astReplacer string + +func (a astReplacer) Eval(opt *Option) (template.HTML, *ParseError) { + return template.HTML(opt.Replaces[[]rune(a)[0]]), nil +} + +func toAstReplacer(s string) block { + return astReplacer(s) +} diff --git a/markdown/ast_paragraph_test.go b/markdown/ast_paragraph_test.go index 46dd714..2014ab9 100644 --- a/markdown/ast_paragraph_test.go +++ b/markdown/ast_paragraph_test.go @@ -11,3 +11,23 @@ func TestParagraph(t *testing.T) { t.Errorf("failed, got %s", c) } } + +func TestParagraph_Replacer(t *testing.T) { + opt := &Option{ + Replaces: map[rune]string{'~': " "}, + } + c, err := Parse("bonsoir", opt) + if err != nil { + t.Fatal(err) + } + if c != "

bonsoir

" { + t.Errorf("failed, got %s", c) + } + c, err = Parse("bonsoir~!", opt) + if err != nil { + t.Fatal(err) + } + if c != "

bonsoir !

" { + t.Errorf("failed, got %s", c) + } +} diff --git a/markdown/ast_quote_test.go b/markdown/ast_quote_test.go index 3346174..50ff23a 100644 --- a/markdown/ast_quote_test.go +++ b/markdown/ast_quote_test.go @@ -7,7 +7,7 @@ func TestQuote(t *testing.T) { > Bonsoir, je suis un **code** avec une source ` - lxs := lex(content) + lxs := lex(content, new(Option)) tree, err := ast(lxs) if err != nil { t.Fatal(err) diff --git a/markdown/eval.go b/markdown/eval.go index 376e577..ae4bafc 100644 --- a/markdown/eval.go +++ b/markdown/eval.go @@ -1,18 +1,16 @@ package markdown -import "html/template" +import ( + "html/template" +) type Option struct { ImageSource func(source string) string RenderLink func(content, href string) template.HTML + Replaces map[rune]string } func Parse(s string, opt *Option) (template.HTML, *ParseError) { - lxs := lex(s) - tree, err := ast(lxs) - if err != nil { - return "", err - } if opt == nil { opt = new(Option) } @@ -22,6 +20,14 @@ func Parse(s string, opt *Option) (template.HTML, *ParseError) { if opt.RenderLink == nil { opt.RenderLink = RenderLink } + if opt.Replaces == nil { + opt.Replaces = make(map[rune]string, 0) + } + lxs := lex(s, opt) + tree, err := ast(lxs) + if err != nil { + return "", err + } return tree.Eval(opt) } diff --git a/markdown/lexer.go b/markdown/lexer.go index b68bbf9..375ef7f 100644 --- a/markdown/lexer.go +++ b/markdown/lexer.go @@ -21,6 +21,7 @@ const ( lexerExternal lexerType = "external" lexerLiteral lexerType = "literal" + lexerReplace lexerType = "replace" ) type lexer struct { @@ -63,7 +64,7 @@ func (l *lexers) String() string { return s + "]" } -func lex(s string) *lexers { +func lex(s string, opt *Option) *lexers { lxs := &lexers{current: -1} var lexs []lexer var currentType lexerType @@ -127,7 +128,11 @@ func lex(s string) *lexers { fn(c, lexerList, nil) default: newLine = false - fn(c, lexerLiteral, nil) + if _, ok := opt.Replaces[c]; ok { + fn(c, lexerReplace, func(c rune) bool { return false }) + } else { + fn(c, lexerLiteral, nil) + } } } if len(previous) > 0 { diff --git a/markdown/lexer_test.go b/markdown/lexer_test.go index e994142..d8ec3ba 100644 --- a/markdown/lexer_test.go +++ b/markdown/lexer_test.go @@ -3,28 +3,43 @@ package markdown import "testing" func TestLex(t *testing.T) { - lxs := lex("bonjour les gens") + opt := new(Option) + lxs := lex("bonjour les gens", opt) if lxs.String() != "Lexers[literal(bonjour les gens) ]" { t.Errorf("invalid lex, got %s", lxs) } - lxs = lex("# bonjour les gens") + lxs = lex("# bonjour les gens", opt) if lxs.String() != "Lexers[header(#) literal( bonjour les gens) ]" { t.Errorf("invalid lex, got %s", lxs) } - lxs = lex("# bonjour les gens\nComment ça va ?") + lxs = lex("# bonjour les gens\nComment ça va ?", opt) if lxs.String() != `Lexers[header(#) literal( bonjour les gens) break({\n}) literal(Comment ça va ?) ]` { t.Errorf("invalid lex, got %s", lxs) } - lxs = lex("***hey***, what's up?") + lxs = lex("***hey***, what's up?", opt) if lxs.String() != "Lexers[modifier(***) literal(hey) modifier(***) literal(, what's up?) ]" { t.Errorf("invalid lex, got %s", lxs) } - lxs = lex(`Xxx\_DarkEmperor\_xxX`) + lxs = lex(`Xxx\_DarkEmperor\_xxX`, opt) if lxs.String() != `Lexers[literal(Xxx_DarkEmperor_xxX) ]` { t.Errorf("invalid lex, got %s", lxs) } - lxs = lex(`* list`) + lxs = lex(`* list`, opt) if lxs.String() != `Lexers[list(*) literal( list) ]` { t.Errorf("invalid lex, got %s", lxs) } } + +func TestLex_Replacer(t *testing.T) { + opt := &Option{ + Replaces: map[rune]string{'~': " "}, + } + lxs := lex("bonjour les gens", opt) + if lxs.String() != "Lexers[literal(bonjour les gens) ]" { + t.Errorf("invalid lex, got %s", lxs) + } + lxs = lex("bonjour les gens~!", opt) + if lxs.String() != "Lexers[literal(bonjour les gens) replace(~) external(!) ]" { + t.Errorf("invalid lex, got %s", lxs) + } +} -- cgit v1.2.3