diff --git a/pkg/prosemirror/html.go b/pkg/prosemirror/html.go
index 3d5d57ab3..b42ec85a8 100644
--- a/pkg/prosemirror/html.go
+++ b/pkg/prosemirror/html.go
@@ -198,8 +198,9 @@ func openMark(buf *bytes.Buffer, m Mark) error {
if attrs.Target != nil {
writeAttr(buf, "target", *attrs.Target)
}
- if attrs.Rel != nil {
- writeAttr(buf, "rel", *attrs.Rel)
+ rel := linkRelToEmit(attrs)
+ if rel != "" {
+ writeAttr(buf, "rel", rel)
}
if attrs.Class != nil {
writeAttr(buf, "class", *attrs.Class)
@@ -261,6 +262,27 @@ func renderTableCell(buf *bytes.Buffer, n Node, tag string) error {
return nil
}
+const linkRelBlankTargetDefault = "noopener noreferrer"
+
+// linkRelToEmit returns the rel attribute value for a link mark, or empty when
+// the attribute should be omitted. When target opens a new browsing context
+// (_blank) and the document provides no rel, browsers would grant the opened
+// page access to window.opener unless noopener is set.
+func linkRelToEmit(attrs LinkAttrs) string {
+ if attrs.Rel != nil {
+ if s := strings.TrimSpace(*attrs.Rel); s != "" {
+ return s
+ }
+ }
+ if attrs.Target == nil {
+ return ""
+ }
+ if !strings.EqualFold(strings.TrimSpace(*attrs.Target), "_blank") {
+ return ""
+ }
+ return linkRelBlankTargetDefault
+}
+
func writeAttr(buf *bytes.Buffer, name, value string) {
buf.WriteByte(' ')
buf.WriteString(name)
diff --git a/pkg/prosemirror/html_block.go b/pkg/prosemirror/html_block.go
index 3a38284c2..231ba99eb 100644
--- a/pkg/prosemirror/html_block.go
+++ b/pkg/prosemirror/html_block.go
@@ -26,12 +26,10 @@ import (
// htmlBlockSanitizePolicy matches tags and attributes we can represent as
// ProseMirror/Tiptap JSON and strips scripts, event handlers, and unsafe URLs.
-func htmlBlockSanitizePolicy() *bluemonday.Policy {
- return bluemonday.UGCPolicy()
-}
+var htmlBlockSanitizePolicy = bluemonday.UGCPolicy()
func sanitizeHTMLBlockContent(s string) string {
- return htmlBlockSanitizePolicy().Sanitize(s)
+ return htmlBlockSanitizePolicy.Sanitize(s)
}
// convertProseMirrorFromInlineHTML sanitizes inline raw HTML and maps it to
diff --git a/pkg/prosemirror/html_test.go b/pkg/prosemirror/html_test.go
index b6cfb07bb..9b7a5e9c1 100644
--- a/pkg/prosemirror/html_test.go
+++ b/pkg/prosemirror/html_test.go
@@ -187,6 +187,45 @@ func TestRenderHTML_LinkMinimalAttrs(t *testing.T) {
assert.Equal(t, `hi`, got)
}
+func TestRenderHTML_LinkBlankTargetDefaultRel(t *testing.T) {
+ t.Parallel()
+
+ for _, tc := range []struct {
+ name string
+ raw string
+ want string
+ }{
+ {
+ name: "no rel",
+ raw: `{"type":"text","marks":[{"type":"link","attrs":{"href":"https://example.com","target":"_blank","rel":null}}],"text":"hi"}`,
+ want: `hi`,
+ },
+ {
+ name: "case insensitive target",
+ raw: `{"type":"text","marks":[{"type":"link","attrs":{"href":"https://example.com","target":"_BLANK"}}],"text":"hi"}`,
+ want: `hi`,
+ },
+ {
+ name: "whitespace rel treated as absent",
+ raw: `{"type":"text","marks":[{"type":"link","attrs":{"href":"https://example.com","target":" _blank ","rel":" "}}],"text":"hi"}`,
+ want: `hi`,
+ },
+ } {
+ t.Run(
+ tc.name,
+ func(t *testing.T) {
+ t.Parallel()
+ var n Node
+ require.NoError(t, json.Unmarshal([]byte(tc.raw), &n))
+
+ got, err := RenderHTML(n)
+ require.NoError(t, err)
+ assert.Equal(t, tc.want, got)
+ },
+ )
+ }
+}
+
func TestRenderHTML_LinkSanitizesDangerousHrefs(t *testing.T) {
t.Parallel()
diff --git a/pkg/prosemirror/markdown.go b/pkg/prosemirror/markdown.go
index b7af49d85..468e71eae 100644
--- a/pkg/prosemirror/markdown.go
+++ b/pkg/prosemirror/markdown.go
@@ -117,6 +117,9 @@ func (c *converter) collectRawHTMLRun(start ast.Node) (run string, next ast.Node
case ast.KindText:
t := ch.(*ast.Text)
buf.Write(t.Segment.Value(c.source))
+ if t.SoftLineBreak() {
+ buf.WriteByte(' ')
+ }
case ast.KindString:
buf.Write(ch.(*ast.String).Value)
default:
@@ -352,6 +355,9 @@ func (c *converter) convertImage(n *ast.Image) ([]Node, error) {
func (c *converter) convertText(n *ast.Text) ([]Node, error) {
content := string(n.Segment.Value(c.source))
+ if n.SoftLineBreak() {
+ content += " "
+ }
if content == "" {
return nil, nil
}
diff --git a/pkg/prosemirror/markdown_test.go b/pkg/prosemirror/markdown_test.go
index 061a45e0b..dfe86b20e 100644
--- a/pkg/prosemirror/markdown_test.go
+++ b/pkg/prosemirror/markdown_test.go
@@ -16,6 +16,7 @@ package prosemirror
import (
"encoding/json"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -351,6 +352,25 @@ func TestParseMarkdown_HardBreak(t *testing.T) {
assert.True(t, hasHardBreak, "expected hard break node")
}
+func TestParseMarkdown_SoftLineBreak(t *testing.T) {
+ t.Parallel()
+
+ doc, err := ParseMarkdown("line one\nand line two")
+ require.NoError(t, err)
+ require.Len(t, doc.Content, 1)
+
+ p := doc.Content[0]
+ require.Equal(t, NodeParagraph, p.Type)
+
+ var joined strings.Builder
+ for _, child := range p.Content {
+ if child.Type == NodeText && child.Text != nil {
+ joined.WriteString(*child.Text)
+ }
+ }
+ assert.Equal(t, "line one and line two", joined.String())
+}
+
func TestParseMarkdown_NestedMarks(t *testing.T) {
t.Parallel()