fix(markdown): fix nested lists and simplify the renderer
This commit is contained in:
parent
f8141b8f8d
commit
b9e310e7fa
2 changed files with 91 additions and 109 deletions
|
|
@ -17,9 +17,6 @@ import (
|
||||||
|
|
||||||
type Renderer struct {
|
type Renderer struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
|
|
||||||
listIx *int
|
|
||||||
listNested int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeBlockIndent = " "
|
const codeBlockIndent = " "
|
||||||
|
|
@ -29,12 +26,10 @@ func NewRenderer(cfg *config.Config) *Renderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Renderer) RenderText(source []byte, node ast.Node, base tview.Style) richtext.Text {
|
func (r *Renderer) RenderText(source []byte, node ast.Node, base tview.Style) richtext.Text {
|
||||||
r.listIx = nil
|
|
||||||
r.listNested = 0
|
|
||||||
|
|
||||||
builder := new(richtext.Builder)
|
builder := new(richtext.Builder)
|
||||||
styleStack := []tview.Style{base}
|
styleStack := []tview.Style{base}
|
||||||
linkDepth := 0
|
linkDepth := 0
|
||||||
|
listDepth := 0
|
||||||
|
|
||||||
currentStyle := func() tview.Style {
|
currentStyle := func() tview.Style {
|
||||||
return styleStack[len(styleStack)-1]
|
return styleStack[len(styleStack)-1]
|
||||||
|
|
@ -50,94 +45,66 @@ func (r *Renderer) RenderText(source []byte, node ast.Node, base tview.Style) ri
|
||||||
|
|
||||||
theme := r.cfg.Theme.MessagesList
|
theme := r.cfg.Theme.MessagesList
|
||||||
_ = ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
|
_ = ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
switch node := node.(type) {
|
if !entering {
|
||||||
case *ast.Document:
|
switch node := node.(type) {
|
||||||
// noop
|
case *ast.Heading:
|
||||||
case *ast.Heading:
|
|
||||||
if entering {
|
|
||||||
builder.Write(strings.Repeat("#", node.Level)+" ", currentStyle())
|
|
||||||
} else {
|
|
||||||
builder.NewLine()
|
builder.NewLine()
|
||||||
}
|
case *ast.ListItem:
|
||||||
case *ast.Text:
|
// A list that the item ends with already ended the line.
|
||||||
if entering {
|
if _, ok := node.LastChild().(*ast.List); !ok {
|
||||||
builder.Write(string(node.Value.Bytes(source)), currentStyle())
|
|
||||||
switch {
|
|
||||||
case node.HardLineBreak():
|
|
||||||
builder.NewLine()
|
|
||||||
builder.NewLine()
|
|
||||||
case node.SoftLineBreak():
|
|
||||||
builder.NewLine()
|
builder.NewLine()
|
||||||
}
|
}
|
||||||
|
case *ast.Link:
|
||||||
|
linkDepth--
|
||||||
|
popStyle()
|
||||||
|
case *ast.List:
|
||||||
|
listDepth--
|
||||||
|
case *md.Inline:
|
||||||
|
if !r.shouldMask(node) {
|
||||||
|
popStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch node := node.(type) {
|
||||||
|
case *ast.Heading:
|
||||||
|
builder.Write(strings.Repeat("#", node.Level)+" ", currentStyle())
|
||||||
|
case *ast.Text:
|
||||||
|
builder.Write(string(node.Value.Bytes(source)), currentStyle())
|
||||||
|
switch {
|
||||||
|
case node.HardLineBreak():
|
||||||
|
builder.NewLine()
|
||||||
|
builder.NewLine()
|
||||||
|
case node.SoftLineBreak():
|
||||||
|
builder.NewLine()
|
||||||
}
|
}
|
||||||
case *ast.CodeBlock:
|
case *ast.CodeBlock:
|
||||||
if entering {
|
builder.NewLine()
|
||||||
builder.NewLine()
|
r.renderCodeBlock(builder, source, node, currentStyle())
|
||||||
r.renderCodeBlock(builder, source, node, currentStyle())
|
|
||||||
}
|
|
||||||
case *ast.AutoLink:
|
case *ast.AutoLink:
|
||||||
if entering {
|
url := string(node.Destination.Bytes(source))
|
||||||
url := string(node.Destination.Bytes(source))
|
style := tview.MergeStyle(currentStyle(), theme.URLStyle.Style).Url(url)
|
||||||
style := tview.MergeStyle(currentStyle(), theme.URLStyle.Style).Url(url)
|
builder.Write(url, style)
|
||||||
builder.Write(url, style)
|
|
||||||
}
|
|
||||||
case *ast.Link:
|
case *ast.Link:
|
||||||
if entering {
|
url := string(node.Destination.Bytes(source))
|
||||||
url := string(node.Destination.Bytes(source))
|
linkDepth++
|
||||||
linkDepth++
|
pushStyle(tview.MergeStyle(currentStyle(), theme.URLStyle.Style).Url(url))
|
||||||
pushStyle(tview.MergeStyle(currentStyle(), theme.URLStyle.Style).Url(url))
|
|
||||||
} else {
|
|
||||||
if linkDepth > 0 {
|
|
||||||
linkDepth--
|
|
||||||
}
|
|
||||||
popStyle()
|
|
||||||
}
|
|
||||||
case *ast.List:
|
case *ast.List:
|
||||||
if node.IsOrdered() {
|
builder.NewLine()
|
||||||
start := node.Start
|
listDepth++
|
||||||
r.listIx = &start
|
|
||||||
} else {
|
|
||||||
r.listIx = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if entering {
|
|
||||||
builder.NewLine()
|
|
||||||
r.listNested++
|
|
||||||
} else {
|
|
||||||
r.listNested--
|
|
||||||
}
|
|
||||||
case *ast.ListItem:
|
case *ast.ListItem:
|
||||||
if entering {
|
builder.Write(strings.Repeat(" ", listDepth-1)+listMarker(node), currentStyle())
|
||||||
builder.Write(strings.Repeat(" ", r.listNested-1), currentStyle())
|
|
||||||
if r.listIx != nil {
|
|
||||||
builder.Write(strconv.Itoa(*r.listIx)+". ", currentStyle())
|
|
||||||
*r.listIx++
|
|
||||||
} else {
|
|
||||||
builder.Write("- ", currentStyle())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
builder.NewLine()
|
|
||||||
}
|
|
||||||
case *md.Inline:
|
case *md.Inline:
|
||||||
if r.cfg.Markdown.MaskSpoilers && node.Attr&md.AttrSpoiler != 0 {
|
if r.shouldMask(node) {
|
||||||
if entering {
|
builder.Write("[spoiler]", currentStyle())
|
||||||
builder.Write("[spoiler]", currentStyle())
|
|
||||||
}
|
|
||||||
return ast.WalkSkipChildren, nil
|
return ast.WalkSkipChildren, nil
|
||||||
}
|
}
|
||||||
if entering {
|
pushStyle(applyInlineAttr(currentStyle(), node.Attr, linkDepth > 0))
|
||||||
pushStyle(applyInlineAttr(currentStyle(), node.Attr, linkDepth > 0))
|
|
||||||
} else {
|
|
||||||
popStyle()
|
|
||||||
}
|
|
||||||
case *md.Mention:
|
case *md.Mention:
|
||||||
if entering {
|
builder.Write(mentionText(node), tview.MergeStyle(currentStyle(), theme.MentionStyle.Style))
|
||||||
builder.Write(mentionText(node), tview.MergeStyle(currentStyle(), theme.MentionStyle.Style))
|
|
||||||
}
|
|
||||||
case *md.Emoji:
|
case *md.Emoji:
|
||||||
if entering {
|
builder.Write(":"+node.Name+":", tview.MergeStyle(currentStyle(), theme.EmojiStyle.Style))
|
||||||
builder.Write(":"+node.Name+":", tview.MergeStyle(currentStyle(), theme.EmojiStyle.Style))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ast.WalkContinue, nil
|
return ast.WalkContinue, nil
|
||||||
})
|
})
|
||||||
|
|
@ -214,6 +181,25 @@ func (r *Renderer) renderCodeBlock(builder *richtext.Builder, source []byte, nod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shouldMask reports whether node is a spoiler shown as "[spoiler]" in place of what it hides.
|
||||||
|
func (r *Renderer) shouldMask(node *md.Inline) bool {
|
||||||
|
return r.cfg.Markdown.MaskSpoilers && node.Attr&md.AttrSpoiler != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// listMarker returns what goes before item: its number in an ordered list, counted from the list's start, or a dash.
|
||||||
|
func listMarker(item *ast.ListItem) string {
|
||||||
|
list := item.Parent().(*ast.List)
|
||||||
|
if !list.IsOrdered() {
|
||||||
|
return "- "
|
||||||
|
}
|
||||||
|
|
||||||
|
number := list.Start
|
||||||
|
for sibling := item.PreviousSibling(); sibling != nil; sibling = sibling.PreviousSibling() {
|
||||||
|
number++
|
||||||
|
}
|
||||||
|
return strconv.Itoa(number) + ". "
|
||||||
|
}
|
||||||
|
|
||||||
func applyChromaStyle(base tview.Style, entry chroma.StyleEntry) tview.Style {
|
func applyChromaStyle(base tview.Style, entry chroma.StyleEntry) tview.Style {
|
||||||
style := base
|
style := base
|
||||||
if entry.Colour.IsSet() {
|
if entry.Colour.IsSet() {
|
||||||
|
|
@ -224,23 +210,16 @@ func applyChromaStyle(base tview.Style, entry chroma.StyleEntry) tview.Style {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
// Token background colors are not applied, so code blocks keep the user's terminal/chat background.
|
// Token background colors are not applied, so code blocks keep the user's terminal/chat background.
|
||||||
switch entry.Bold {
|
|
||||||
case chroma.Yes:
|
// An attribute chroma passes on is left as it is.
|
||||||
style = style.Bold(true)
|
if entry.Bold != chroma.Pass {
|
||||||
case chroma.No:
|
style = style.Bold(entry.Bold == chroma.Yes)
|
||||||
style = style.Bold(false)
|
|
||||||
}
|
}
|
||||||
switch entry.Italic {
|
if entry.Italic != chroma.Pass {
|
||||||
case chroma.Yes:
|
style = style.Italic(entry.Italic == chroma.Yes)
|
||||||
style = style.Italic(true)
|
|
||||||
case chroma.No:
|
|
||||||
style = style.Italic(false)
|
|
||||||
}
|
}
|
||||||
switch entry.Underline {
|
if entry.Underline != chroma.Pass {
|
||||||
case chroma.Yes:
|
style = style.Underline(entry.Underline == chroma.Yes)
|
||||||
style = style.Underline(true)
|
|
||||||
case chroma.No:
|
|
||||||
style = style.Underline(false)
|
|
||||||
}
|
}
|
||||||
return style
|
return style
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,27 +10,30 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRendererRenderText(t *testing.T) {
|
func TestRendererRenderText(t *testing.T) {
|
||||||
source := []byte("before ||secret|| after")
|
|
||||||
node := md.Parse(source)
|
|
||||||
|
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
name string
|
name string
|
||||||
mask bool
|
source string
|
||||||
want string
|
mask bool
|
||||||
|
want string
|
||||||
}{
|
}{
|
||||||
{"spoilers_masked", true, "before [spoiler] after"},
|
{"spoilers_masked", "before ||secret|| after", true, "before [spoiler] after"},
|
||||||
{"spoilers_visible", false, "before secret after"},
|
{"spoilers_visible", "before ||secret|| after", false, "before secret after"},
|
||||||
|
{"ordered_list_after_nested", "1. a\n2. b\n - x\n - y\n3. c", false, "\n1. a\n2. b\n - x\n - y\n3. c"},
|
||||||
|
{"ordered_list_after_nested_ordered", "1. a\n 1. x\n 2. y\n2. b", false, "\n1. a\n 1. x\n 2. y\n2. b"},
|
||||||
|
{"list_nested_twice", "- a\n - b\n - c\n- d", false, "\n- a\n - b\n - c\n- d"},
|
||||||
|
{"ordered_list_start", "3. a\n4. b", false, "\n3. a\n4. b"},
|
||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
rendered := NewRenderer(&config.Config{Markdown: config.MarkdownConfig{MaskSpoilers: test.mask}}).RenderText(source, node, tcell.StyleDefault)
|
source := []byte(test.source)
|
||||||
var got strings.Builder
|
rendered := NewRenderer(&config.Config{Markdown: config.MarkdownConfig{MaskSpoilers: test.mask}}).RenderText(source, md.Parse(source), tcell.StyleDefault)
|
||||||
for _, line := range rendered {
|
lines := make([]string, len(rendered))
|
||||||
|
for i, line := range rendered {
|
||||||
for _, segment := range line {
|
for _, segment := range line {
|
||||||
got.WriteString(segment.Text)
|
lines[i] += segment.Text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if got.String() != test.want {
|
if got := strings.Join(lines, "\n"); got != test.want {
|
||||||
t.Fatalf("mask=%t: got %q, want %q", test.mask, got.String(), test.want)
|
t.Fatalf("got %q, want %q", got, test.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue