Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions core/block/import/markdown/anymark/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,19 @@ func (r *Renderer) renderAutoLink(_ util.BufWriter, source []byte, node ast.Node
return ast.WalkContinue, nil
}

// extractAnytypeObjectID extracts the objectId from an anytype://object?objectId=... URL.
// Returns empty string if the URL is not an anytype object link.
func extractAnytypeObjectID(raw string) string {
u, err := url.Parse(raw)
if err != nil {
return ""
}
if u.Scheme == "anytype" && u.Host == "object" {
return u.Query().Get("objectId")
}
return ""
}

func IsUrl(raw string) bool {
colon := strings.IndexByte(raw, ':')

Expand Down Expand Up @@ -365,10 +378,17 @@ func (r *Renderer) renderLink(_ util.BufWriter,

to := int32(text.UTF16RuneCountString(r.GetText()))

markType := model.BlockContentTextMark_Link
markParam := linkPath
if objectID := extractAnytypeObjectID(linkPath); objectID != "" {
markType = model.BlockContentTextMark_Mention
markParam = objectID
}

r.AddMark(model.BlockContentTextMark{
Range: &model.Range{From: int32(r.GetMarkStart()), To: to},
Type: model.BlockContentTextMark_Link,
Param: linkPath,
Type: markType,
Param: markParam,
})
}
return ast.WalkContinue, nil
Expand Down
189 changes: 189 additions & 0 deletions core/block/import/markdown/anymark/renderer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,192 @@ type expectedBlock struct {
IsImage bool
Marks []*model.BlockContentTextMark
}

func TestAnytypeObjectLinks(t *testing.T) {
const objectID = "bafyreiepe46rglxszewcokukqhbbuu2quzby2hue4h6cue3lhdxil2lxii"
const spaceID = "bafyreielnx2xgkomxvfdhg36kgemica7kdokt5r7d3rmdwnndaewvfesdi.7wp5no5ccvt2"

tests := []struct {
name string
markdown string
expected []expectedBlock
}{
{
name: "Inline anytype object link becomes mention",
markdown: "See [My Task](anytype://object?objectId=" + objectID + "&spaceId=" + spaceID + ") for details.",
expected: []expectedBlock{
{
Type: model.BlockContentText_Paragraph,
Text: "See My Task for details.",
Marks: []*model.BlockContentTextMark{
{
Type: model.BlockContentTextMark_Mention,
Param: objectID,
Range: &model.Range{From: 4, To: 11},
},
},
},
},
},
{
name: "Anytype link without spaceId",
markdown: "Link to [Page](anytype://object?objectId=" + objectID + ") here.",
expected: []expectedBlock{
{
Type: model.BlockContentText_Paragraph,
Text: "Link to Page here.",
Marks: []*model.BlockContentTextMark{
{
Type: model.BlockContentTextMark_Mention,
Param: objectID,
Range: &model.Range{From: 8, To: 12},
},
},
},
},
},
{
name: "Regular HTTP link stays as Link mark",
markdown: "Visit [Google](https://google.com) today.",
expected: []expectedBlock{
{
Type: model.BlockContentText_Paragraph,
Text: "Visit Google today.",
Marks: []*model.BlockContentTextMark{
{
Type: model.BlockContentTextMark_Link,
Param: "https://google.com",
Range: &model.Range{From: 6, To: 12},
},
},
},
},
},
{
name: "Mixed anytype and HTTP links",
markdown: "See [task](anytype://object?objectId=" + objectID + ") and [docs](https://example.com).",
expected: []expectedBlock{
{
Type: model.BlockContentText_Paragraph,
Text: "See task and docs.",
Marks: []*model.BlockContentTextMark{
{
Type: model.BlockContentTextMark_Mention,
Param: objectID,
Range: &model.Range{From: 4, To: 8},
},
{
Type: model.BlockContentTextMark_Link,
Param: "https://example.com",
Range: &model.Range{From: 13, To: 17},
},
},
},
},
},
{
name: "Standalone anytype link on its own line",
markdown: "[My Task](anytype://object?objectId=" + objectID + "&spaceId=" + spaceID + ")",
expected: []expectedBlock{
{
Type: model.BlockContentText_Paragraph,
Text: "My Task",
Marks: []*model.BlockContentTextMark{
{
Type: model.BlockContentTextMark_Mention,
Param: objectID,
Range: &model.Range{From: 0, To: 7},
},
},
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
blocks, _, err := MarkdownToBlocks([]byte(tt.markdown), "", nil)
require.NoError(t, err)

var textBlocks []*model.Block
for _, block := range blocks {
if block.GetText() != nil {
textBlocks = append(textBlocks, block)
}
}

require.Equal(t, len(tt.expected), len(textBlocks), "Number of blocks mismatch")

for i, exp := range tt.expected {
block := textBlocks[i]
require.NotNil(t, block.GetText())
assert.Equal(t, exp.Text, block.GetText().GetText())
assert.Equal(t, exp.Type, block.GetText().GetStyle())

if len(exp.Marks) > 0 {
marks := block.GetText().GetMarks()
require.NotNil(t, marks)
require.Equal(t, len(exp.Marks), len(marks.GetMarks()))
for j, expectedMark := range exp.Marks {
actualMark := marks.GetMarks()[j]
assert.Equal(t, expectedMark.Type, actualMark.Type, "mark %d type mismatch", j)
assert.Equal(t, expectedMark.Param, actualMark.Param, "mark %d param mismatch", j)
assert.Equal(t, expectedMark.Range.From, actualMark.Range.From, "mark %d range.from mismatch", j)
assert.Equal(t, expectedMark.Range.To, actualMark.Range.To, "mark %d range.to mismatch", j)
}
}
}
})
}
}

func TestExtractAnytypeObjectID(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "Valid anytype object URL",
input: "anytype://object?objectId=bafyrei123&spaceId=space456",
expected: "bafyrei123",
},
{
name: "Valid anytype object URL without spaceId",
input: "anytype://object?objectId=bafyrei123",
expected: "bafyrei123",
},
{
name: "HTTP URL returns empty",
input: "https://example.com",
expected: "",
},
{
name: "Anytype URL with wrong host",
input: "anytype://file?fileId=abc",
expected: "",
},
{
name: "Anytype object URL without objectId param",
input: "anytype://object?spaceId=space456",
expected: "",
},
{
name: "Empty string returns empty",
input: "",
expected: "",
},
{
name: "Local file path returns empty",
input: "my-document.md",
expected: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := extractAnytypeObjectID(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}