@@ -33,7 +33,10 @@ func ParseMarkdown(markdown string) (Node, error) {
|
||||
source := []byte(markdown)
|
||||
|
||||
md := goldmark.New(
|
||||
goldmark.WithExtensions(goldmarkext.Strikethrough),
|
||||
goldmark.WithExtensions(
|
||||
goldmarkext.Strikethrough,
|
||||
goldmarkext.Table,
|
||||
),
|
||||
goldmark.WithParserOptions(parser.WithAutoHeadingID()),
|
||||
)
|
||||
|
||||
@@ -169,8 +172,17 @@ func (c *converter) convertNode(n ast.Node) ([]Node, error) {
|
||||
case ast.KindHTMLBlock:
|
||||
return c.convertHTMLBlock(n.(*ast.HTMLBlock))
|
||||
default:
|
||||
if n.Kind() == goldmarkast.KindStrikethrough {
|
||||
switch n.Kind() {
|
||||
case goldmarkast.KindStrikethrough:
|
||||
return c.convertStrikethrough(n)
|
||||
case goldmarkast.KindTable:
|
||||
return c.convertTable(n)
|
||||
case goldmarkast.KindTableHeader:
|
||||
return c.convertTableHeaderRow(n.(*goldmarkast.TableHeader))
|
||||
case goldmarkast.KindTableRow:
|
||||
return c.convertTableDataRow(n)
|
||||
case goldmarkast.KindTableCell:
|
||||
return nil, fmt.Errorf("cannot convert table cell outside of a table row")
|
||||
}
|
||||
return nil, fmt.Errorf("cannot convert markdown node of kind %s", n.Kind())
|
||||
}
|
||||
@@ -516,6 +528,83 @@ func (c *converter) convertStrikethrough(n ast.Node) ([]Node, error) {
|
||||
return children, nil
|
||||
}
|
||||
|
||||
func (c *converter) convertTable(n ast.Node) ([]Node, error) {
|
||||
var rows []Node
|
||||
|
||||
for child := n.FirstChild(); child != nil; child = child.NextSibling() {
|
||||
converted, err := c.convertNode(child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows = append(rows, converted...)
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return []Node{{Type: NodeTable, Content: rows}}, nil
|
||||
}
|
||||
|
||||
func (c *converter) convertTableHeaderRow(n *goldmarkast.TableHeader) ([]Node, error) {
|
||||
cells, err := c.convertTableCells(n, NodeTableHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(cells) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return []Node{{Type: NodeTableRow, Content: cells}}, nil
|
||||
}
|
||||
|
||||
func (c *converter) convertTableDataRow(n ast.Node) ([]Node, error) {
|
||||
cells, err := c.convertTableCells(n, NodeTableCell)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(cells) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return []Node{{Type: NodeTableRow, Content: cells}}, nil
|
||||
}
|
||||
|
||||
func (c *converter) convertTableCells(row ast.Node, cellType NodeType) ([]Node, error) {
|
||||
var cells []Node
|
||||
|
||||
for child := row.FirstChild(); child != nil; child = child.NextSibling() {
|
||||
if child.Kind() != goldmarkast.KindTableCell {
|
||||
continue
|
||||
}
|
||||
|
||||
inlineContent, err := c.convertInlineChildren(child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cellAttrs := TableCellAttrs{
|
||||
Colspan: 1,
|
||||
Rowspan: 1,
|
||||
}
|
||||
|
||||
attrs, err := json.Marshal(cellAttrs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot marshal table cell attrs: %w", err)
|
||||
}
|
||||
|
||||
cells = append(cells, Node{
|
||||
Type: cellType,
|
||||
Attrs: attrs,
|
||||
Content: []Node{{Type: NodeParagraph, Content: inlineContent}},
|
||||
})
|
||||
}
|
||||
|
||||
return cells, nil
|
||||
}
|
||||
|
||||
// extractText recursively collects the text content of all descendant nodes.
|
||||
func (c *converter) extractText(n ast.Node) string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
@@ -570,6 +570,80 @@ func TestParseMarkdown_BlockHTMLTableCellSpans(t *testing.T) {
|
||||
assert.Equal(t, 2, attrs.Rowspan)
|
||||
}
|
||||
|
||||
func TestParseMarkdown_MarkdownTable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
md := "| Name | Age |\n| --- | --- |\n| Alice | 30 |\n| Bob | 25 |\n"
|
||||
doc, err := ParseMarkdown(md)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, doc.Content, 1)
|
||||
assert.Equal(t, NodeTable, doc.Content[0].Type)
|
||||
|
||||
table := doc.Content[0]
|
||||
require.Len(t, table.Content, 3)
|
||||
|
||||
headerRow := table.Content[0]
|
||||
assert.Equal(t, NodeTableRow, headerRow.Type)
|
||||
require.Len(t, headerRow.Content, 2)
|
||||
assert.Equal(t, NodeTableHeader, headerRow.Content[0].Type)
|
||||
assert.Equal(t, NodeTableHeader, headerRow.Content[1].Type)
|
||||
|
||||
require.Len(t, headerRow.Content[0].Content, 1)
|
||||
require.Equal(t, NodeParagraph, headerRow.Content[0].Content[0].Type)
|
||||
require.Len(t, headerRow.Content[0].Content[0].Content, 1)
|
||||
assert.Equal(t, "Name", *headerRow.Content[0].Content[0].Content[0].Text)
|
||||
|
||||
require.Len(t, headerRow.Content[1].Content, 1)
|
||||
require.Equal(t, NodeParagraph, headerRow.Content[1].Content[0].Type)
|
||||
require.Len(t, headerRow.Content[1].Content[0].Content, 1)
|
||||
assert.Equal(t, "Age", *headerRow.Content[1].Content[0].Content[0].Text)
|
||||
|
||||
for i := 1; i <= 2; i++ {
|
||||
row := table.Content[i]
|
||||
assert.Equal(t, NodeTableRow, row.Type)
|
||||
require.Len(t, row.Content, 2)
|
||||
assert.Equal(t, NodeTableCell, row.Content[0].Type)
|
||||
assert.Equal(t, NodeTableCell, row.Content[1].Type)
|
||||
|
||||
attrs, err := row.Content[0].TableCellAttrs()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, attrs.Colspan)
|
||||
assert.Equal(t, 1, attrs.Rowspan)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Alice", *table.Content[1].Content[0].Content[0].Content[0].Text)
|
||||
assert.Equal(t, "30", *table.Content[1].Content[1].Content[0].Content[0].Text)
|
||||
assert.Equal(t, "Bob", *table.Content[2].Content[0].Content[0].Content[0].Text)
|
||||
assert.Equal(t, "25", *table.Content[2].Content[1].Content[0].Content[0].Text)
|
||||
}
|
||||
|
||||
func TestParseMarkdown_MarkdownTableWithInlineMarks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
md := "| Header |\n| --- |\n| **bold** and *italic* |\n"
|
||||
doc, err := ParseMarkdown(md)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, doc.Content, 1)
|
||||
assert.Equal(t, NodeTable, doc.Content[0].Type)
|
||||
|
||||
dataRow := doc.Content[0].Content[1]
|
||||
assert.Equal(t, NodeTableRow, dataRow.Type)
|
||||
cell := dataRow.Content[0]
|
||||
assert.Equal(t, NodeTableCell, cell.Type)
|
||||
|
||||
p := cell.Content[0]
|
||||
require.Equal(t, NodeParagraph, p.Type)
|
||||
require.GreaterOrEqual(t, len(p.Content), 3)
|
||||
|
||||
assert.Equal(t, "bold", *p.Content[0].Text)
|
||||
require.Len(t, p.Content[0].Marks, 1)
|
||||
assert.Equal(t, MarkStrong, p.Content[0].Marks[0].Type)
|
||||
|
||||
assert.Equal(t, "italic", *p.Content[2].Text)
|
||||
require.Len(t, p.Content[2].Marks, 1)
|
||||
assert.Equal(t, MarkEm, p.Content[2].Marks[0].Type)
|
||||
}
|
||||
|
||||
func TestParseMarkdown_BlockHTMLScriptRemovedKeepsSafeContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user