diff --git a/core/block/import/html/converter.go b/core/block/import/html/converter.go index 641bd4b7d2..5ad5a6fd3e 100644 --- a/core/block/import/html/converter.go +++ b/core/block/import/html/converter.go @@ -189,7 +189,7 @@ func (h *HTML) updateFilesInLinks(block *model.Block, filesSource source.Source, if newFileName, createFileBlock, err = common.ProvideFileName(mark.Param, filesSource, path, h.tempDirProvider); err == nil { mark.Param = newFileName if createFileBlock { - anymark.ConvertTextToFile(block) + block.Content = anymark.ConvertTextToFile(mark.Param) break } continue diff --git a/core/block/import/markdown/anymark/anyblocks.go b/core/block/import/markdown/anymark/anyblocks.go index 7fe6c06265..eeea01d640 100644 --- a/core/block/import/markdown/anymark/anyblocks.go +++ b/core/block/import/markdown/anymark/anyblocks.go @@ -92,10 +92,10 @@ func provideCodeBlock(textArr []string, language string, id string) *model.Block } } -func ConvertTextToFile(block *model.Block) { +func ConvertTextToFile(filePath string) *model.BlockContentOfFile { // "svg" excluded - if block.GetText().GetMarks().Marks[0].Param == "" { - return + if filePath == "" { + return nil } imageFormats := []string{"jpg", "jpeg", "png", "gif", "webp"} @@ -104,7 +104,7 @@ func ConvertTextToFile(block *model.Block) { pdfFormat := "pdf" fileType := model.BlockContentFile_File - fileExt := filepath.Ext(block.GetText().GetMarks().Marks[0].Param) + fileExt := filepath.Ext(filePath) if fileExt != "" { fileExt = fileExt[1:] for _, ext := range imageFormats { @@ -131,14 +131,13 @@ func ConvertTextToFile(block *model.Block) { if strings.EqualFold(fileExt, pdfFormat) { fileType = model.BlockContentFile_PDF } - - block.Content = &model.BlockContentOfFile{ - File: &model.BlockContentFile{ - Name: block.GetText().GetMarks().Marks[0].Param, - State: model.BlockContentFile_Empty, - Type: fileType, - }, - } + } + return &model.BlockContentOfFile{ + File: &model.BlockContentFile{ + Name: filePath, + State: model.BlockContentFile_Empty, + Type: fileType, + }, } } diff --git a/core/block/import/markdown/blockconverter.go b/core/block/import/markdown/blockconverter.go index 9bbe405b6c..1a9cc36950 100644 --- a/core/block/import/markdown/blockconverter.go +++ b/core/block/import/markdown/blockconverter.go @@ -98,35 +98,72 @@ func (m *mdConverter) processBlocks(shortPath string, file *FileInfo, files map[ func (m *mdConverter) processTextBlock(block *model.Block, files map[string]*FileInfo) { txt := block.GetText() - if txt != nil && txt.Marks != nil && len(txt.Marks.Marks) == 1 && - txt.Marks.Marks[0].Type == model.BlockContentTextMark_Link { - link := txt.Marks.Marks[0].Param - wholeLineLink := m.isWholeLineLink(txt) - ext := filepath.Ext(link) - - // todo: bug with multiple markup links in arow when the first is external - if file := files[link]; file != nil { - if strings.EqualFold(ext, ".csv") { - m.processCSVFileLink(block, files, link, wholeLineLink) + if txt != nil && txt.Marks != nil { + if len(txt.Marks.Marks) == 1 && txt.Marks.Marks[0].Type == model.BlockContentTextMark_Link { + m.handleSingleMark(block, files) + } else { + m.handleMultipleMarks(block, files) + } + } +} + +func (m *mdConverter) handleSingleMark(block *model.Block, files map[string]*FileInfo) { + txt := block.GetText() + link := txt.Marks.Marks[0].Param + wholeLineLink := m.isWholeLineLink(txt.Text, txt.Marks.Marks[0]) + ext := filepath.Ext(link) + if file := files[link]; file != nil { + if strings.EqualFold(ext, ".csv") { + m.processCSVFileLink(block, files, link, wholeLineLink) + return + } + if strings.EqualFold(ext, ".md") { + // only convert if this is the only link in the row + m.convertToAnytypeLinkBlock(block, wholeLineLink) + } else { + block.Content = anymark.ConvertTextToFile(txt.Marks.Marks[0].Param) + } + file.HasInboundLinks = true + } else if wholeLineLink { + block.Content = m.convertTextToBookmark(txt.Marks.Marks[0].Param) + } +} + +func (m *mdConverter) handleMultipleMarks(block *model.Block, files map[string]*FileInfo) { + txt := block.GetText() + for _, mark := range txt.Marks.Marks { + if mark.Type == model.BlockContentTextMark_Link { + if stop := m.handleSingleLinkMark(block, files, mark, txt); stop { return } - if strings.EqualFold(ext, ".md") { - // only convert if this is the only link in the row - m.convertToAnytypeLinkBlock(block, wholeLineLink) - } else { - anymark.ConvertTextToFile(block) - } - file.HasInboundLinks = true - } else if wholeLineLink { - m.convertTextToBookmark(block) } } } -func (m *mdConverter) isWholeLineLink(txt *model.BlockContentText) bool { +func (m *mdConverter) handleSingleLinkMark(block *model.Block, files map[string]*FileInfo, mark *model.BlockContentTextMark, txt *model.BlockContentText) bool { + link := mark.Param + ext := filepath.Ext(link) + if file := files[link]; file != nil { + file.HasInboundLinks = true + if strings.EqualFold(ext, ".md") || strings.EqualFold(ext, ".csv") { + mark.Type = model.BlockContentTextMark_Mention + return false + } + if m.isWholeLineLink(txt.Text, mark) { + block.Content = anymark.ConvertTextToFile(mark.Param) + return true + } + } else if m.isWholeLineLink(txt.Text, mark) { + block.Content = m.convertTextToBookmark(mark.Param) + return true + } + return false +} + +func (m *mdConverter) isWholeLineLink(text string, marks *model.BlockContentTextMark) bool { var wholeLineLink bool - textRunes := []rune(txt.Text) - var from, to = int(txt.Marks.Marks[0].Range.From), int(txt.Marks.Marks[0].Range.To) + textRunes := []rune(text) + var from, to = int(marks.Range.From), int(marks.Range.To) if from == 0 || (from < len(textRunes) && len(strings.TrimSpace(string(textRunes[0:from]))) == 0) { if to >= len(textRunes) || len(strings.TrimSpace(string(textRunes[to:]))) == 0 { wholeLineLink = true @@ -201,14 +238,14 @@ func (m *mdConverter) convertTextToPageLink(block *model.Block) { } } -func (m *mdConverter) convertTextToBookmark(block *model.Block) { - if err := uri.ValidateURI(block.GetText().Marks.Marks[0].Param); err != nil { - return +func (m *mdConverter) convertTextToBookmark(url string) *model.BlockContentOfBookmark { + if err := uri.ValidateURI(url); err != nil { + return nil } - block.Content = &model.BlockContentOfBookmark{ + return &model.BlockContentOfBookmark{ Bookmark: &model.BlockContentBookmark{ - Url: block.GetText().Marks.Marks[0].Param, + Url: url, }, } } diff --git a/core/block/import/markdown/blockconverter_test.go b/core/block/import/markdown/blockconverter_test.go index 874efa0633..f5e8c1df65 100644 --- a/core/block/import/markdown/blockconverter_test.go +++ b/core/block/import/markdown/blockconverter_test.go @@ -39,7 +39,7 @@ func Test_processFiles(t *testing.T) { files := converter.processFiles(absolutePath, common.NewError(pb.RpcObjectImportRequest_IGNORE_ERRORS), source) // then - assert.Len(t, files, 3) + assert.Len(t, files, 6) pdfFilePath := filepath.Join(absolutePath, "test.pdf") assert.Contains(t, files, pdfFilePath) @@ -71,7 +71,7 @@ func Test_processFiles(t *testing.T) { files := converter.processFiles(absolutePath, common.NewError(pb.RpcObjectImportRequest_IGNORE_ERRORS), source) // then - assert.Len(t, files, 1) + assert.Len(t, files, 4) pdfFilePath := filepath.Join(absolutePath, "test.pdf") assert.NotContains(t, files, pdfFilePath) diff --git a/core/block/import/markdown/import.go b/core/block/import/markdown/import.go index bc5e0386f7..19704748ea 100644 --- a/core/block/import/markdown/import.go +++ b/core/block/import/markdown/import.go @@ -140,8 +140,8 @@ func (m *Markdown) getSnapshotsAndRootObjectsIds( m.processImportStep(pathsCount, files, progress, allErrors, details, m.setNewID) || m.processImportStep(pathsCount, files, progress, allErrors, details, m.addLinkToObjectBlocks) || m.processImportStep(pathsCount, files, progress, allErrors, details, m.linkPagesWithRootFile) || - m.processImportStep(pathsCount, files, progress, allErrors, details, m.fillEmptyBlocks) || m.processImportStep(pathsCount, files, progress, allErrors, details, m.addLinkBlocks) || + m.processImportStep(pathsCount, files, progress, allErrors, details, m.fillEmptyBlocks) || m.processImportStep(pathsCount, files, progress, allErrors, details, m.addChildBlocks) { return nil, nil } @@ -448,7 +448,7 @@ func (m *Markdown) addChildBlocks(files map[string]*FileInfo, progress process.P continue } - var childrenIds = make([]string, len(file.ParsedBlocks)) + childrenIds := make([]string, 0, len(file.ParsedBlocks)) for _, b := range file.ParsedBlocks { if isChildBlock(childBlocks, b) { continue diff --git a/core/block/import/markdown/import_test.go b/core/block/import/markdown/import_test.go index 62381f2ecc..dc8c40e346 100644 --- a/core/block/import/markdown/import_test.go +++ b/core/block/import/markdown/import_test.go @@ -11,6 +11,7 @@ import ( "github.com/anyproto/anytype-heart/core/block/process" "github.com/anyproto/anytype-heart/pb" "github.com/anyproto/anytype-heart/pkg/lib/pb/model" + "github.com/anyproto/anytype-heart/tests/blockbuilder" ) func TestMarkdown_GetSnapshots(t *testing.T) { @@ -75,6 +76,146 @@ func TestMarkdown_GetSnapshots(t *testing.T) { assert.Nil(t, sn) assert.True(t, err.IsNoObjectToImportError(1)) }) + t.Run("import file with links", func(t *testing.T) { + // given + tempDirProvider := &MockTempDir{} + converter := newMDConverter(tempDirProvider) + h := &Markdown{blockConverter: converter} + p := process.NewProgress(pb.ModelProcess_Import) + + // when + sn, err := h.GetSnapshots(context.Background(), &pb.RpcObjectImportRequest{ + Params: &pb.RpcObjectImportRequestParamsOfMarkdownParams{ + MarkdownParams: &pb.RpcObjectImportRequestMarkdownParams{Path: []string{"testdata"}}, + }, + Type: model.Import_Markdown, + Mode: pb.RpcObjectImportRequest_IGNORE_ERRORS, + }, p) + + // then + assert.Nil(t, err) + assert.NotNil(t, sn) + assert.Len(t, sn.Snapshots, 4) + + fileNameToObjectId := make(map[string]string, len(sn.Snapshots)) + for _, snapshot := range sn.Snapshots { + fileNameToObjectId[snapshot.FileName] = snapshot.Id + } + var found bool + expectedPath := filepath.Join("testdata", "links.md") + rootId := fileNameToObjectId[expectedPath] + want := buildExpectedTree(fileNameToObjectId, tempDirProvider, rootId) + for _, snapshot := range sn.Snapshots { + if snapshot.FileName == expectedPath { + found = true + blockbuilder.AssertTreesEqual(t, want.Build(), snapshot.Snapshot.Data.Blocks) + } + } + assert.True(t, found) + }) +} + +func buildExpectedTree(fileNameToObjectId map[string]string, provider *MockTempDir, rootId string) *blockbuilder.Block { + fileMdPath := filepath.Join("testdata", "file.md") + testMdPath := filepath.Join("testdata", "test.md") + testCsvPath := filepath.Join("testdata", "test.csv") + testTxtPath := filepath.Join("testdata", "test.txt") + want := blockbuilder.Root( + blockbuilder.ID(rootId), + blockbuilder.Children( + blockbuilder.Text("File does not exist test1", blockbuilder.TextMarks(model.BlockContentTextMarks{Marks: []*model.BlockContentTextMark{ + { + Range: &model.Range{From: 20, To: 25}, + Type: model.BlockContentTextMark_Link, + Param: fileMdPath, + }, + }})), + blockbuilder.Text("Test link to page test2", blockbuilder.TextMarks(model.BlockContentTextMarks{Marks: []*model.BlockContentTextMark{ + { + Range: &model.Range{From: 18, To: 23}, + Type: model.BlockContentTextMark_Mention, + Param: fileNameToObjectId[testMdPath], + }, + }})), + blockbuilder.File("", blockbuilder.FileName(provider.TempDir()+testTxtPath), blockbuilder.FileType(model.BlockContentFile_File)), + blockbuilder.Text("Test link to csv test4", blockbuilder.TextMarks(model.BlockContentTextMarks{Marks: []*model.BlockContentTextMark{ + { + Range: &model.Range{From: 17, To: 22}, + Type: model.BlockContentTextMark_Mention, + Param: fileNameToObjectId[testCsvPath], + }, + }})), + blockbuilder.Text("File does not exist with bold mark test1", blockbuilder.TextMarks(model.BlockContentTextMarks{Marks: []*model.BlockContentTextMark{ + { + Range: &model.Range{From: 35, To: 40}, + Type: model.BlockContentTextMark_Link, + Param: fileMdPath, + }, + { + Range: &model.Range{From: 35, To: 40}, + Type: model.BlockContentTextMark_Bold, + }, + }})), + blockbuilder.Text("Test link to page with bold mark test2", blockbuilder.TextMarks(model.BlockContentTextMarks{Marks: []*model.BlockContentTextMark{ + { + Range: &model.Range{From: 33, To: 38}, + Type: model.BlockContentTextMark_Mention, + Param: fileNameToObjectId[testMdPath], + }, + { + Range: &model.Range{From: 33, To: 38}, + Type: model.BlockContentTextMark_Bold, + }, + }})), + blockbuilder.Text("Test file block with bold mark test3", blockbuilder.TextMarks(model.BlockContentTextMarks{Marks: []*model.BlockContentTextMark{ + { + Range: &model.Range{From: 31, To: 36}, + Type: model.BlockContentTextMark_Link, + Param: testTxtPath, + }, + { + Range: &model.Range{From: 31, To: 36}, + Type: model.BlockContentTextMark_Bold, + }, + }})), + blockbuilder.Text("Test link to csv with bold mark test4", blockbuilder.TextMarks(model.BlockContentTextMarks{Marks: []*model.BlockContentTextMark{ + { + Range: &model.Range{From: 32, To: 37}, + Type: model.BlockContentTextMark_Mention, + Param: fileNameToObjectId[testCsvPath], + }, + { + Range: &model.Range{From: 32, To: 37}, + Type: model.BlockContentTextMark_Bold, + }, + }})), + blockbuilder.Bookmark(fileMdPath), + blockbuilder.Text("test2", blockbuilder.TextMarks(model.BlockContentTextMarks{Marks: []*model.BlockContentTextMark{ + { + Range: &model.Range{From: 0, To: 5}, + Type: model.BlockContentTextMark_Mention, + Param: fileNameToObjectId[testMdPath], + }, + { + Range: &model.Range{From: 0, To: 5}, + Type: model.BlockContentTextMark_Bold, + }, + }})), + blockbuilder.File("", blockbuilder.FileName(provider.TempDir()+testTxtPath), blockbuilder.FileType(model.BlockContentFile_File)), + blockbuilder.Text("test4", blockbuilder.TextMarks(model.BlockContentTextMarks{Marks: []*model.BlockContentTextMark{ + { + Range: &model.Range{From: 0, To: 5}, + Type: model.BlockContentTextMark_Mention, + Param: fileNameToObjectId[testCsvPath], + }, + { + Range: &model.Range{From: 0, To: 5}, + Type: model.BlockContentTextMark_Bold, + }, + }})), + blockbuilder.Link(rootId), + )) + return want } func setupTestDirectory(t *testing.T) string { diff --git a/core/block/import/markdown/testdata/links.md b/core/block/import/markdown/testdata/links.md new file mode 100644 index 0000000000..4de86546ea --- /dev/null +++ b/core/block/import/markdown/testdata/links.md @@ -0,0 +1,23 @@ +File does not exist [test1](file.md) + +Test link to page [test2](test.md) + +Test file block [test3](test.txt) + +Test link to csv [test4](test.csv) + +File does not exist with bold mark **[test1](file.md)** + +Test link to page with bold mark **[test2](test.md)** + +Test file block with bold mark **[test3](test.txt)** + +Test link to csv with bold mark **[test4](test.csv)** + +**[test1](file.md)** + +**[test2](test.md)** + +**[test3](test.txt)** + +**[test4](test.csv)** diff --git a/core/block/import/markdown/testdata/test.csv b/core/block/import/markdown/testdata/test.csv new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/block/import/markdown/testdata/test.txt b/core/block/import/markdown/testdata/test.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/blockbuilder/constructors.go b/tests/blockbuilder/constructors.go index c7d8dbc6be..41665307cf 100644 --- a/tests/blockbuilder/constructors.go +++ b/tests/blockbuilder/constructors.go @@ -83,6 +83,8 @@ type options struct { id string backgroundColor string fileHash string + fileName string + fileType model.BlockContentFileType } type Option func(*options) @@ -214,6 +216,18 @@ func FileHash(hash string) Option { } } +func FileName(fileName string) Option { + return func(o *options) { + o.fileName = fileName + } +} + +func FileType(fileType model.BlockContentFileType) Option { + return func(o *options) { + o.fileType = fileType + } +} + func File(targetObjectId string, opts ...Option) *Block { var o options for _, apply := range opts { @@ -225,7 +239,29 @@ func File(targetObjectId string, opts ...Option) *Block { File: &model.BlockContentFile{ Hash: o.fileHash, TargetObjectId: targetObjectId, + Name: o.fileName, + Type: o.fileType, }, }, }, opts...) } + +func Bookmark(url string) *Block { + return mkBlock(&model.Block{ + Content: &model.BlockContentOfBookmark{ + Bookmark: &model.BlockContentBookmark{ + Url: url, + }, + }, + }) +} + +func Link(targetBlockId string) *Block { + return mkBlock(&model.Block{ + Content: &model.BlockContentOfLink{ + Link: &model.BlockContentLink{ + TargetBlockId: targetBlockId, + }, + }, + }) +}