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 {
|
if attrs.Target != nil {
|
||||||
writeAttr(buf, "target", *attrs.Target)
|
writeAttr(buf, "target", *attrs.Target)
|
||||||
}
|
}
|
||||||
if attrs.Rel != nil {
|
rel := linkRelToEmit(attrs)
|
||||||
writeAttr(buf, "rel", *attrs.Rel)
|
if rel != "" {
|
||||||
|
writeAttr(buf, "rel", rel)
|
||||||
}
|
}
|
||||||
if attrs.Class != nil {
|
if attrs.Class != nil {
|
||||||
writeAttr(buf, "class", *attrs.Class)
|
writeAttr(buf, "class", *attrs.Class)
|
||||||
@@ -261,6 +262,27 @@ func renderTableCell(buf *bytes.Buffer, n Node, tag string) error {
|
|||||||
return nil
|
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) {
|
func writeAttr(buf *bytes.Buffer, name, value string) {
|
||||||
buf.WriteByte(' ')
|
buf.WriteByte(' ')
|
||||||
buf.WriteString(name)
|
buf.WriteString(name)
|
||||||
|
|||||||
@@ -26,12 +26,10 @@ import (
|
|||||||
|
|
||||||
// htmlBlockSanitizePolicy matches tags and attributes we can represent as
|
// htmlBlockSanitizePolicy matches tags and attributes we can represent as
|
||||||
// ProseMirror/Tiptap JSON and strips scripts, event handlers, and unsafe URLs.
|
// ProseMirror/Tiptap JSON and strips scripts, event handlers, and unsafe URLs.
|
||||||
func htmlBlockSanitizePolicy() *bluemonday.Policy {
|
var htmlBlockSanitizePolicy = bluemonday.UGCPolicy()
|
||||||
return bluemonday.UGCPolicy()
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeHTMLBlockContent(s string) string {
|
func sanitizeHTMLBlockContent(s string) string {
|
||||||
return htmlBlockSanitizePolicy().Sanitize(s)
|
return htmlBlockSanitizePolicy.Sanitize(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertProseMirrorFromInlineHTML sanitizes inline raw HTML and maps it to
|
// 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)
|
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) {
|
func TestRenderHTML_LinkSanitizesDangerousHrefs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ func (c *converter) collectRawHTMLRun(start ast.Node) (run string, next ast.Node
|
|||||||
case ast.KindText:
|
case ast.KindText:
|
||||||
t := ch.(*ast.Text)
|
t := ch.(*ast.Text)
|
||||||
buf.Write(t.Segment.Value(c.source))
|
buf.Write(t.Segment.Value(c.source))
|
||||||
|
if t.SoftLineBreak() {
|
||||||
|
buf.WriteByte(' ')
|
||||||
|
}
|
||||||
case ast.KindString:
|
case ast.KindString:
|
||||||
buf.Write(ch.(*ast.String).Value)
|
buf.Write(ch.(*ast.String).Value)
|
||||||
default:
|
default:
|
||||||
@@ -352,6 +355,9 @@ func (c *converter) convertImage(n *ast.Image) ([]Node, error) {
|
|||||||
|
|
||||||
func (c *converter) convertText(n *ast.Text) ([]Node, error) {
|
func (c *converter) convertText(n *ast.Text) ([]Node, error) {
|
||||||
content := string(n.Segment.Value(c.source))
|
content := string(n.Segment.Value(c.source))
|
||||||
|
if n.SoftLineBreak() {
|
||||||
|
content += " "
|
||||||
|
}
|
||||||
if content == "" {
|
if content == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package prosemirror
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -351,6 +352,25 @@ func TestParseMarkdown_HardBreak(t *testing.T) {
|
|||||||
assert.True(t, hasHardBreak, "expected hard break node")
|
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) {
|
func TestParseMarkdown_NestedMarks(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user