diff --git a/pkg/prosemirror/html_block.go b/pkg/prosemirror/html_block.go index b7be1bb7c..3a38284c2 100644 --- a/pkg/prosemirror/html_block.go +++ b/pkg/prosemirror/html_block.go @@ -225,6 +225,16 @@ func (c *htmlBlockConverter) convertBlockElement(n *html.Node) ([]Node, error) { } func (c *htmlBlockConverter) unwrapBlockElement(n *html.Node) ([]Node, error) { + if !hasBlockElementChild(n) { + inlines, err := c.convertInlineFragments(n) + if err != nil { + return nil, err + } + if len(inlines) == 0 { + return nil, nil + } + return []Node{{Type: NodeParagraph, Content: inlines}}, nil + } return c.convertBlockChildren(n) } @@ -395,26 +405,46 @@ func hasBlockElementChild(n *html.Node) bool { func blockTagName(name string) bool { switch name { case "p", "div", "blockquote", "pre", "ul", "ol", "table", - "h1", "h2", "h3", "h4", "h5", "h6", "hr": + "h1", "h2", "h3", "h4", "h5", "h6", "hr", + "section", "article", "aside", "main", "header", "footer", "nav", + "figure", "center", + "html", "body", "head": return true default: return false } } -func (c *htmlBlockConverter) convertTable(n *html.Node) ([]Node, error) { +// collectTableRows returns nodes that belong to this table only: direct +// children of thead/tbody/tfoot, or direct children of the table element +// (implicit tbody). It does not descend into cells or nested tables, so inner +// tables cannot contribute rows to the outer table. +func collectTableRows(table *html.Node) []*html.Node { var rows []*html.Node - var collect func(*html.Node) - collect = func(x *html.Node) { - if x.Type == html.ElementNode && x.Data == "tr" { - rows = append(rows, x) - return + for ch := table.FirstChild; ch != nil; ch = ch.NextSibling { + if ch.Type != html.ElementNode { + continue } - for ch := x.FirstChild; ch != nil; ch = ch.NextSibling { - collect(ch) + switch ch.Data { + case "thead", "tbody", "tfoot": + for tr := ch.FirstChild; tr != nil; tr = tr.NextSibling { + if tr.Type == html.ElementNode && tr.Data == "tr" { + rows = append(rows, tr) + } + } + case "tr": + rows = append(rows, ch) + case "caption", "colgroup", "col": + // Table metadata / columns; not row containers. + default: + // Ignore other direct children (e.g. invalid markup). } } - collect(n) + return rows +} + +func (c *htmlBlockConverter) convertTable(n *html.Node) ([]Node, error) { + rows := collectTableRows(n) var rowNodes []Node for _, tr := range rows { diff --git a/pkg/prosemirror/markdown_test.go b/pkg/prosemirror/markdown_test.go index d6136e270..061a45e0b 100644 --- a/pkg/prosemirror/markdown_test.go +++ b/pkg/prosemirror/markdown_test.go @@ -406,6 +406,42 @@ func TestParseMarkdown_BlockHTML(t *testing.T) { assert.Equal(t, "block", *doc.Content[0].Content[0].Text) } +func TestParseMarkdown_BlockHTMLDivPreservesInlineMarks(t *testing.T) { + t.Parallel() + + doc, err := ParseMarkdown("
hello world
\n") + require.NoError(t, err) + require.Len(t, doc.Content, 1) + p := doc.Content[0] + require.Equal(t, NodeParagraph, p.Type) + + var foundStrong bool + for _, ch := range p.Content { + if ch.Type != NodeText || ch.Text == nil { + continue + } + if *ch.Text != "world" { + continue + } + require.Len(t, ch.Marks, 1) + assert.Equal(t, MarkStrong, ch.Marks[0].Type) + foundStrong = true + } + assert.True(t, foundStrong, "expected bold mark on 'world' inside a single paragraph") +} + +func TestParseMarkdown_BlockHTMLDivAroundSectionWithParagraphs(t *testing.T) { + t.Parallel() + + doc, err := ParseMarkdown("

a

b

\n") + require.NoError(t, err) + require.Len(t, doc.Content, 2) + assert.Equal(t, NodeParagraph, doc.Content[0].Type) + assert.Equal(t, NodeParagraph, doc.Content[1].Type) + assert.Equal(t, "a", *doc.Content[0].Content[0].Text) + assert.Equal(t, "b", *doc.Content[1].Content[0].Text) +} + func TestParseMarkdown_BlockHTMLWithClosureLine(t *testing.T) { t.Parallel() @@ -467,6 +503,19 @@ func TestParseMarkdown_BlockHTMLTable(t *testing.T) { assert.Equal(t, 1, tdAttrs.Rowspan) } +func TestParseMarkdown_BlockHTMLNestedTableDoesNotHoistInnerRows(t *testing.T) { + t.Parallel() + + md := "
inner
\n" + doc, err := ParseMarkdown(md) + require.NoError(t, err) + require.Len(t, doc.Content, 1) + assert.Equal(t, NodeTable, doc.Content[0].Type) + // Outer table must have exactly one row; inner must not become a second outer row. + require.Len(t, doc.Content[0].Content, 1) + assert.Equal(t, NodeTableRow, doc.Content[0].Content[0].Type) +} + func TestParseMarkdown_BlockHTMLTableCellSpans(t *testing.T) { t.Parallel()