package glamour import ( "regexp" "strings" "testing" "charm.land/glamour/v2/ansi" "charm.land/glamour/v2/styles" "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" ) var kindShout = ast.NewNodeKind("Shout") // shout is %word%, a custom inline node for the extension tests. type shout struct { ast.BaseInline word string } func (n *shout) Kind() ast.NodeKind { return kindShout } func (n *shout) Dump(source []byte, level int) { ast.DumpHelper(n, source, level, nil, nil) } // shoutExtension parses %word% into a shout, or into an ast.String when // asString: a kind glamour registers but has no case for. type shoutExtension struct{ asString bool } func (e shoutExtension) Extend(m goldmark.Markdown) { m.Parser().AddOptions(parser.WithInlineParsers(util.Prioritized(shoutParser(e), 500))) } type shoutParser struct{ asString bool } func (shoutParser) Trigger() []byte { return []byte{'%'} } func (p shoutParser) Parse(_ ast.Node, block text.Reader, _ parser.Context) ast.Node { line, _ := block.PeekLine() end := strings.IndexByte(string(line[1:]), '%') if end < 1 { return nil } block.Advance(end + 2) if p.asString { return ast.NewString(line[1 : end+1]) } return &shout{word: string(line[1 : end+1])} } func renderShout(t *testing.T, in string, extension shoutExtension, element ansi.ElementFunc) string { t.Helper() options := []TermRendererOption{ WithStyles(styles.NoTTYStyleConfig), WithWordWrap(40), WithExtensions(extension), } if element != nil { options = append(options, WithElement(kindShout, element)) } r, err := NewTermRenderer(options...) if err != nil { t.Fatal(err) } out, err := r.Render(in) if err != nil { t.Fatal(err) } return strings.TrimSpace(out) } func TestWithElementRendersExtensionNodes(t *testing.T) { upper := func(node ast.Node, _ []byte, _ ansi.RenderContext) ansi.Element { return ansi.Element{Renderer: &ansi.BaseElement{Token: strings.ToUpper(node.(*shout).word), Verbatim: true}} } for in, want := range map[string]string{ "say %hi% now": "say HI now", "say *%hi%*": "say *HI*", `%a\[b%`: `A\[B`, } { if got := renderShout(t, in, shoutExtension{}, upper); got != want { t.Errorf("%q rendered %q, want %q", in, got, want) } } } func TestElementsSeeTheBlockWidth(t *testing.T) { var widths []int width := func(_ ast.Node, _ []byte, ctx ansi.RenderContext) ansi.Element { widths = append(widths, ctx.Width()) return ansi.Element{} } renderShout(t, "%a%\n\n> %b%", shoutExtension{}, width) if len(widths) < 2 || widths[0] <= widths[len(widths)-1] { t.Errorf("widths %v: a quote should leave less room than the document", widths) } } func TestUnhandledKindsRenderNothing(t *testing.T) { if got := renderShout(t, "say %hi% now", shoutExtension{asString: true}, nil); got != "say now" { t.Errorf("rendered %q, want the unknown node dropped", got) } } var trailingSpace = regexp.MustCompile(`(?m) +$`) func TestListItemsHangTheirRows(t *testing.T) { r, err := NewTermRenderer(WithStyles(styles.NoTTYStyleConfig), WithWordWrap(24)) if err != nil { t.Fatal(err) } out, err := r.Render("- one two three four five six\n\n second paragraph\n\n ```\n code\n ```\n- next") if err != nil { t.Fatal(err) } want := " • one two three four\n five six\n\n second paragraph\n\n code\n • next" if got := strings.Trim(trailingSpace.ReplaceAllString(out, ""), "\n"); got != want { t.Errorf("rendered\n%s\nwant\n%s", got, want) } }