diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx index 0892d011e5b04..0af253c52d8e0 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx @@ -30,19 +30,23 @@ const BlocksuiteTextRenderer = createReactComponentFromLit({ const CollapsibleSection = ({ title, children, + length, }: { title: ReactNode; children: ReactNode; + length?: number; }) => { const [open, setOpen] = useState(false); return ( {title} - + {length ? ( + + ) : null} {children} @@ -152,15 +156,28 @@ export const BiDirectionalLinkPanel = () => { } + length={linkGroup.links.length} >
{linkGroup.links.map(link => { if (!link.markdownPreview) { return null; } + const searchParams = new URLSearchParams(); + searchParams.set('mode', link.displayMode || 'page'); + + // if note has displayMode === 'edgeless' && has noteBlockId, + // set noteBlockId as blockId + searchParams.set( + 'blockIds', + link.displayMode === 'edgeless' && link.noteBlockId + ? link.noteBlockId + : link.blockId + ); + const to = { pathname: '/' + linkGroup.docId, - search: `?blockIds=${link.blockId}`, + search: '?' + searchParams.toString(), hash: '', }; return ( diff --git a/packages/frontend/core/src/modules/doc-link/entities/doc-backlinks.ts b/packages/frontend/core/src/modules/doc-link/entities/doc-backlinks.ts index cee54f9810a42..5af4562ff8ef8 100644 --- a/packages/frontend/core/src/modules/doc-link/entities/doc-backlinks.ts +++ b/packages/frontend/core/src/modules/doc-link/entities/doc-backlinks.ts @@ -7,6 +7,8 @@ export interface Backlink { docId: string; blockId: string; title: string; + noteBlockId?: string; + displayMode?: string; markdownPreview?: string; } diff --git a/packages/frontend/core/src/modules/docs-search/schema.ts b/packages/frontend/core/src/modules/docs-search/schema.ts index 9c8fe09ed15d7..4f59ecf8710a3 100644 --- a/packages/frontend/core/src/modules/docs-search/schema.ts +++ b/packages/frontend/core/src/modules/docs-search/schema.ts @@ -12,6 +12,8 @@ export type DocIndexSchema = typeof docIndexSchema; export const blockIndexSchema = defineSchema({ docId: 'String', blockId: 'String', + // note id of the block. used for focusing the note in edgeless mode + noteBlockId: 'String', content: 'FullText', flavour: 'String', blob: 'String', @@ -29,6 +31,8 @@ export const blockIndexSchema = defineSchema({ // { "databaseName": "xxx" } additional: { type: 'String', index: false }, markdownPreview: { type: 'String', index: false }, + // display mode of the block, value could be 'page' or 'edgeless' or 'both' + displayMode: 'String', }); export type BlockIndexSchema = typeof blockIndexSchema; diff --git a/packages/frontend/core/src/modules/docs-search/services/docs-search.ts b/packages/frontend/core/src/modules/docs-search/services/docs-search.ts index 9e788eff2fb8a..7a83ba142516c 100644 --- a/packages/frontend/core/src/modules/docs-search/services/docs-search.ts +++ b/packages/frontend/core/src/modules/docs-search/services/docs-search.ts @@ -478,7 +478,13 @@ export class DocsSearchService extends Service { 'docId', { hits: { - fields: ['docId', 'blockId', 'markdownPreview'], + fields: [ + 'docId', + 'blockId', + 'noteBlockId', + 'markdownPreview', + 'displayMode', + ], pagination: { limit: 5, // the max number of backlinks to show for each doc }, @@ -502,6 +508,9 @@ export class DocsSearchService extends Service { return bucket.hits.nodes.map(node => { const blockId = node.fields.blockId ?? ''; const markdownPreview = node.fields.markdownPreview ?? ''; + const displayMode = node.fields.displayMode ?? ''; + const noteBlockId = node.fields.noteBlockId ?? ''; + return { docId: bucket.key, blockId: typeof blockId === 'string' ? blockId : blockId[0], @@ -510,6 +519,14 @@ export class DocsSearchService extends Service { typeof markdownPreview === 'string' ? markdownPreview : markdownPreview[0], + displayMode: + typeof displayMode === 'string' + ? displayMode + : displayMode[0], + noteBlockId: + typeof noteBlockId === 'string' + ? noteBlockId + : noteBlockId[0], }; }); }); diff --git a/packages/frontend/core/src/modules/docs-search/worker/in-worker.ts b/packages/frontend/core/src/modules/docs-search/worker/in-worker.ts index 54239af69c69d..140f7559ba1c8 100644 --- a/packages/frontend/core/src/modules/docs-search/worker/in-worker.ts +++ b/packages/frontend/core/src/modules/docs-search/worker/in-worker.ts @@ -89,9 +89,11 @@ interface BlockDocumentInfo { ref?: string[]; parentFlavour?: string; parentBlockId?: string; + noteBlockId?: string; additional?: { databaseName?: string }; yblock: YMap; markdownPreview?: string; + displayMode?: string; } const bookmarkFlavours = new Set([ @@ -260,7 +262,7 @@ function generateMarkdownPreviewBuilder( const url = getDocLink(block.docId, databaseModel.id); const title = databaseModel.title; - return `[database · ${title}][](${url})\n`; + return `[database · ${title ?? 'Untitled'}][](${url})\n`; }; const generateImageMarkdownPreview = (block: BlockDocumentInfo) => { @@ -351,10 +353,7 @@ function generateMarkdownPreviewBuilder( return `[${draftModel.name}](${draftModel.sourceId})\n`; }; - const generateMarkdownPreview = async ( - block: BlockDocumentInfo, - target?: BlockDocumentInfo - ) => { + const generateMarkdownPreview = async (block: BlockDocumentInfo) => { if (markdownPreviewCache.has(block)) { return markdownPreviewCache.get(block); } @@ -380,18 +379,6 @@ function generateMarkdownPreviewBuilder( markdown = trimParagraph(markdown); } } - - // only list can have indent - if (flavour === 'affine:list' && markdown) { - const blockDepth = getParentBlockCount(block); - const targetDepth = target ? getParentBlockCount(target) : blockDepth; - - const depth = targetDepth ? blockDepth - targetDepth : 0; - markdown = indentMarkdown( - markdown ?? '', - targetDepth > 0 ? depth + 1 : depth - ); - } } else if (flavour === 'affine:database') { markdown = generateDatabaseMarkdownPreview(block); } else if ( @@ -412,6 +399,12 @@ function generateMarkdownPreviewBuilder( } else { console.warn(`unknown flavour: ${flavour}`); } + + if (markdown) { + const blockDepth = getParentBlockCount(block); + markdown = indentMarkdown(markdown, Math.max(0, blockDepth)); + } + markdownPreviewCache.set(block, markdown); return markdown; }; @@ -419,6 +412,18 @@ function generateMarkdownPreviewBuilder( return generateMarkdownPreview; } +// find the common least indent of all lines, and remove it from all lines +// ignore empty lines +function unindentMarkdown(markdown: string) { + const lines = markdown.split('\n'); + const minIndent = Math.min( + ...lines + .filter(line => line.trim() !== '') + .map(line => line.match(/^\s*/)?.[0]?.length ?? 0) + ); + return lines.map(line => line.slice(minIndent)).join('\n'); +} + async function crawlingDocData({ docBuffer, storageDocId, @@ -479,10 +484,41 @@ async function crawlingDocData({ const blocks = ydoc.getMap('blocks'); + // build a parent map for quick lookup + // for each block, record its parent id + const parentMap: Record = {}; + for (const [id, block] of blocks.entries()) { + const children = block.get('sys:children') as YArray | undefined; + if (children instanceof YArray && children.length) { + for (const child of children) { + parentMap[child] = id; + } + } + } + if (blocks.size === 0) { return { deletedDoc: [docId] }; } + // find the nearest block that satisfies the predicate + const nearest = ( + blockId: string, + predicate: (block: YMap) => boolean + ) => { + let current: string | null = blockId; + while (current) { + const block = blocks.get(current); + if (block && predicate(block)) { + return block; + } + current = parentMap[current] ?? null; + } + return null; + }; + + const nearestByFlavour = (blockId: string, flavour: string) => + nearest(blockId, block => block.get('sys:flavour') === flavour); + let rootBlockId: string | null = null; for (const block of blocks.values()) { const flavour = block.get('sys:flavour')?.toString(); @@ -528,17 +564,29 @@ async function crawlingDocData({ const flavour = block.get('sys:flavour')?.toString(); const parentFlavour = parentBlock?.get('sys:flavour')?.toString(); + const noteBlock = nearestByFlavour(blockId, 'affine:note'); + + const displayMode = noteBlock?.get('sys:displayMode') || 'edgeless'; + const noteBlockId: string | undefined = noteBlock + ?.get('sys:id') + ?.toString(); pushChildren(blockId, block); + const commonBlockProps = { + docId, + flavour, + blockId, + displayMode, + noteBlockId, + yblock: block, + }; + if (flavour === 'affine:page') { docTitle = block.get('prop:title').toString(); blockDocuments.push({ - docId, - flavour, - blockId, + ...commonBlockProps, content: docTitle, - yblock: block, }); } else if ( flavour === 'affine:paragraph' || @@ -578,9 +626,7 @@ async function crawlingDocData({ : undefined; blockDocuments.push({ - docId, - flavour, - blockId, + ...commonBlockProps, content: text.toString(), ...refs.reduce<{ refDocId: string[]; ref: string[] }>( (prev, curr) => { @@ -593,7 +639,6 @@ async function crawlingDocData({ parentFlavour, parentBlockId, additional: { databaseName }, - yblock: block, }); if (summaryLenNeeded > 0) { @@ -609,14 +654,11 @@ async function crawlingDocData({ // reference info const params = block.get('prop:params') ?? {}; blockDocuments.push({ - docId, - flavour, - blockId, + ...commonBlockProps, refDocId: [pageId], ref: [JSON.stringify({ docId: pageId, ...params })], parentFlavour, parentBlockId, - yblock: block, }); } } else if ( @@ -626,13 +668,10 @@ async function crawlingDocData({ const blobId = block.get('prop:sourceId'); if (typeof blobId === 'string') { blockDocuments.push({ - docId, - flavour, - blockId, + ...commonBlockProps, blob: [blobId], parentFlavour, parentBlockId, - yblock: block, }); } } else if (flavour === 'affine:surface') { @@ -665,13 +704,10 @@ async function crawlingDocData({ } blockDocuments.push({ - docId, - flavour, - blockId, + ...commonBlockProps, content: texts, parentFlavour, parentBlockId, - yblock: block, }); } else if (flavour === 'affine:database') { const texts = []; @@ -710,26 +746,17 @@ async function crawlingDocData({ } blockDocuments.push({ - docId, - flavour, - blockId, + ...commonBlockProps, content: texts, - yblock: block, }); } else if (flavour === 'affine:latex') { blockDocuments.push({ - docId, - flavour, - blockId, + ...commonBlockProps, content: block.get('prop:latex')?.toString() ?? '', - yblock: block, }); } else if (bookmarkFlavours.has(flavour)) { blockDocuments.push({ - docId, - flavour, - blockId, - yblock: block, + ...commonBlockProps, }); } } @@ -743,6 +770,15 @@ async function crawlingDocData({ const block = blockDocuments[i]; if (block.ref?.length) { const target = block; + + // should only generate the markdown preview belong to the same affine:note + const noteBlock = nearestByFlavour(block.blockId, 'affine:note'); + + const sameNoteBlocks = blockDocuments.filter( + candidate => + nearestByFlavour(candidate.blockId, 'affine:note') === noteBlock + ); + // only generate markdown preview for reference blocks let previewText = (await generateMarkdownPreview(target)) ?? ''; let previousBlock = 0; @@ -756,16 +792,16 @@ async function crawlingDocData({ previewText.length > TARGET_PREVIEW_CHARACTER || // stop if preview text reaches the limit ((previousBlock >= TARGET_PREVIOUS_BLOCK || previousIndex < 0) && (followBlock >= TARGET_FOLLOW_BLOCK || - followIndex >= blockDocuments.length)) + followIndex >= sameNoteBlocks.length)) ) // stop if no more blocks, or preview block reaches the limit ) ) { if (previousBlock < TARGET_PREVIOUS_BLOCK) { previousIndex--; const block = - previousIndex >= 0 ? blockDocuments.at(previousIndex) : null; + previousIndex >= 0 ? sameNoteBlocks.at(previousIndex) : null; const markdown = block - ? await generateMarkdownPreview(block, target) + ? await generateMarkdownPreview(block) : null; if ( markdown && @@ -780,9 +816,9 @@ async function crawlingDocData({ if (followBlock < TARGET_FOLLOW_BLOCK) { followIndex++; - const block = blockDocuments.at(followIndex); + const block = sameNoteBlocks.at(followIndex); const markdown = block - ? await generateMarkdownPreview(block, target) + ? await generateMarkdownPreview(block) : null; if ( markdown && @@ -796,7 +832,7 @@ async function crawlingDocData({ } } - block.markdownPreview = previewText; + block.markdownPreview = unindentMarkdown(previewText); } } // #endregion @@ -824,6 +860,8 @@ async function crawlingDocData({ ? JSON.stringify(block.additional) : undefined, markdownPreview: block.markdownPreview, + displayMode: block.displayMode, + noteBlockId: block.noteBlockId, }) ), },