Skip to content

Commit

Permalink
commonmark: allow configuration of empty href/content behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
JohannesKaufmann committed Dec 15, 2024
1 parent 130f633 commit 057c7a9
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 3 deletions.
28 changes: 28 additions & 0 deletions plugin/commonmark/commonmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,34 @@ func WithHeadingStyle(style headingStyle) OptionFunc {
}
}

// WithLinkEmptyHrefBehavior configures how links with *empty hrefs* are rendered.
// Take for example:
//
// <a href="">the link content</a>
//
// LinkBehaviorRenderAsLink would result in "[the link content]()""
//
// LinkBehaviorSkipLink would result in "the link content"
func WithLinkEmptyHrefBehavior(behavior linkRenderingBehavior) OptionFunc {
return func(config *config) {
config.LinkEmptyHrefBehavior = behavior
}
}

// WithLinkEmptyContentBehavior configures how links *without content* are rendered.
// Take for example:
//
// <a href="/page"></a>
//
// LinkBehaviorRenderAsLink would result in "[](/page)""
//
// LinkBehaviorSkipLink would result in an empty string.
func WithLinkEmptyContentBehavior(behavior linkRenderingBehavior) OptionFunc {
return func(config *config) {
config.LinkEmptyContentBehavior = behavior
}
}

// TODO: allow changing the link style once the render logic is implemented
//
// "inlined" or "referenced_index" or "referenced_short"
Expand Down
40 changes: 40 additions & 0 deletions plugin/commonmark/commonmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func TestOptionFunc(t *testing.T) {
options []commonmark.OptionFunc
expected string
}{
// - - - - - - - - - - Italic & Bold - - - - - - - - - - //
{
desc: "WithEmDelimiter",
options: []commonmark.OptionFunc{
Expand All @@ -65,6 +66,7 @@ func TestOptionFunc(t *testing.T) {
expected: `__bold__`,
},

// - - - - - - - - - - Horizontal Rule - - - - - - - - - - //
{
desc: "WithHorizontalRule(***)",
options: []commonmark.OptionFunc{
Expand Down Expand Up @@ -98,6 +100,7 @@ func TestOptionFunc(t *testing.T) {
expected: `___`,
},

// - - - - - - - - - - List - - - - - - - - - - //
{
desc: "WithBulletListMarker(+)",
options: []commonmark.OptionFunc{
Expand All @@ -124,6 +127,7 @@ func TestOptionFunc(t *testing.T) {
expected: "* list a\n\n* list b",
},

// - - - - - - - - - - Code - - - - - - - - - - //
{
desc: "WithCodeBlockFence",
options: []commonmark.OptionFunc{
Expand All @@ -133,6 +137,7 @@ func TestOptionFunc(t *testing.T) {
expected: "~~~\nhello world\n~~~",
},

// - - - - - - - - - - Heading - - - - - - - - - - //
{
desc: "WithHeadingStyle(atx)",
options: []commonmark.OptionFunc{
Expand All @@ -150,6 +155,41 @@ func TestOptionFunc(t *testing.T) {
expected: "important\n\\\nheading\n=========",
},

// - - - - - - - - - - Link - - - - - - - - - - //
{
desc: "WithLinkEmptyHrefBehavior(render)",
options: []commonmark.OptionFunc{
commonmark.WithLinkEmptyHrefBehavior("render"),
},
input: `<a href="">the link content</a>`,
expected: "[the link content]()",
},
{
desc: "WithLinkEmptyHrefBehavior(skip)",
options: []commonmark.OptionFunc{
commonmark.WithLinkEmptyHrefBehavior("skip"),
},
input: `<a href="">the link content</a>`,
expected: "the link content",
},
// - - - //
{
desc: "WithLinkEmptyContentBehavior(render)",
options: []commonmark.OptionFunc{
commonmark.WithLinkEmptyContentBehavior("render"),
},
input: `<a href="/page"></a>`,
expected: "[](/page)",
},
{
desc: "WithLinkEmptyContentBehavior(skip)",
options: []commonmark.OptionFunc{
commonmark.WithLinkEmptyContentBehavior("skip"),
},
input: `<a href="/page"></a>`,
expected: "",
},

// TODO: handle other link styles
// {
// desc: "WithLinkStyle(LinkInlined)",
Expand Down
18 changes: 18 additions & 0 deletions plugin/commonmark/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ const (
HeadingStyleSetext headingStyle = "setext"
)

type linkRenderingBehavior string

const (
// LinkBehaviorRenderAsLink renders the element as a link
LinkBehaviorRenderAsLink linkRenderingBehavior = "render"
// LinkBehaviorSkipLink skips link rendering and falls back to the other rules (e.g. paragraph)
LinkBehaviorSkipLink linkRenderingBehavior = "skip"
)

// config to customize the output. You can change stuff like
// the character that is used for strong text.
type config struct {
Expand Down Expand Up @@ -73,6 +82,9 @@ type config struct {
//
// default: inlined
LinkStyle linkStyle

LinkEmptyHrefBehavior linkRenderingBehavior
LinkEmptyContentBehavior linkRenderingBehavior
}

func fillInDefaultConfig(cfg *config) config {
Expand Down Expand Up @@ -104,6 +116,12 @@ func fillInDefaultConfig(cfg *config) config {
cfg.HeadingStyle = "atx"
}

if cfg.LinkEmptyHrefBehavior == "" {
cfg.LinkEmptyHrefBehavior = LinkBehaviorRenderAsLink
}
if cfg.LinkEmptyContentBehavior == "" {
cfg.LinkEmptyContentBehavior = LinkBehaviorRenderAsLink
}
if cfg.LinkStyle == "" {
cfg.LinkStyle = LinkStyleInlined
}
Expand Down
14 changes: 11 additions & 3 deletions plugin/commonmark/render_link.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ func (c *commonmark) renderLink(ctx converter.Context, w converter.Writer, n *ht
href = strings.TrimSpace(href)
href = ctx.AssembleAbsoluteURL(ctx, "a", href)

if href == "" && c.config.LinkEmptyHrefBehavior == LinkBehaviorSkipLink {
// There is *no href* for the link. Now we have two options:
// Continue rendering as a link OR skip to let other renderers take over.
return converter.RenderTryNext
}

title := dom.GetAttributeOr(n, "title", "")
title = strings.ReplaceAll(title, "\n", " ")

Expand All @@ -66,12 +72,14 @@ func (c *commonmark) renderLink(ctx converter.Context, w converter.Writer, n *ht
ctx.RenderChildNodes(ctx, &buf, n)
content := buf.Bytes()

if bytes.TrimFunc(content, marker.IsSpace) == nil {
if len(bytes.TrimFunc(content, marker.IsSpace)) == 0 {
// Fallback to the title
content = []byte(l.title)
}
if bytes.TrimSpace(content) == nil {
return converter.RenderSuccess
if len(bytes.TrimSpace(content)) == 0 && c.config.LinkEmptyContentBehavior == LinkBehaviorSkipLink {
// There is *no content* inside the link. Now we have two options:
// Continue rendering as a link OR skip to let other renderers take over.
return converter.RenderTryNext
}

if l.href == "" {
Expand Down
10 changes: 10 additions & 0 deletions plugin/commonmark/testdata/GoldenFiles/link.out.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@

<!--no content-->

[](/no_content)

[](/no_content)

[](/no_content)

[](/no_content)

[](/no_content)

<!--no content but fallback-->

[link title](/no_content "link title")
Expand Down

0 comments on commit 057c7a9

Please sign in to comment.