1
0

fix markdown escaping

This commit is contained in:
Arpad Ryszka 2025-12-08 00:41:08 +01:00
parent cb1b9806c3
commit fed32c8bbe
6 changed files with 114 additions and 61 deletions

View File

@ -96,39 +96,36 @@ func escapeMarkdownEdit(r rune, s mdEscapeState) ([]rune, mdEscapeState) {
switch r { switch r {
case '\\', '`', '*', '_', '[', ']', '#', '<', '>': case '\\', '`', '*', '_', '[', ']', '#', '<', '>':
ret = append(ret, '\\', r) ret = append(ret, '\\', r)
default: case '+', '-':
switch {
case s.lineStarted:
ret = append(ret, r)
default:
ret = append(ret, '\\', r)
}
case '.':
switch { switch {
case !s.lineStarted:
switch r {
case '+', '-':
ret = append(ret, '\\', r)
default:
ret = append(ret, r)
}
case s.numberOnNewLine: case s.numberOnNewLine:
switch r { ret = append(ret, '\\', r)
case '.':
ret = append(ret, '\\', r)
default:
ret = append(ret, r)
}
case s.linkClosed:
switch r {
case '(':
ret = append(ret, '\\', r)
default:
ret = append(ret, r)
}
case s.linkValue:
switch r {
case ')':
ret = append(ret, '\\', r)
default:
ret = append(ret, r)
}
default: default:
ret = append(ret, r) ret = append(ret, r)
} }
case '(':
switch {
case s.linkClosed:
ret = append(ret, '\\', r)
default:
ret = append(ret, r)
}
case ')':
switch {
case s.linkValue:
ret = append(ret, '\\', r)
default:
ret = append(ret, r)
}
default:
ret = append(ret, r)
} }
s.numberOnNewLine = (!s.lineStarted || s.numberOnNewLine) && r >= '0' && r <= '9' s.numberOnNewLine = (!s.lineStarted || s.numberOnNewLine) && r >= '0' && r <= '9'
@ -139,11 +136,12 @@ func escapeMarkdownEdit(r rune, s mdEscapeState) ([]rune, mdEscapeState) {
return ret, s return ret, s
} }
func escapeMarkdown() wrapper { func escapeMarkdown(s mdEscapeState) wrapper {
return editor( return editor(
textedit.Func( textedit.FuncInit(
escapeMarkdownEdit, escapeMarkdownEdit,
func(mdEscapeState) []rune { return nil }, func(mdEscapeState) []rune { return nil },
s,
), ),
) )
} }

2
go.mod
View File

@ -5,5 +5,5 @@ go 1.25.3
require ( require (
code.squareroundforest.org/arpio/html v0.0.0-20251102001159-f3efe9c7b176 code.squareroundforest.org/arpio/html v0.0.0-20251102001159-f3efe9c7b176
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2
code.squareroundforest.org/arpio/textedit v0.0.0-20251102002300-caf622f43f10 code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f
) )

4
go.sum
View File

@ -2,5 +2,5 @@ code.squareroundforest.org/arpio/html v0.0.0-20251102001159-f3efe9c7b176 h1:ynJ4
code.squareroundforest.org/arpio/html v0.0.0-20251102001159-f3efe9c7b176/go.mod h1:JKD2DXph0Zt975trJII7YbdhM2gL1YEHjsh5M1X63eA= code.squareroundforest.org/arpio/html v0.0.0-20251102001159-f3efe9c7b176/go.mod h1:JKD2DXph0Zt975trJII7YbdhM2gL1YEHjsh5M1X63eA=
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 h1:S4mjQHL70CuzFg1AGkr0o0d+4M+ZWM0sbnlYq6f0b3I= code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 h1:S4mjQHL70CuzFg1AGkr0o0d+4M+ZWM0sbnlYq6f0b3I=
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY= code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY=
code.squareroundforest.org/arpio/textedit v0.0.0-20251102002300-caf622f43f10 h1:u3hMmSBJzrSnJ+C7krjHFkCEVG6Ms9W6vX6F+Mk/KnY= code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f h1:gomu8xTD953IkL3M528qVEuZ2z93C2I6Hr4vyIwE7kI=
code.squareroundforest.org/arpio/textedit v0.0.0-20251102002300-caf622f43f10/go.mod h1:nXdFdxdI69JrkIT97f+AEE4OgplmxbgNFZC5j7gsdqs= code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f/go.mod h1:nXdFdxdI69JrkIT97f+AEE4OgplmxbgNFZC5j7gsdqs=

View File

@ -8,9 +8,9 @@ import (
"strings" "strings"
) )
func mdTextToString(text Txt) string { func mdTextToString(text Txt, es mdEscapeState) string {
var b bytes.Buffer var b bytes.Buffer
renderMDText(&b, text) renderMDText(&b, text, es)
return b.String() return b.String()
} }
@ -19,7 +19,7 @@ func mdCellTexts(rows []TableRow) [][]string {
for _, row := range rows { for _, row := range rows {
var rowTexts []string var rowTexts []string
for _, cell := range row.cells { for _, cell := range row.cells {
rowTexts = append(rowTexts, mdTextToString(cell.text)) rowTexts = append(rowTexts, mdTextToString(cell.text, mdEscapeState{lineStarted: false}))
} }
texts = append(texts, rowTexts) texts = append(texts, rowTexts)
@ -41,14 +41,16 @@ func mdEnsureHeaderTexts(h []string) []string {
return hh return hh
} }
func renderMDText(w io.Writer, text Txt) { func renderMDText(w io.Writer, text Txt, es mdEscapeState, wr ...wrapper) {
if len(text.cat) > 0 { if len(text.cat) > 0 {
for i, tc := range text.cat { for i, tc := range text.cat {
if i > 0 { if i > 0 {
write(w, " ") write(w, " ")
} }
renderMDText(w, tc) esi := es
esi.lineStarted = esi.lineStarted || i > 0
renderMDText(w, tc, esi, wr...)
} }
return return
@ -56,10 +58,12 @@ func renderMDText(w io.Writer, text Txt) {
if text.bold { if text.bold {
write(w, "**") write(w, "**")
es.lineStarted = true
} }
if text.italic { if text.italic {
write(w, "_") write(w, "_")
es.lineStarted = true
} }
defer func() { defer func() {
@ -72,30 +76,31 @@ func renderMDText(w io.Writer, text Txt) {
} }
}() }()
wr = append([]wrapper{singleLine()}, wr...)
if text.link == "" { if text.link == "" {
w, f := writeWith(w, singleLine(), escapeMarkdown()) w, f := writeWith(w, append(wr, escapeMarkdown(es))...)
write(w, text.text) write(w, text.text)
w, _ = f() w, _ = f()
return return
} }
if text.text == "" { if text.text == "" {
w, f := writeWith(w, singleLine(), escapeMarkdown()) w, f := writeWith(w, append(wr, escapeMarkdown(es))...)
write(w, text.link) write(w, text.link)
w, _ = f() w, _ = f()
return return
} }
write(w, "[") write(w, "[")
w, f := writeWith(w, singleLine(), escapeMarkdown()) es.lineStarted = true
w, f := writeWith(w, append(wr, escapeMarkdown(es))...)
write(w, text.text) write(w, text.text)
w, _ = f() w, _ = f()
write(w, "](") write(w, "](")
w, f = writeWith(w, singleLine(), escapeMarkdown()) w, f = writeWith(w, append(wr, escapeMarkdown(es))...)
write(w, text.link) write(w, text.link)
w, _ = f() w, _ = f()
write(w, ")") write(w, ")")
w, _ = f()
} }
func renderMDTitle(w io.Writer, e Entry) { func renderMDTitle(w io.Writer, e Entry) {
@ -105,26 +110,23 @@ func renderMDTitle(w io.Writer, e Entry) {
} }
write(w, timesn("#", hashes), " ") write(w, timesn("#", hashes), " ")
renderMDText(w, e.text) renderMDText(w, e.text, mdEscapeState{lineStarted: true})
} }
func renderMDParagraphIndent(w io.Writer, e Entry) { func renderMDParagraphIndent(w io.Writer, e Entry, es mdEscapeState) {
var f func() (io.Writer, error) var wr []wrapper
if e.wrapWidth > 0 { if e.wrapWidth > 0 {
indentFirst := e.indent + e.indentFirst indentFirst := e.indent + e.indentFirst
w, f = writeWith(w, wrapIndent(indentFirst, e.indent, e.wrapWidth, e.wrapWidth)) wr = []wrapper{wrapIndent(indentFirst, e.indent, e.wrapWidth, e.wrapWidth)}
} }
renderMDText(w, e.text) renderMDText(w, e.text, es, wr...)
if f != nil {
f()
}
} }
func renderMDParagraph(w io.Writer, e Entry) { func renderMDParagraph(w io.Writer, e Entry) {
e.indent = 0 e.indent = 0
e.indentFirst = 0 e.indentFirst = 0
renderMDParagraphIndent(w, e) renderMDParagraphIndent(w, e, mdEscapeState{})
} }
func renderMDList(w io.Writer, e Entry) { func renderMDList(w io.Writer, e Entry) {
@ -141,7 +143,7 @@ func renderMDList(w io.Writer, e Entry) {
write(w, "- ") write(w, "- ")
p := itemToParagraph(e, item.text) p := itemToParagraph(e, item.text)
renderMDParagraphIndent(w, p) renderMDParagraphIndent(w, p, mdEscapeState{lineStarted: true})
} }
} }
@ -160,7 +162,7 @@ func renderMDNumberedList(w io.Writer, e Entry) {
write(w, padRight(fmt.Sprintf("%d.", i+1), maxDigits+2)) write(w, padRight(fmt.Sprintf("%d.", i+1), maxDigits+2))
p := itemToParagraph(e, item.text) p := itemToParagraph(e, item.text)
renderMDParagraphIndent(w, p) renderMDParagraphIndent(w, p, mdEscapeState{lineStarted: true})
} }
} }
@ -168,7 +170,7 @@ func renderMDDefinitions(w io.Writer, e Entry) {
for _, d := range e.definitions { for _, d := range e.definitions {
e.items = append( e.items = append(
e.items, e.items,
Item(Cat(Text(fmt.Sprintf("%s:", d.name.text)), d.value)), Item(Text(fmt.Sprintf("%s: %s", d.name.text, d.value.text))),
) )
} }
@ -179,7 +181,7 @@ func renderMDNumberedDefinitions(w io.Writer, e Entry) {
for _, d := range e.definitions { for _, d := range e.definitions {
e.items = append( e.items = append(
e.items, e.items,
Item(Cat(Text(fmt.Sprintf("%s:", d.name.text)), d.value)), Item(Text(fmt.Sprintf("%s: %s", d.name.text, d.value.text))),
) )
} }
@ -303,9 +305,7 @@ func renderMDChoice(w io.Writer, s SyntaxItem) {
} }
func renderMDSymbol(w io.Writer, s SyntaxItem) { func renderMDSymbol(w io.Writer, s SyntaxItem) {
w, f := writeWith(w, escapeMarkdown())
write(w, s.symbol) write(w, s.symbol)
f()
} }
func renderMDSyntaxItem(w io.Writer, s SyntaxItem) { func renderMDSyntaxItem(w io.Writer, s SyntaxItem) {

View File

@ -550,6 +550,31 @@ textfmt supports the following entries:
} }
}) })
t.Run("non line start dash", func(t *testing.T) {
doc := textfmt.Doc(
textfmt.List(
textfmt.Item(textfmt.Text("--foo one")),
textfmt.Item(textfmt.Text("--bar two")),
textfmt.Item(textfmt.Text("--baz three")),
),
)
var b bytes.Buffer
if err := textfmt.Markdown(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `
- --foo one
- --bar two
- --baz three
`
if "\n"+b.String() != expect {
t.Fatal("\n" + b.String())
}
})
t.Run("indent ignored", func(t *testing.T) { t.Run("indent ignored", func(t *testing.T) {
doc := textfmt.Doc( doc := textfmt.Doc(
textfmt.Indent( textfmt.Indent(
@ -1509,7 +1534,11 @@ textfmt supports the following entries:
return textfmt.Document( return textfmt.Document(
textfmt.Paragraph(textfmt.Text("Hello, world!")), textfmt.Paragraph(textfmt.Text("Hello, world!")),
) )
}` }
/*
- no escape: -` + "`" + `[*]` + "`" + `
*/`
var b bytes.Buffer var b bytes.Buffer
if err := textfmt.Markdown(&b, textfmt.Doc(textfmt.CodeBlock(code))); err != nil { if err := textfmt.Markdown(&b, textfmt.Doc(textfmt.CodeBlock(code))); err != nil {
@ -1773,5 +1802,31 @@ textfmt supports the following entries:
t.Fatal(b.String()) t.Fatal(b.String())
} }
}) })
t.Run("no escape", func(t *testing.T) {
doc := textfmt.Doc(
textfmt.Syntax(
textfmt.Symbol("foo"),
textfmt.ZeroOrMore(textfmt.Symbol("options")),
textfmt.Optional(textfmt.Symbol("--")),
textfmt.Required(textfmt.Symbol("filename")),
textfmt.ZeroOrMore(
textfmt.Choice(
textfmt.Symbol("string"),
textfmt.Symbol("number"),
),
),
),
)
var b bytes.Buffer
if err := textfmt.Markdown(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != "```\nfoo [options]... [--] <filename> [string|number]...\n```\n" {
t.Fatal(b.String())
}
})
}) })
} }

View File

@ -25,12 +25,12 @@ func (w *errorWriter) Write(p []byte) (int, error) {
} }
func writeWith(out io.Writer, w ...wrapper) (io.Writer, func() (io.Writer, error)) { func writeWith(out io.Writer, w ...wrapper) (io.Writer, func() (io.Writer, error)) {
var f []func() error f := make([]func() error, len(w))
ww := out ww := out
for i := len(w) - 1; i >= 0; i-- { for i := len(w) - 1; i >= 0; i-- {
var fi func() error var fi func() error
ww, fi = w[i](ww) ww, fi = w[i](ww)
f = append(f, fi) f[i] = fi
} }
return ww, func() (io.Writer, error) { return ww, func() (io.Writer, error) {