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 := `
+

a link to https://example.com>.

+
+` + 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 := `
+

a link to the doc

+
+` + 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 := `
+

a link to the doc

+
+` + 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 := `
+

a link to the doc

+
+` + 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 := `
+

a link to the doc

+
+` + 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 := `
+

a link to the doc

+
+` + 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 := `
+

a link to the doc

+
+` + 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 := `
+

a link to the doc

+
+` + 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 := `
+

a link to the doc

+
+` + 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,