diff --git a/adapter/markdown/markdown.go b/adapter/markdown/markdown.go index 223178c..0a1c1e7 100644 --- a/adapter/markdown/markdown.go +++ b/adapter/markdown/markdown.go @@ -31,9 +31,7 @@ func NewParser() *Parser { } // Parse implements note.Parse. -func (p *Parser) Parse(source string) (note.Content, error) { - out := note.Content{} - +func (p *Parser) Parse(source string) (*note.Content, error) { bytes := []byte(source) context := parser.NewContext() @@ -42,21 +40,28 @@ func (p *Parser) Parse(source string) (note.Content, error) { parser.WithContext(context), ) + links, err := parseLinks(root, bytes) + if err != nil { + return nil, err + } + frontmatter, err := parseFrontmatter(context, bytes) if err != nil { - return out, err + return nil, err } title, bodyStart, err := parseTitle(frontmatter, root, bytes) if err != nil { - return out, err + return nil, err } - - out.Title = title - out.Body = parseBody(bodyStart, bytes) - out.Lead = parseLead(out.Body) - - return out, nil + body := parseBody(bodyStart, bytes) + + return ¬e.Content{ + Title: title, + Body: body, + Lead: parseLead(body), + Links: links, + }, nil } // parseTitle extracts the note title with its node. @@ -116,6 +121,26 @@ func parseLead(body opt.String) opt.String { return opt.NewNotEmptyString(strings.TrimSpace(lead)) } +// parseLinks extracts outbound links from the note. +func parseLinks(root ast.Node, source []byte) ([]note.Link, error) { + links := make([]note.Link, 0) + + err := ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if link, ok := n.(*ast.Link); ok && entering { + target := string(link.Destination) + if target != "" { + links = append(links, note.Link{ + Title: string(link.Text(source)), + Target: target, + Rels: strings.Fields(string(link.Title)), + }) + } + } + return ast.WalkContinue, nil + }) + return links, err +} + // frontmatter contains metadata parsed from a YAML frontmatter. type frontmatter struct { values map[string]interface{} diff --git a/adapter/markdown/markdown_test.go b/adapter/markdown/markdown_test.go index aa056ba..987f5a7 100644 --- a/adapter/markdown/markdown_test.go +++ b/adapter/markdown/markdown_test.go @@ -153,8 +153,56 @@ Paragraph`, ) } +func TestParseLinks(t *testing.T) { + test := func(source string, links []note.Link) { + content := parse(t, source) + assert.Equal(t, content.Links, links) + } + + test("", []note.Link{}) + test("No links around here", []note.Link{}) + + test(` +# Heading with a [link](heading) + +Paragraph containing [multiple **links**](stripped-formatting) some [external like this one](http://example.com), and other [relative](../other). +A link can have [one relation](one "rel-1") or [several relations](several "rel-1 rel-2"). +`, []note.Link{ + { + Title: "link", + Target: "heading", + Rels: []string{}, + }, + { + Title: "multiple links", + Target: "stripped-formatting", + Rels: []string{}, + }, + { + Title: "external like this one", + Target: "http://example.com", + Rels: []string{}, + }, + { + Title: "relative", + Target: "../other", + Rels: []string{}, + }, + { + Title: "one relation", + Target: "one", + Rels: []string{"rel-1"}, + }, + { + Title: "several relations", + Target: "several", + Rels: []string{"rel-1", "rel-2"}, + }, + }) +} + func parse(t *testing.T, source string) note.Content { content, err := NewParser().Parse(source) assert.Nil(t, err) - return content + return *content } diff --git a/core/note/parse.go b/core/note/parse.go index 3535390..6e3d740 100644 --- a/core/note/parse.go +++ b/core/note/parse.go @@ -11,8 +11,16 @@ type Content struct { Lead opt.String // Body is the content of the note, including the Lead but without the Title. Body opt.String + // Links is the list of outbound links found in the note. + Links []Link +} + +type Link struct { + Title string + Target string + Rels []string } type Parser interface { - Parse(source string) (Content, error) + Parse(source string) (*Content, error) }