Fix html conversion of soft line breaks + rel attr for _blank link targets
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -187,6 +187,45 @@ func TestRenderHTML_LinkMinimalAttrs(t *testing.T) {
|
||||
assert.Equal(t, `<a href="https://example.com">hi</a>`, 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: `<a href="https://example.com" target="_blank" rel="noopener noreferrer">hi</a>`,
|
||||
},
|
||||
{
|
||||
name: "case insensitive target",
|
||||
raw: `{"type":"text","marks":[{"type":"link","attrs":{"href":"https://example.com","target":"_BLANK"}}],"text":"hi"}`,
|
||||
want: `<a href="https://example.com" target="_BLANK" rel="noopener noreferrer">hi</a>`,
|
||||
},
|
||||
{
|
||||
name: "whitespace rel treated as absent",
|
||||
raw: `{"type":"text","marks":[{"type":"link","attrs":{"href":"https://example.com","target":" _blank ","rel":" "}}],"text":"hi"}`,
|
||||
want: `<a href="https://example.com" target=" _blank " rel="noopener noreferrer">hi</a>`,
|
||||
},
|
||||
} {
|
||||
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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user