From 3b6239c4660ec833be4e241e70564eeeee4b60c0 Mon Sep 17 00:00:00 2001
From: Xavier Coulon
Date: Wed, 20 Apr 2022 20:22:07 +0200
Subject: [PATCH] feat(renderer): support caret in links
Fixes #391
Signed-off-by: Xavier Coulon
---
pkg/parser/link_test.go | 262 +++++++++++++++++++++++----
pkg/renderer/sgml/html5/link.go | 2 +-
pkg/renderer/sgml/html5/link_test.go | 135 ++++++++------
pkg/renderer/sgml/link.go | 24 ++-
pkg/types/types.go | 5 +
5 files changed, 330 insertions(+), 98 deletions(-)
diff --git a/pkg/parser/link_test.go b/pkg/parser/link_test.go
index 4928e240..d4849ea1 100644
--- a/pkg/parser/link_test.go
+++ b/pkg/parser/link_test.go
@@ -270,7 +270,7 @@ a link to <{example}>.`
})
It("with text only", func() {
- source := "a link to mailto:foo@bar[the foo@bar email]."
+ source := "a link to mailto:hello@example.com[the hello@example.com email]."
expected := &types.Document{
Elements: []interface{}{
&types.Paragraph{
@@ -279,10 +279,10 @@ a link to <{example}>.`
&types.InlineLink{
Location: &types.Location{
Scheme: "mailto:",
- Path: "foo@bar",
+ Path: "hello@example.com",
},
Attributes: types.Attributes{
- types.AttrInlineLinkText: "the foo@bar email",
+ types.AttrInlineLinkText: "the hello@example.com email",
},
},
&types.StringElement{
@@ -296,7 +296,7 @@ a link to <{example}>.`
})
It("with text and extra attributes", func() {
- source := "a link to mailto:foo@bar[the foo@bar email, foo=bar]"
+ source := "a link to mailto:hello@example.com[the hello@example.com email, foo=bar]"
expected := &types.Document{
Elements: []interface{}{
&types.Paragraph{
@@ -305,10 +305,10 @@ a link to <{example}>.`
&types.InlineLink{
Location: &types.Location{
Scheme: "mailto:",
- Path: "foo@bar",
+ Path: "hello@example.com",
},
Attributes: types.Attributes{
- types.AttrInlineLinkText: "the foo@bar email",
+ types.AttrInlineLinkText: "the hello@example.com email",
"foo": "bar",
},
},
@@ -396,6 +396,103 @@ next lines`
Expect(ParseDocument(source)).To(MatchDocument(expected))
})
+ It("with text and custom target", func() {
+ source := `a link to https://example.com[the doc,window=read-later]`
+ expected := &types.Document{
+ Elements: []interface{}{
+ &types.Paragraph{
+ Elements: []interface{}{
+ &types.StringElement{Content: "a link to "},
+ &types.InlineLink{
+ Attributes: types.Attributes{
+ types.AttrInlineLinkText: "the doc",
+ types.AttrInlineLinkTarget: "read-later",
+ },
+ Location: &types.Location{
+ Scheme: "https://",
+ Path: "example.com",
+ },
+ },
+ },
+ },
+ },
+ }
+ Expect(ParseDocument(source)).To(MatchDocument(expected))
+ })
+
+ It("with text and custom target with noopener", func() {
+ source := `a link to https://example.com[the doc,window=read-later,opts=noopener]`
+ expected := &types.Document{
+ Elements: []interface{}{
+ &types.Paragraph{
+ Elements: []interface{}{
+ &types.StringElement{Content: "a link to "},
+ &types.InlineLink{
+ Attributes: types.Attributes{
+ types.AttrInlineLinkText: "the doc",
+ types.AttrInlineLinkTarget: "read-later",
+ types.AttrOptions: types.Options{"noopener"},
+ },
+ Location: &types.Location{
+ Scheme: "https://",
+ Path: "example.com",
+ },
+ },
+ },
+ },
+ },
+ }
+ Expect(ParseDocument(source)).To(MatchDocument(expected))
+ })
+
+ It("with text and explicit blank target", func() {
+ source := `a link to https://example.com[the doc,window=_blank]`
+ expected := &types.Document{
+ Elements: []interface{}{
+ &types.Paragraph{
+ Elements: []interface{}{
+ &types.StringElement{Content: "a link to "},
+ &types.InlineLink{
+ Attributes: types.Attributes{
+ types.AttrInlineLinkText: "the doc",
+ types.AttrInlineLinkTarget: "_blank",
+ },
+ Location: &types.Location{
+ Scheme: "https://",
+ Path: "example.com",
+ },
+ },
+ },
+ },
+ },
+ }
+ Expect(ParseDocument(source)).To(MatchDocument(expected))
+ })
+
+ It("with text and blank target short-hand", func() {
+ source := `a link to https://example.com[the doc^]` // the ^ character is used to define a `blank` target
+ expected := &types.Document{
+ Elements: []interface{}{
+ &types.Paragraph{
+ Elements: []interface{}{
+ &types.StringElement{Content: "a link to "},
+ &types.InlineLink{
+ Attributes: types.Attributes{
+ types.AttrInlineLinkText: "the doc",
+ types.AttrInlineLinkTarget: "_blank",
+ },
+ Location: &types.Location{
+ Scheme: "https://",
+ Path: "example.com",
+ },
+ },
+ },
+ },
+ },
+ }
+ Expect(ParseDocument(source)).To(MatchDocument(expected))
+ })
+
Context("text attribute with comma", func() {
It("only with text having comma", func() {
@@ -1059,7 +1156,7 @@ a link to {scheme}://{path} and https://foo.com`
Context("relative links", func() {
It("to doc without text", func() {
- source := "a link to link:foo.adoc[]"
+ source := "a link to link:https://example.com[]"
expected := &types.Document{
Elements: []interface{}{
&types.Paragraph{
@@ -1067,8 +1164,8 @@ a link to {scheme}://{path} and https://foo.com`
&types.StringElement{Content: "a link to "},
&types.InlineLink{
Location: &types.Location{
- Scheme: "",
- Path: "foo.adoc",
+ Scheme: "https://",
+ Path: "example.com",
},
},
},
@@ -1079,7 +1176,7 @@ a link to {scheme}://{path} and https://foo.com`
})
It("to doc with text", func() {
- source := "a link to link:foo.adoc[foo doc]"
+ source := "a link to link:https://example.com[the doc]"
expected := &types.Document{
Elements: []interface{}{
&types.Paragraph{
@@ -1087,11 +1184,11 @@ a link to {scheme}://{path} and https://foo.com`
&types.StringElement{Content: "a link to "},
&types.InlineLink{
Location: &types.Location{
- Scheme: "",
- Path: "foo.adoc",
+ Scheme: "https://",
+ Path: "example.com",
},
Attributes: types.Attributes{
- types.AttrInlineLinkText: "foo doc",
+ types.AttrInlineLinkText: "the doc",
},
},
},
@@ -1102,7 +1199,7 @@ a link to {scheme}://{path} and https://foo.com`
})
It("to external URL with text only", func() {
- source := "a link to link:https://example.com[foo doc]"
+ source := "a link to link:https://example.com[the doc]"
expected := &types.Document{
Elements: []interface{}{
&types.Paragraph{
@@ -1114,7 +1211,7 @@ a link to {scheme}://{path} and https://foo.com`
Path: "example.com",
},
Attributes: types.Attributes{
- types.AttrInlineLinkText: "foo doc",
+ types.AttrInlineLinkText: "the doc",
},
},
},
@@ -1125,7 +1222,7 @@ a link to {scheme}://{path} and https://foo.com`
})
It("to external URL with text and extra attributes", func() {
- source := "a link to link:https://example.com[foo doc, foo=bar]"
+ source := "a link to link:https://example.com[the doc, foo=bar]"
expected := &types.Document{
Elements: []interface{}{
&types.Paragraph{
@@ -1137,7 +1234,7 @@ a link to {scheme}://{path} and https://foo.com`
Path: "example.com",
},
Attributes: types.Attributes{
- types.AttrInlineLinkText: "foo doc",
+ types.AttrInlineLinkText: "the doc",
"foo": "bar",
},
},
@@ -1171,21 +1268,22 @@ a link to {scheme}://{path} and https://foo.com`
Expect(ParseDocument(source)).To(MatchDocument(expected))
})
- It("invalid syntax", func() {
- source := "a link to link:foo.adoc"
- expected := &types.Document{
- Elements: []interface{}{
- &types.Paragraph{
- Elements: []interface{}{
- &types.StringElement{
- Content: "a link to link:foo.adoc",
- },
- },
- },
- },
- }
- Expect(ParseDocument(source)).To(MatchDocument(expected))
- })
+ // skipped: should be consistent with other syntaxes, and allow for skipped/missing attributes?
+ // It("invalid syntax", func() {
+ // source := "a link to link:https://example.com"
+ // expected := &types.Document{
+ // Elements: []interface{}{
+ // &types.Paragraph{
+ // Elements: []interface{}{
+ // &types.StringElement{
+ // Content: "a link to link:https://example.com",
+ // },
+ // },
+ // },
+ // },
+ // }
+ // Expect(ParseDocument(source)).To(MatchDocument(expected))
+ // })
It("with quoted text attribute", func() {
source := "link:/[a _a_ b *b* c `c`]"
@@ -1400,7 +1498,7 @@ a link to {scheme}:{path}[] and https://foo.com`
})
It("with line breaks in attributes", func() {
- source := `link:x[
+ source := `link:example.com[
title]`
expected := &types.Document{
Elements: []interface{}{
@@ -1411,7 +1509,80 @@ title]`
types.AttrInlineLinkText: "title",
},
Location: &types.Location{
- Path: "x",
+ Path: "example.com",
+ },
+ },
+ },
+ },
+ },
+ }
+ Expect(ParseDocument(source)).To(MatchDocument(expected))
+ })
+
+ It("with text and custom target", func() {
+ source := `a link to link:https://example.com[the doc,window=read-later]`
+ expected := &types.Document{
+ Elements: []interface{}{
+ &types.Paragraph{
+ Elements: []interface{}{
+ &types.StringElement{Content: "a link to "},
+ &types.InlineLink{
+ Attributes: types.Attributes{
+ types.AttrInlineLinkText: "the doc",
+ types.AttrInlineLinkTarget: "read-later",
+ },
+ Location: &types.Location{
+ Scheme: "https://",
+ Path: "example.com",
+ },
+ },
+ },
+ },
+ },
+ }
+ Expect(ParseDocument(source)).To(MatchDocument(expected))
+ })
+
+ It("with text and custom target with noopener", func() {
+ source := `a link to link:https://example.com[the doc,window=read-later,opts=noopener]`
+ expected := &types.Document{
+ Elements: []interface{}{
+ &types.Paragraph{
+ Elements: []interface{}{
+ &types.StringElement{Content: "a link to "},
+ &types.InlineLink{
+ Attributes: types.Attributes{
+ types.AttrInlineLinkText: "the doc",
+ types.AttrInlineLinkTarget: "read-later",
+ types.AttrOptions: types.Options{"noopener"},
+ },
+ Location: &types.Location{
+ Scheme: "https://",
+ Path: "example.com",
+ },
+ },
+ },
+ },
+ },
+ }
+ Expect(ParseDocument(source)).To(MatchDocument(expected))
+ })
+
+ It("with text and explicit blank target", func() {
+ source := `a link to link:https://example.com[the doc,window=_blank]`
+ expected := &types.Document{
+ Elements: []interface{}{
+ &types.Paragraph{
+ Elements: []interface{}{
+ &types.StringElement{Content: "a link to "},
+ &types.InlineLink{
+ Attributes: types.Attributes{
+ types.AttrInlineLinkText: "the doc",
+ types.AttrInlineLinkTarget: "_blank",
+ },
+ Location: &types.Location{
+ Scheme: "https://",
+ Path: "example.com",
},
},
},
@@ -1421,6 +1592,29 @@ title]`
Expect(ParseDocument(source)).To(MatchDocument(expected))
})
+ It("with text and blank target short-hand", func() {
+ source := `a link to link:https://example.com[the doc^]` // the ^ character is used to define a `blank` target
+ expected := &types.Document{
+ Elements: []interface{}{
+ &types.Paragraph{
+ Elements: []interface{}{
+ &types.StringElement{Content: "a link to "},
+ &types.InlineLink{
+ Attributes: types.Attributes{
+ types.AttrInlineLinkText: "the doc",
+ types.AttrInlineLinkTarget: "_blank",
+ },
+ Location: &types.Location{
+ Scheme: "https://",
+ Path: "example.com",
+ },
+ },
+ },
+ },
+ },
+ }
+ Expect(ParseDocument(source)).To(MatchDocument(expected))
+ })
Context("text attribute with comma", func() {
It("with text having comma", func() {
diff --git a/pkg/renderer/sgml/html5/link.go b/pkg/renderer/sgml/html5/link.go
index c06ed4f9..08ef30ef 100644
--- a/pkg/renderer/sgml/html5/link.go
+++ b/pkg/renderer/sgml/html5/link.go
@@ -1,5 +1,5 @@
package html5
const (
- linkTmpl = `{{ .Text }}`
+ linkTmpl = `{{ .Text }}`
)
diff --git a/pkg/renderer/sgml/html5/link_test.go b/pkg/renderer/sgml/html5/link_test.go
index cb0c615e..47491b4e 100644
--- a/pkg/renderer/sgml/html5/link_test.go
+++ b/pkg/renderer/sgml/html5/link_test.go
@@ -4,7 +4,6 @@ import (
"time"
"github.com/bytesparadise/libasciidoc/pkg/configuration"
- "github.com/bytesparadise/libasciidoc/pkg/types"
. "github.com/bytesparadise/libasciidoc/testsupport"
. "github.com/onsi/ginkgo/v2"
@@ -88,62 +87,20 @@ a link to <{example}>.`
It("with special character in URL", func() {
source := `a link to https://example.com>[].`
- expected := &types.Document{
- Elements: []interface{}{
- &types.Paragraph{
- Elements: []interface{}{
- &types.StringElement{
- Content: "a link to ",
- },
- &types.InlineLink{
- Location: &types.Location{
- Scheme: "https://",
- Path: []interface{}{
- &types.StringElement{
- Content: "example.com",
- },
- &types.SpecialCharacter{
- Name: ">",
- },
- },
- },
- },
- &types.StringElement{
- Content: ".",
- },
- },
- },
- },
- }
- Expect(ParseDocument(source)).To(MatchDocument(expected))
+ expected := `
+`
+ Expect(RenderHTML(source)).To(MatchHTML(expected))
})
It("with opening angle bracket", func() {
source := `a link to
+a link to <https://example.com.
+
+`
+ Expect(RenderHTML(source)).To(MatchHTML(expected))
})
})
})
@@ -246,6 +203,42 @@ next lines
Expect(RenderHTML(source)).To(MatchHTML(expected))
})
+ It("with text and custom target", func() {
+ source := `a link to https://example.com[the doc,window=read-later]`
+ expected := `
+`
+ Expect(RenderHTML(source)).To(MatchHTML(expected))
+ })
+
+ It("with text and custom target with noopener", func() {
+ source := `a link to https://example.com[the doc,window=read-later,opts=noopener]`
+ expected := `
+`
+ Expect(RenderHTML(source)).To(MatchHTML(expected))
+ })
+
+ It("with text and explicit blank target", func() {
+ source := `a link to https://example.com[the doc,window=_blank]`
+ expected := `
+`
+ Expect(RenderHTML(source)).To(MatchHTML(expected))
+ })
+
+ It("with text and blank target short-hand", func() {
+ source := `a link to https://example.com[the doc^]` // the ^ character is used to define a `blank` target
+ expected := `
+`
+ Expect(RenderHTML(source)).To(MatchHTML(expected))
+ })
+
Context("with document attribute substitutions", func() {
It("with a document attribute substitution for the whole URL", func() {
@@ -422,6 +415,42 @@ a link to {scheme}://{path} and https://foo.baz`
Expect(RenderHTML(source)).To(MatchHTML(expected))
})
+ It("with text and custom target", func() {
+ source := `a link to link:https://example.com[the doc,window=read-later]`
+ expected := `
+`
+ Expect(RenderHTML(source)).To(MatchHTML(expected))
+ })
+
+ It("with text and custom target with noopener", func() {
+ source := `a link to link:https://example.com[the doc,window=read-later,opts=noopener]`
+ expected := `
+`
+ Expect(RenderHTML(source)).To(MatchHTML(expected))
+ })
+
+ It("with text and explicit blank target", func() {
+ source := `a link to link:https://example.com[the doc,window=_blank]`
+ expected := `
+`
+ Expect(RenderHTML(source)).To(MatchHTML(expected))
+ })
+
+ It("with text and blank target short-hand", func() {
+ source := `a link to link:https://example.com[the doc^]` // the ^ character is used to define a `blank` target
+ expected := `
+`
+ Expect(RenderHTML(source)).To(MatchHTML(expected))
+ })
+
Context("with document attribute substitutions", func() {
It("with attribute in section 0 title", func() {
diff --git a/pkg/renderer/sgml/link.go b/pkg/renderer/sgml/link.go
index 8b647b06..959db381 100644
--- a/pkg/renderer/sgml/link.go
+++ b/pkg/renderer/sgml/link.go
@@ -40,18 +40,22 @@ func (r *sgmlRenderer) renderLink(ctx *renderer.Context, l *types.InlineLink) (s
class = "bare"
}
}
+ target := l.Attributes.GetAsStringWithDefault(types.AttrInlineLinkTarget, "")
+ noopener := target == "_blank" || l.Attributes.HasOption("noopener")
err = r.link.Execute(result, struct {
- ID string
- URL string
- Text string
- Class string
- Target string
+ ID string
+ URL string
+ Text string
+ Class string
+ Target string
+ NoOpener bool
}{
- ID: id,
- URL: location,
- Text: text,
- Class: class,
- Target: l.Attributes.GetAsStringWithDefault(types.AttrInlineLinkTarget, ""),
+ ID: id,
+ URL: location,
+ Text: text,
+ Class: class,
+ Target: target,
+ NoOpener: noopener,
})
if err != nil {
return "", errors.Wrap(err, "unable to render link")
diff --git a/pkg/types/types.go b/pkg/types/types.go
index c8b83b9f..74cc26d9 100644
--- a/pkg/types/types.go
+++ b/pkg/types/types.go
@@ -2861,6 +2861,11 @@ func NewInlineLink(url *Location, attributes interface{}) (*InlineLink, error) {
attrs := toAttributesWithMapping(attributes, map[string]string{
AttrPositional1: AttrInlineLinkText,
})
+ // also, look for `^` suffix in `AttrInlineLinkText` attribute for a `_blank` target
+ if text, found := attrs[AttrInlineLinkText].(string); found && strings.HasSuffix(text, "^") {
+ attrs[AttrInlineLinkText] = strings.TrimSuffix(text, "^")
+ attrs[AttrInlineLinkTarget] = "_blank"
+ }
return &InlineLink{
Location: url,
Attributes: attrs,