fix markdown escaping
This commit is contained in:
parent
cb1b9806c3
commit
fed32c8bbe
28
escape.go
28
escape.go
@ -96,32 +96,30 @@ func escapeMarkdownEdit(r rune, s mdEscapeState) ([]rune, mdEscapeState) {
|
|||||||
switch r {
|
switch r {
|
||||||
case '\\', '`', '*', '_', '[', ']', '#', '<', '>':
|
case '\\', '`', '*', '_', '[', ']', '#', '<', '>':
|
||||||
ret = append(ret, '\\', r)
|
ret = append(ret, '\\', r)
|
||||||
default:
|
|
||||||
switch {
|
|
||||||
case !s.lineStarted:
|
|
||||||
switch r {
|
|
||||||
case '+', '-':
|
case '+', '-':
|
||||||
ret = append(ret, '\\', r)
|
switch {
|
||||||
default:
|
case s.lineStarted:
|
||||||
ret = append(ret, r)
|
ret = append(ret, r)
|
||||||
|
default:
|
||||||
|
ret = append(ret, '\\', r)
|
||||||
}
|
}
|
||||||
case s.numberOnNewLine:
|
|
||||||
switch r {
|
|
||||||
case '.':
|
case '.':
|
||||||
|
switch {
|
||||||
|
case s.numberOnNewLine:
|
||||||
ret = append(ret, '\\', r)
|
ret = append(ret, '\\', r)
|
||||||
default:
|
default:
|
||||||
ret = append(ret, r)
|
ret = append(ret, r)
|
||||||
}
|
}
|
||||||
case s.linkClosed:
|
|
||||||
switch r {
|
|
||||||
case '(':
|
case '(':
|
||||||
|
switch {
|
||||||
|
case s.linkClosed:
|
||||||
ret = append(ret, '\\', r)
|
ret = append(ret, '\\', r)
|
||||||
default:
|
default:
|
||||||
ret = append(ret, r)
|
ret = append(ret, r)
|
||||||
}
|
}
|
||||||
case s.linkValue:
|
|
||||||
switch r {
|
|
||||||
case ')':
|
case ')':
|
||||||
|
switch {
|
||||||
|
case s.linkValue:
|
||||||
ret = append(ret, '\\', r)
|
ret = append(ret, '\\', r)
|
||||||
default:
|
default:
|
||||||
ret = append(ret, r)
|
ret = append(ret, r)
|
||||||
@ -129,7 +127,6 @@ func escapeMarkdownEdit(r rune, s mdEscapeState) ([]rune, mdEscapeState) {
|
|||||||
default:
|
default:
|
||||||
ret = append(ret, r)
|
ret = append(ret, r)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
s.numberOnNewLine = (!s.lineStarted || s.numberOnNewLine) && r >= '0' && r <= '9'
|
s.numberOnNewLine = (!s.lineStarted || s.numberOnNewLine) && r >= '0' && r <= '9'
|
||||||
s.lineStarted = r != '\n'
|
s.lineStarted = r != '\n'
|
||||||
@ -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
2
go.mod
@ -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
4
go.sum
@ -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=
|
||||||
|
|||||||
50
markdown.go
50
markdown.go
@ -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) {
|
||||||
|
|||||||
@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
4
write.go
4
write.go
@ -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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user