diff options
| author | Anhgelus Morhtuuzh <william@herges.fr> | 2025-12-14 17:50:33 +0000 |
|---|---|---|
| committer | Anhgelus Morhtuuzh <william@herges.fr> | 2025-12-14 17:50:33 +0000 |
| commit | 3312977ebeff03edc5b1bc1a2f815cad6a1ba7b8 (patch) | |
| tree | 419617345dba2da3d0b666d5993964ce75904efa | |
| parent | 8edad5f9aee4625384485ad07180da751858839c (diff) | |
| parent | 0c0c6fb6df755d8f53d353e8b941e22a6c474b60 (diff) | |
Merge pull request '[Feat] Custom replace' (#3) from feat/custom-replace into main
Reviewed-on: https://git.anhgelus.world/anhgelus/small-web/pulls/3
| -rw-r--r-- | backend/config.go | 20 | ||||
| -rw-r--r-- | backend/parser.go | 22 | ||||
| -rw-r--r-- | backend/templates/components.html | 2 | ||||
| -rw-r--r-- | backend/templates/data.html | 2 | ||||
| -rw-r--r-- | markdown/ast.go | 2 | ||||
| -rw-r--r-- | markdown/ast_list_test.go | 2 | ||||
| -rw-r--r-- | markdown/ast_modifier_test.go | 2 | ||||
| -rw-r--r-- | markdown/ast_paragraph.go | 47 | ||||
| -rw-r--r-- | markdown/ast_paragraph_test.go | 20 | ||||
| -rw-r--r-- | markdown/ast_quote_test.go | 2 | ||||
| -rw-r--r-- | markdown/eval.go | 18 | ||||
| -rw-r--r-- | markdown/lexer.go | 9 | ||||
| -rw-r--r-- | markdown/lexer_test.go | 27 |
13 files changed, 131 insertions, 44 deletions
diff --git a/backend/config.go b/backend/config.go index 44b2033..e009b8a 100644 --- a/backend/config.go +++ b/backend/config.go @@ -5,6 +5,7 @@ import ( "log/slog" "os" + "git.anhgelus.world/anhgelus/small-web/markdown" "github.com/pelletier/go-toml/v2" ) @@ -22,6 +23,11 @@ type Logo struct { Favicon string `toml:"favicon"` } +type Replacer struct { + Symbol string `toml:"symbol"` + Replace string `tomle:"replace"` +} + type Config struct { Domain string `toml:"domain"` Name string `toml:"name"` @@ -37,6 +43,8 @@ type Config struct { Links []Link `toml:"links"` Logo Logo `toml:"logo"` + + Replacers []Replacer `toml:"replacers"` } func (c *Config) DefaultValues() { @@ -66,8 +74,11 @@ func (c *Config) DefaultValues() { c.RootFolder = "data" c.PublicFolder = "public" c.Quotes = []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do."} + c.Replacers = []Replacer{{"~", " "}} } +var defaultMarkdownOption markdown.Option + func LoadConfig(path string) (*Config, bool) { b, err := os.ReadFile(path) var config Config @@ -97,5 +108,14 @@ func LoadConfig(path string) (*Config, bool) { slog.Error("unmarshalling config file", "error", err) return nil, false } + defaultMarkdownOption.ImageSource = getStatic + defaultMarkdownOption.Replaces = make(map[rune]string, len(config.Replacers)) + for _, r := range config.Replacers { + if len(r.Symbol) != 1 { + slog.Error("invalid symbol in config", "symbol", r.Symbol) + return nil, false + } + defaultMarkdownOption.Replaces[[]rune(r.Symbol)[0]] = r.Replace + } return &config, true } diff --git a/backend/parser.go b/backend/parser.go index 3625671..8dba673 100644 --- a/backend/parser.go +++ b/backend/parser.go @@ -3,6 +3,7 @@ package backend import ( "errors" "fmt" + "html" "html/template" "log/slog" "strings" @@ -14,7 +15,7 @@ import ( type EntryInfo struct { Title string `toml:"title"` - Description string `toml:"description"` + Description template.HTML `toml:"description"` Img image `toml:"image"` PubLocalDate toml.LocalDate `toml:"publication_date"` } @@ -38,30 +39,37 @@ func renderLink(content, href, url string) template.HTML { } func parse(b []byte, info *EntryInfo, d *data) (template.HTML, bool) { + opt := defaultMarkdownOption + opt.RenderLink = renderLinkFunc(d.URL) + var dd string + var err error splits := strings.SplitN(string(b), "---", 2) if len(splits) == 2 && info != nil { - err := toml.Unmarshal([]byte(splits[0]), info) + err = toml.Unmarshal([]byte(splits[0]), info) if err != nil { slog.Warn("parsing entry info", "error", err) } else { + info.Description, err = markdown.Parse(string(info.Description), &opt) dd = splits[1] } } else { dd = string(b) } - opt := new(markdown.Option) - opt.ImageSource = getStatic - opt.RenderLink = renderLinkFunc(d.URL) - content, err := markdown.Parse(dd, opt) + var errMd *markdown.ParseError errors.As(err, &errMd) + var content template.HTML + if errMd == nil { + content, err = markdown.Parse(dd, &opt) + errors.As(err, &errMd) + } if errMd != nil { slog.Error("parsing markdown") fmt.Println(errMd.Pretty()) return "", false } - d.PageDescription = info.Description + d.PageDescription = html.UnescapeString(string(info.Description)) d.title = info.Title d.Image = info.Img.Src return content, true diff --git a/backend/templates/components.html b/backend/templates/components.html index b4ffbe6..d9a653f 100644 --- a/backend/templates/components.html +++ b/backend/templates/components.html @@ -7,7 +7,7 @@ <a href="/{{ $uri }}/{{ .Slug }}"><img src="{{ static .Img.Src }}" alt="{{ .Img.Alt }}" /></a> <figcaption>{{ .Img.Legend }}</figcaption> </figure> - <p>{{ .Description }}</p> + {{ .Description }} </article> {{ end }} <div class="pagination"> diff --git a/backend/templates/data.html b/backend/templates/data.html index a384459..6fe15c4 100644 --- a/backend/templates/data.html +++ b/backend/templates/data.html @@ -1,7 +1,7 @@ {{define "body"}} <article id="content"> <h1>{{ .DataTitle }}</h1> - <p>{{ .Description }}</p> + {{ .Description }} <figure> <img src="{{ static .Img.Src }}" alt="{{ .Img.Alt }}" class="large" /> <figcaption>{{ .Img.Legend }}</figcaption> 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 != "<p>bonsoir</p>" { + t.Errorf("failed, got %s", c) + } + c, err = Parse("bonsoir~!", opt) + if err != nil { + t.Fatal(err) + } + if c != "<p>bonsoir !</p>" { + 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) + } +} |
