diff --git a/blocksuite/affine/all/package.json b/blocksuite/affine/all/package.json index 0f1fdcfb1b..3b674f3dd4 100644 --- a/blocksuite/affine/all/package.json +++ b/blocksuite/affine/all/package.json @@ -226,6 +226,7 @@ "./components/block-zero-width": "./src/components/block-zero-width.ts", "./components/caption": "./src/components/caption.ts", "./components/card-style-dropdown-menu": "./src/components/card-style-dropdown-menu.ts", + "./components/citation": "./src/components/citation.ts", "./components/color-picker": "./src/components/color-picker.ts", "./components/context-menu": "./src/components/context-menu.ts", "./components/date-picker": "./src/components/date-picker.ts", diff --git a/blocksuite/affine/all/src/adapters/markdown/block-matcher.ts b/blocksuite/affine/all/src/adapters/markdown/block-matcher.ts index 58ac5806bb..f946528479 100644 --- a/blocksuite/affine/all/src/adapters/markdown/block-matcher.ts +++ b/blocksuite/affine/all/src/adapters/markdown/block-matcher.ts @@ -1,3 +1,4 @@ +import { AttachmentBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-attachment'; import { BookmarkBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-bookmark'; import { CodeBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-code'; import { DatabaseBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-database'; @@ -38,4 +39,5 @@ export const defaultBlockMarkdownAdapterMatchers = [ DividerBlockMarkdownAdapterExtension, ImageBlockMarkdownAdapterExtension, LatexBlockMarkdownAdapterExtension, + AttachmentBlockMarkdownAdapterExtension, ]; diff --git a/blocksuite/affine/all/src/effects.ts b/blocksuite/affine/all/src/effects.ts index 5c0a6d2603..f54c5342b7 100644 --- a/blocksuite/affine/all/src/effects.ts +++ b/blocksuite/affine/all/src/effects.ts @@ -21,6 +21,7 @@ import { BlockSelection } from '@blocksuite/affine-components/block-selection'; import { BlockZeroWidth } from '@blocksuite/affine-components/block-zero-width'; import { effects as componentCaptionEffects } from '@blocksuite/affine-components/caption'; import { effects as componentCardStyleDropdownMenuEffects } from '@blocksuite/affine-components/card-style-dropdown-menu'; +import { effects as componentCitationEffects } from '@blocksuite/affine-components/citation'; import { effects as componentColorPickerEffects } from '@blocksuite/affine-components/color-picker'; import { effects as componentContextMenuEffects } from '@blocksuite/affine-components/context-menu'; import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker'; @@ -154,6 +155,7 @@ export function effects() { componentEmbedCardModalEffects(); componentLinkPreviewEffects(); componentLinkedDocTitleEffects(); + componentCitationEffects(); componentCardStyleDropdownMenuEffects(); componentHighlightDropdownMenuEffects(); componentViewDropdownMenuEffects(); diff --git a/blocksuite/affine/blocks/attachment/src/adapters/extension.ts b/blocksuite/affine/blocks/attachment/src/adapters/extension.ts new file mode 100644 index 0000000000..d2a1d2c5f3 --- /dev/null +++ b/blocksuite/affine/blocks/attachment/src/adapters/extension.ts @@ -0,0 +1,9 @@ +import type { ExtensionType } from '@blocksuite/store'; + +import { AttachmentBlockMarkdownAdapterExtension } from './markdown.js'; +import { AttachmentBlockNotionHtmlAdapterExtension } from './notion-html.js'; + +export const AttachmentBlockAdapterExtensions: ExtensionType[] = [ + AttachmentBlockNotionHtmlAdapterExtension, + AttachmentBlockMarkdownAdapterExtension, +]; diff --git a/blocksuite/affine/blocks/attachment/src/adapters/index.ts b/blocksuite/affine/blocks/attachment/src/adapters/index.ts new file mode 100644 index 0000000000..da925cf1ae --- /dev/null +++ b/blocksuite/affine/blocks/attachment/src/adapters/index.ts @@ -0,0 +1,2 @@ +export * from './markdown.js'; +export * from './notion-html.js'; diff --git a/blocksuite/affine/blocks/attachment/src/adapters/markdown.ts b/blocksuite/affine/blocks/attachment/src/adapters/markdown.ts new file mode 100644 index 0000000000..5f5e8537f5 --- /dev/null +++ b/blocksuite/affine/blocks/attachment/src/adapters/markdown.ts @@ -0,0 +1,93 @@ +import { + AttachmentBlockSchema, + FootNoteReferenceParamsSchema, +} from '@blocksuite/affine-model'; +import { + BlockMarkdownAdapterExtension, + type BlockMarkdownAdapterMatcher, + FOOTNOTE_DEFINITION_PREFIX, + getFootnoteDefinitionText, + isFootnoteDefinitionNode, + type MarkdownAST, +} from '@blocksuite/affine-shared/adapters'; +import { FeatureFlagService } from '@blocksuite/affine-shared/services'; +import { nanoid } from '@blocksuite/store'; + +const isAttachmentFootnoteDefinitionNode = (node: MarkdownAST) => { + if (!isFootnoteDefinitionNode(node)) return false; + const footnoteDefinition = getFootnoteDefinitionText(node); + try { + const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse( + JSON.parse(footnoteDefinition) + ); + return ( + footnoteDefinitionJson.type === 'attachment' && + !!footnoteDefinitionJson.blobId + ); + } catch { + return false; + } +}; + +export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = + { + flavour: AttachmentBlockSchema.model.flavour, + toMatch: o => isAttachmentFootnoteDefinitionNode(o.node), + fromMatch: o => o.node.flavour === AttachmentBlockSchema.model.flavour, + toBlockSnapshot: { + enter: (o, context) => { + const { provider } = context; + let enableCitation = false; + try { + const featureFlagService = provider?.get(FeatureFlagService); + enableCitation = !!featureFlagService?.getFlag('enable_citation'); + } catch { + enableCitation = false; + } + if (!isFootnoteDefinitionNode(o.node) || !enableCitation) { + return; + } + + const { walkerContext, configs } = context; + const footnoteIdentifier = o.node.identifier; + const footnoteDefinitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${footnoteIdentifier}`; + const footnoteDefinition = configs.get(footnoteDefinitionKey); + if (!footnoteDefinition) { + return; + } + try { + const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse( + JSON.parse(footnoteDefinition) + ); + const { blobId, fileName } = footnoteDefinitionJson; + if (!blobId || !fileName) { + return; + } + walkerContext + .openNode( + { + type: 'block', + id: nanoid(), + flavour: AttachmentBlockSchema.model.flavour, + props: { + name: fileName, + sourceId: blobId, + footnoteIdentifier, + }, + children: [], + }, + 'children' + ) + .closeNode(); + walkerContext.skipAllChildren(); + } catch (err) { + console.warn('Failed to parse attachment footnote definition:', err); + return; + } + }, + }, + fromBlockSnapshot: {}, + }; + +export const AttachmentBlockMarkdownAdapterExtension = + BlockMarkdownAdapterExtension(attachmentBlockMarkdownAdapterMatcher); diff --git a/blocksuite/affine/blocks/attachment/src/attachment-block.ts b/blocksuite/affine/blocks/attachment/src/attachment-block.ts index b4543f30fb..7e5302b87a 100644 --- a/blocksuite/affine/blocks/attachment/src/attachment-block.ts +++ b/blocksuite/affine/blocks/attachment/src/attachment-block.ts @@ -53,6 +53,10 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent { return this.std .get(AttachmentEmbedProvider) @@ -147,7 +151,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent { this.doc.updateBlock(this.model, { style: AttachmentBlockStyles[1], @@ -322,6 +326,18 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent { + const { name, footnoteIdentifier } = this.model.props; + const fileType = name.split('.').pop() ?? ''; + const fileTypeIcon = getAttachmentFileIcon(fileType); + return html``; + }; + override renderBlock() { return html`
${when( - this.embedView, + this.isCitation, + () => this._renderCitation(), () => - html`
- ${this.embedView} -
`, - this.renderCard + when( + this.embedView, + () => + html`
+ ${this.embedView} +
`, + this.renderCard + ) )}
`; diff --git a/blocksuite/affine/blocks/attachment/src/attachment-spec.ts b/blocksuite/affine/blocks/attachment/src/attachment-spec.ts index 727bcd07ca..928778625a 100644 --- a/blocksuite/affine/blocks/attachment/src/attachment-spec.ts +++ b/blocksuite/affine/blocks/attachment/src/attachment-spec.ts @@ -4,7 +4,7 @@ import { BlockViewExtension, FlavourExtension } from '@blocksuite/std'; import type { ExtensionType } from '@blocksuite/store'; import { literal } from 'lit/static-html.js'; -import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html.js'; +import { AttachmentBlockAdapterExtensions } from './adapters/extension.js'; import { AttachmentDropOption } from './attachment-service.js'; import { attachmentSlashMenuConfig } from './configs/slash-menu.js'; import { createBuiltinToolbarConfigExtension } from './configs/toolbar'; @@ -25,7 +25,7 @@ export const AttachmentBlockSpec: ExtensionType[] = [ AttachmentDropOption, AttachmentEmbedConfigExtension(), AttachmentEmbedService, - AttachmentBlockNotionHtmlAdapterExtension, + AttachmentBlockAdapterExtensions, createBuiltinToolbarConfigExtension(flavour), SlashMenuConfigExtension(flavour, attachmentSlashMenuConfig), ].flat(); diff --git a/blocksuite/affine/blocks/attachment/src/configs/toolbar.ts b/blocksuite/affine/blocks/attachment/src/configs/toolbar.ts index 0133d8a2d6..d9e952c755 100644 --- a/blocksuite/affine/blocks/attachment/src/configs/toolbar.ts +++ b/blocksuite/affine/blocks/attachment/src/configs/toolbar.ts @@ -139,6 +139,12 @@ const downloadAction = { const block = ctx.getCurrentBlockByType(AttachmentBlockComponent); block?.download(); }, + when: ctx => { + const model = ctx.getCurrentModelByType(AttachmentBlockModel); + if (!model) return false; + // Current citation attachment block does not support download + return model.props.style !== 'citation' && !model.props.footnoteIdentifier; + }, } as const satisfies ToolbarAction; const captionAction = { @@ -331,7 +337,6 @@ const builtinSurfaceToolbarConfig = { id: 'e.caption', }, ], - when: ctx => ctx.getSurfaceModelsByType(AttachmentBlockModel).length === 1, } as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/blocks/attachment/src/index.ts b/blocksuite/affine/blocks/attachment/src/index.ts index 86842b0028..df20d1d708 100644 --- a/blocksuite/affine/blocks/attachment/src/index.ts +++ b/blocksuite/affine/blocks/attachment/src/index.ts @@ -1,4 +1,4 @@ -export * from './adapters/notion-html'; +export * from './adapters'; export * from './attachment-block'; export * from './attachment-service'; export * from './attachment-spec'; diff --git a/blocksuite/affine/blocks/attachment/src/store.ts b/blocksuite/affine/blocks/attachment/src/store.ts index 114446856c..56605746e7 100644 --- a/blocksuite/affine/blocks/attachment/src/store.ts +++ b/blocksuite/affine/blocks/attachment/src/store.ts @@ -4,7 +4,7 @@ import { } from '@blocksuite/affine-ext-loader'; import { AttachmentBlockSchemaExtension } from '@blocksuite/affine-model'; -import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html'; +import { AttachmentBlockAdapterExtensions } from './adapters/extension'; export class AttachmentStoreExtension extends StoreExtensionProvider { override name = 'affine-attachment-block'; @@ -12,6 +12,6 @@ export class AttachmentStoreExtension extends StoreExtensionProvider { override setup(context: StoreExtensionContext) { super.setup(context); context.register(AttachmentBlockSchemaExtension); - context.register(AttachmentBlockNotionHtmlAdapterExtension); + context.register(AttachmentBlockAdapterExtensions); } } diff --git a/blocksuite/affine/blocks/bookmark/src/adapters/markdown/markdown.ts b/blocksuite/affine/blocks/bookmark/src/adapters/markdown/markdown.ts index 9bcb816b3d..56dfc0d4be 100644 --- a/blocksuite/affine/blocks/bookmark/src/adapters/markdown/markdown.ts +++ b/blocksuite/affine/blocks/bookmark/src/adapters/markdown/markdown.ts @@ -1,9 +1,102 @@ import { createEmbedBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-embed'; -import { BookmarkBlockSchema } from '@blocksuite/affine-model'; -import { BlockMarkdownAdapterExtension } from '@blocksuite/affine-shared/adapters'; +import { + BookmarkBlockSchema, + FootNoteReferenceParamsSchema, +} from '@blocksuite/affine-model'; +import { + BlockMarkdownAdapterExtension, + FOOTNOTE_DEFINITION_PREFIX, + getFootnoteDefinitionText, + isFootnoteDefinitionNode, + type MarkdownAST, +} from '@blocksuite/affine-shared/adapters'; +import { FeatureFlagService } from '@blocksuite/affine-shared/services'; +import { nanoid } from '@blocksuite/store'; + +const isUrlFootnoteDefinitionNode = (node: MarkdownAST) => { + if (!isFootnoteDefinitionNode(node)) return false; + const footnoteDefinition = getFootnoteDefinitionText(node); + try { + const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse( + JSON.parse(footnoteDefinition) + ); + return ( + footnoteDefinitionJson.type === 'url' && !!footnoteDefinitionJson.url + ); + } catch { + return false; + } +}; export const bookmarkBlockMarkdownAdapterMatcher = - createEmbedBlockMarkdownAdapterMatcher(BookmarkBlockSchema.model.flavour); + createEmbedBlockMarkdownAdapterMatcher(BookmarkBlockSchema.model.flavour, { + toMatch: o => isUrlFootnoteDefinitionNode(o.node), + toBlockSnapshot: { + enter: (o, context) => { + const { provider } = context; + let enableCitation = false; + try { + const featureFlagService = provider?.get(FeatureFlagService); + enableCitation = !!featureFlagService?.getFlag('enable_citation'); + } catch { + enableCitation = false; + } + if (!isFootnoteDefinitionNode(o.node) || !enableCitation) { + return; + } + + const { walkerContext, configs } = context; + const footnoteIdentifier = o.node.identifier; + const footnoteDefinitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${footnoteIdentifier}`; + const footnoteDefinition = configs.get(footnoteDefinitionKey); + if (!footnoteDefinition) { + return; + } + let footnoteDefinitionJson; + try { + footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse( + JSON.parse(footnoteDefinition) + ); + // If the footnote definition contains url, decode it + if (footnoteDefinitionJson.url) { + footnoteDefinitionJson.url = decodeURIComponent( + footnoteDefinitionJson.url + ); + } + if (footnoteDefinitionJson.favicon) { + footnoteDefinitionJson.favicon = decodeURIComponent( + footnoteDefinitionJson.favicon + ); + } + } catch (err) { + console.warn('Failed to parse or decode footnote definition:', err); + return; + } + + const { url, favicon, title, description } = footnoteDefinitionJson; + walkerContext + .openNode( + { + type: 'block', + id: nanoid(), + flavour: BookmarkBlockSchema.model.flavour, + props: { + url, + footnoteIdentifier, + icon: favicon, + title, + description, + style: 'citation', + }, + children: [], + }, + 'children' + ) + .closeNode(); + walkerContext.skipAllChildren(); + }, + }, + }); export const BookmarkBlockMarkdownAdapterExtension = BlockMarkdownAdapterExtension(bookmarkBlockMarkdownAdapterMatcher); diff --git a/blocksuite/affine/blocks/bookmark/src/bookmark-block.ts b/blocksuite/affine/blocks/bookmark/src/bookmark-block.ts index e287eafbee..22f862a99d 100644 --- a/blocksuite/affine/blocks/bookmark/src/bookmark-block.ts +++ b/blocksuite/affine/blocks/bookmark/src/bookmark-block.ts @@ -4,6 +4,7 @@ import { } from '@blocksuite/affine-components/caption'; import type { BookmarkBlockModel } from '@blocksuite/affine-model'; import { DocModeProvider } from '@blocksuite/affine-shared/services'; +import { BlockSelection } from '@blocksuite/std'; import { computed, type ReadonlySignal } from '@preact/signals-core'; import { html } from 'lit'; import { property, query } from 'lit/decorators.js'; @@ -27,6 +28,14 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent; + selectBlock = () => { + const selectionManager = this.std.selection; + const blockSelection = selectionManager.create(BlockSelection, { + blockId: this.blockId, + }); + selectionManager.setGroup('note', [blockSelection]); + }; + open = () => { let link = this.model.props.url; if (!link.match(/^[a-zA-Z]+:\/\//)) { @@ -41,6 +50,37 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent { + const { title, description, url, icon, footnoteIdentifier } = + this.model.props; + return html` + + `; + }; + + private readonly _renderCardView = () => { + return html``; + }; + override connectedCallback() { super.connectedCallback(); @@ -58,6 +98,9 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent - + ${this.isCitation ? this._renderCitationView() : this._renderCardView()} `; } diff --git a/blocksuite/affine/blocks/bookmark/src/components/bookmark-card.ts b/blocksuite/affine/blocks/bookmark/src/components/bookmark-card.ts index 4424aaee35..672c5cbe9b 100644 --- a/blocksuite/affine/blocks/bookmark/src/components/bookmark-card.ts +++ b/blocksuite/affine/blocks/bookmark/src/components/bookmark-card.ts @@ -5,11 +5,7 @@ import { ThemeProvider } from '@blocksuite/affine-shared/services'; import { getHostName } from '@blocksuite/affine-shared/utils'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { OpenInNewIcon } from '@blocksuite/icons/lit'; -import { - BlockSelection, - isGfxBlockComponent, - ShadowlessElement, -} from '@blocksuite/std'; +import { isGfxBlockComponent, ShadowlessElement } from '@blocksuite/std'; import { html } from 'lit'; import { property } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; @@ -27,7 +23,7 @@ export class BookmarkCard extends SignalWatcher( const model = this.bookmark.model; if (model.parent?.flavour !== 'affine:surface') { - this._selectBlock(); + this.bookmark.selectBlock(); } } @@ -36,14 +32,6 @@ export class BookmarkCard extends SignalWatcher( this.bookmark.open(); } - private _selectBlock() { - const selectionManager = this.bookmark.host.selection; - const blockSelection = selectionManager.create(BlockSelection, { - blockId: this.bookmark.blockId, - }); - selectionManager.setGroup('note', [blockSelection]); - } - override connectedCallback(): void { super.connectedCallback(); diff --git a/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/adapters/markdown.ts b/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/adapters/markdown.ts index 796a67ac3b..1d405e5408 100644 --- a/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/adapters/markdown.ts +++ b/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/adapters/markdown.ts @@ -1,16 +1,91 @@ -import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model'; +import { + EmbedLinkedDocBlockSchema, + FootNoteReferenceParamsSchema, +} from '@blocksuite/affine-model'; import { AdapterTextUtils, BlockMarkdownAdapterExtension, type BlockMarkdownAdapterMatcher, + FOOTNOTE_DEFINITION_PREFIX, + getFootnoteDefinitionText, + isFootnoteDefinitionNode, + type MarkdownAST, } from '@blocksuite/affine-shared/adapters'; +import { FeatureFlagService } from '@blocksuite/affine-shared/services'; +import { nanoid } from '@blocksuite/store'; + +const isLinkedDocFootnoteDefinitionNode = (node: MarkdownAST) => { + if (!isFootnoteDefinitionNode(node)) return false; + const footnoteDefinition = getFootnoteDefinitionText(node); + try { + const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse( + JSON.parse(footnoteDefinition) + ); + return ( + footnoteDefinitionJson.type === 'doc' && !!footnoteDefinitionJson.docId + ); + } catch { + return false; + } +}; export const embedLinkedDocBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = { flavour: EmbedLinkedDocBlockSchema.model.flavour, - toMatch: () => false, + toMatch: o => isLinkedDocFootnoteDefinitionNode(o.node), fromMatch: o => o.node.flavour === EmbedLinkedDocBlockSchema.model.flavour, - toBlockSnapshot: {}, + toBlockSnapshot: { + enter: (o, context) => { + const { provider } = context; + let enableCitation = false; + try { + const featureFlagService = provider?.get(FeatureFlagService); + enableCitation = !!featureFlagService?.getFlag('enable_citation'); + } catch { + enableCitation = false; + } + if (!isFootnoteDefinitionNode(o.node) || !enableCitation) { + return; + } + + const { walkerContext, configs } = context; + const footnoteIdentifier = o.node.identifier; + const footnoteDefinitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${footnoteIdentifier}`; + const footnoteDefinition = configs.get(footnoteDefinitionKey); + if (!footnoteDefinition) { + return; + } + try { + const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse( + JSON.parse(footnoteDefinition) + ); + const { docId } = footnoteDefinitionJson; + if (!docId) { + return; + } + walkerContext + .openNode( + { + type: 'block', + id: nanoid(), + flavour: EmbedLinkedDocBlockSchema.model.flavour, + props: { + pageId: docId, + footnoteIdentifier, + style: 'citation', + }, + children: [], + }, + 'children' + ) + .closeNode(); + walkerContext.skipAllChildren(); + } catch (err) { + console.warn('Failed to parse linked doc footnote definition:', err); + return; + } + }, + }, fromBlockSnapshot: { enter: (o, context) => { const { configs, walkerContext } = context; diff --git a/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/embed-edgeless-linked-doc-block.ts b/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/embed-edgeless-linked-doc-block.ts index e04095918b..45fa614348 100644 --- a/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/embed-edgeless-linked-doc-block.ts +++ b/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/embed-edgeless-linked-doc-block.ts @@ -53,11 +53,11 @@ export class EmbedEdgelessLinkedDocBlockComponent extends toEdgelessEmbedBlock( doc.deleteBlock(this.model); }; - protected override _handleClick(evt: MouseEvent): void { + protected override _handleClick = (evt: MouseEvent): void => { if (isNewTabTrigger(evt)) { this.open({ openMode: 'open-in-new-tab', event: evt }); } else if (isNewViewTrigger(evt)) { this.open({ openMode: 'open-in-new-view', event: evt }); } - } + }; } diff --git a/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/embed-linked-doc-block.ts b/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/embed-linked-doc-block.ts index 992de2dd10..da1c43e343 100644 --- a/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/embed-linked-doc-block.ts +++ b/blocksuite/affine/blocks/embed/src/embed-linked-doc-block/embed-linked-doc-block.ts @@ -55,6 +55,11 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent { + // If this is a citation linked doc block, we don't need to load the linked doc and render linked doc content in card + if (this.isCitation) { + return; + } + const { loading = true, isError = false, @@ -243,6 +248,17 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent { if (isNewTabTrigger(event)) { this.open({ openMode: 'open-in-new-tab', event }); } else if (isNewViewTrigger(event)) { this.open({ openMode: 'open-in-new-view', event }); } - this._selectBlock(); - } - override connectedCallback() { - super.connectedCallback(); - - this._cardStyle = this.model.props.style; - this._referenceToNode = referenceToNode(this.model.props); - - this._load().catch(e => { - console.error(e); - this.isError = true; - }); - - const linkedDoc = this.linkedDoc; - if (linkedDoc) { - this.disposables.add( - linkedDoc.workspace.slots.docListUpdated.subscribe(() => { - this._load().catch(e => { - console.error(e); - this.isError = true; - }); - }) - ); - // Should throttle the blockUpdated event to avoid too many re-renders - // Because the blockUpdated event is triggered too frequently at some cases - this.disposables.add( - linkedDoc.slots.blockUpdated.subscribe( - throttle(payload => { - if ( - payload.type === 'update' && - ['', 'caption', 'xywh'].includes(payload.props.key) - ) { - return; - } - - if (payload.type === 'add' && payload.init) { - return; - } - - this._load().catch(e => { - console.error(e); - this.isError = true; - }); - }, RENDER_CARD_THROTTLE_MS) - ) - ); - - this._setDocUpdatedAt(); - this.disposables.add( - this.doc.workspace.slots.docListUpdated.subscribe(() => { - this._setDocUpdatedAt(); - }) - ); - - if (this._referenceToNode) { - this._linkedDocMode = this.model.props.params?.mode ?? 'page'; - } else { - const docMode = this.std.get(DocModeProvider); - this._linkedDocMode = docMode.getPrimaryMode(this.model.props.pageId); - this.disposables.add( - docMode.onPrimaryModeChange(mode => { - this._linkedDocMode = mode; - }, this.model.props.pageId) - ); - } + if (this.readonly) { + return; } + this._selectBlock(); + }; - this.disposables.add( - this.model.propsUpdated.subscribe(({ key }) => { - if (key === 'style') { - this._cardStyle = this.model.props.style; - } - if (key === 'pageId' || key === 'style') { - this._load().catch(e => { - console.error(e); - this.isError = true; - }); - } - }) - ); - } + private readonly _renderCitationView = () => { + const { footnoteIdentifier } = this.model.props; + return html`
+ +
`; + }; - getInitialState(): { - loading?: boolean; - isError?: boolean; - isNoteContentEmpty?: boolean; - isBannerEmpty?: boolean; - } { - return {}; - } - - override renderBlock() { + private readonly _renderEmbedView = () => { const linkedDoc = this.linkedDoc; const isDeleted = !linkedDoc; const isLoading = this._loading; @@ -502,9 +455,107 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent ` ); + }; + + override connectedCallback() { + super.connectedCallback(); + + this._cardStyle = this.model.props.style; + this._referenceToNode = referenceToNode(this.model.props); + + this._load().catch(e => { + console.error(e); + this.isError = true; + }); + + const linkedDoc = this.linkedDoc; + if (linkedDoc) { + this.disposables.add( + linkedDoc.workspace.slots.docListUpdated.subscribe(() => { + this._load().catch(e => { + console.error(e); + this.isError = true; + }); + }) + ); + // Should throttle the blockUpdated event to avoid too many re-renders + // Because the blockUpdated event is triggered too frequently at some cases + this.disposables.add( + linkedDoc.slots.blockUpdated.subscribe( + throttle(payload => { + if ( + payload.type === 'update' && + ['', 'caption', 'xywh'].includes(payload.props.key) + ) { + return; + } + + if (payload.type === 'add' && payload.init) { + return; + } + + this._load().catch(e => { + console.error(e); + this.isError = true; + }); + }, RENDER_CARD_THROTTLE_MS) + ) + ); + + this._setDocUpdatedAt(); + this.disposables.add( + this.doc.workspace.slots.docListUpdated.subscribe(() => { + this._setDocUpdatedAt(); + }) + ); + + if (this._referenceToNode) { + this._linkedDocMode = this.model.props.params?.mode ?? 'page'; + } else { + const docMode = this.std.get(DocModeProvider); + this._linkedDocMode = docMode.getPrimaryMode(this.model.props.pageId); + this.disposables.add( + docMode.onPrimaryModeChange(mode => { + this._linkedDocMode = mode; + }, this.model.props.pageId) + ); + } + } + + this.disposables.add( + this.model.propsUpdated.subscribe(({ key }) => { + if (key === 'style') { + this._cardStyle = this.model.props.style; + } + if (key === 'pageId' || key === 'style') { + this._load().catch(e => { + console.error(e); + this.isError = true; + }); + } + }) + ); + } + + getInitialState(): { + loading?: boolean; + isError?: boolean; + isNoteContentEmpty?: boolean; + isBannerEmpty?: boolean; + } { + return {}; + } + + override renderBlock() { + return this.isCitation + ? this._renderCitationView() + : this._renderEmbedView(); } override updated() { + if (this.readonly) { + return; + } // update card style when linked doc deleted const linkedDoc = this.linkedDoc; const { xywh, style } = this.model.props; diff --git a/blocksuite/affine/blocks/note/src/adapters/markdown.ts b/blocksuite/affine/blocks/note/src/adapters/markdown.ts index 879c645d02..a625bd5791 100644 --- a/blocksuite/affine/blocks/note/src/adapters/markdown.ts +++ b/blocksuite/affine/blocks/note/src/adapters/markdown.ts @@ -3,14 +3,13 @@ import { BlockMarkdownAdapterExtension, type BlockMarkdownAdapterMatcher, FOOTNOTE_DEFINITION_PREFIX, + isFootnoteDefinitionNode, type MarkdownAST, } from '@blocksuite/affine-shared/adapters'; -import type { FootnoteDefinition, Root } from 'mdast'; +import { FeatureFlagService } from '@blocksuite/affine-shared/services'; +import type { Root } from 'mdast'; const isRootNode = (node: MarkdownAST): node is Root => node.type === 'root'; -const isFootnoteDefinitionNode = ( - node: MarkdownAST -): node is FootnoteDefinition => node.type === 'footnoteDefinition'; const createFootnoteDefinition = ( identifier: string, @@ -67,10 +66,35 @@ const createNoteBlockMarkdownAdapterMatcher = ( } }); - // Remove the footnoteDefinition node from the noteAst - noteAst.children = noteAst.children.filter( - child => !isFootnoteDefinitionNode(child) - ); + const { provider } = context; + let enableCitation = false; + try { + const featureFlagService = provider?.get(FeatureFlagService); + enableCitation = !!featureFlagService?.getFlag('enable_citation'); + } catch { + enableCitation = false; + } + if (enableCitation) { + // if there are footnoteDefinition nodes, add a heading node to the noteAst before the first footnoteDefinition node + const footnoteDefinitionIndex = noteAst.children.findIndex(child => + isFootnoteDefinitionNode(child) + ); + if (footnoteDefinitionIndex !== -1) { + noteAst.children.splice(footnoteDefinitionIndex, 0, { + type: 'heading', + depth: 6, + data: { + collapsed: true, + }, + children: [{ type: 'text', value: 'Sources' }], + }); + } + } else { + // Remove the footnoteDefinition node from the noteAst + noteAst.children = noteAst.children.filter( + child => !isFootnoteDefinitionNode(child) + ); + } }, }, fromBlockSnapshot: { diff --git a/blocksuite/affine/blocks/paragraph/src/adapters/markdown.ts b/blocksuite/affine/blocks/paragraph/src/adapters/markdown.ts index 8179cba179..5abdbd5b9a 100644 --- a/blocksuite/affine/blocks/paragraph/src/adapters/markdown.ts +++ b/blocksuite/affine/blocks/paragraph/src/adapters/markdown.ts @@ -9,6 +9,15 @@ import type { DeltaInsert } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store'; import type { Heading } from 'mdast'; +/** + * Extend the HeadingData type to include the collapsed property + */ +declare module 'mdast' { + interface HeadingData { + collapsed?: boolean; + } +} + const PARAGRAPH_MDAST_TYPE = new Set(['paragraph', 'heading', 'blockquote']); const isParagraphMDASTType = (node: MarkdownAST) => @@ -46,6 +55,7 @@ export const paragraphBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = break; } case 'heading': { + const isCollapsed = !!o.node.data?.collapsed; walkerContext .openNode( { @@ -54,6 +64,7 @@ export const paragraphBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = flavour: 'affine:paragraph', props: { type: `h${o.node.depth}`, + collapsed: isCollapsed, text: { '$blocksuite:internal:text$': true, delta: deltaConverter.astToDelta(o.node), diff --git a/blocksuite/affine/components/package.json b/blocksuite/affine/components/package.json index 9206eac154..b7500ffdc7 100644 --- a/blocksuite/affine/components/package.json +++ b/blocksuite/affine/components/package.json @@ -63,6 +63,7 @@ "./linked-doc-title": "./src/linked-doc-title/index.ts", "./view-dropdown-menu": "./src/view-dropdown-menu/index.ts", "./card-style-dropdown-menu": "./src/card-style-dropdown-menu/index.ts", + "./citation": "./src/citation/index.ts", "./highlight-dropdown-menu": "./src/highlight-dropdown-menu/index.ts", "./tooltip-content-with-shortcut": "./src/tooltip-content-with-shortcut/index.ts", "./size-dropdown-menu": "./src/size-dropdown-menu/index.ts", diff --git a/blocksuite/affine/components/src/citation/citation.ts b/blocksuite/affine/components/src/citation/citation.ts new file mode 100644 index 0000000000..0595f63b92 --- /dev/null +++ b/blocksuite/affine/components/src/citation/citation.ts @@ -0,0 +1,167 @@ +import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; +import { baseTheme } from '@toeverything/theme'; +import { + css, + html, + LitElement, + nothing, + type TemplateResult, + unsafeCSS, +} from 'lit'; +import { property } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; + +export class CitationCard extends SignalWatcher(WithDisposable(LitElement)) { + static override styles = css` + .citation-container { + width: 100%; + box-sizing: border-box; + border-radius: 8px; + display: flex; + gap: 2px; + flex-direction: column; + align-items: flex-start; + align-self: stretch; + padding: 4px 8px; + background-color: ${unsafeCSSVarV2('layer/background/primary')}; + border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')}; + font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; + cursor: pointer; + } + + .citation-header { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + box-sizing: border-box; + + .citation-icon { + display: flex; + align-items: center; + justify-content: center; + height: 16px; + width: 16px; + color: ${unsafeCSSVarV2('icon/primary')}; + border-radius: 4px; + + svg, + img { + width: 16px; + height: 16px; + fill: ${unsafeCSSVarV2('icon/primary')}; + } + } + + .citation-title { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: left; + line-height: 22px; + color: ${unsafeCSSVarV2('text/primary')}; + font-size: var(--affine-font-sm); + font-weight: 500; + } + + .citation-identifier { + display: flex; + width: 14px; + height: 14px; + justify-content: center; + align-items: center; + border-radius: 36px; + background: ${unsafeCSSVarV2('block/footnote/numberBg')}; + color: ${unsafeCSSVarV2('text/primary')}; + text-align: center; + font-size: 10px; + font-style: normal; + font-weight: 400; + line-height: 22px; /* 220% */ + transition: background-color 0.3s ease-in-out; + } + } + + .citation-container:hover .citation-identifier, + .citation-identifier.active { + background: ${unsafeCSSVarV2('button/primary')}; + color: ${unsafeCSSVarV2('button/pureWhiteText')}; + } + + .citation-content { + width: 100%; + box-sizing: border-box; + overflow: hidden; + color: ${unsafeCSSVarV2('text/primary')}; + font-feature-settings: + 'liga' off, + 'clig' off; + text-overflow: ellipsis; + white-space: nowrap; + font-size: var(--affine-font-xs); + font-style: normal; + font-weight: 400; + line-height: 20px; /* 166.667% */ + } + `; + + private readonly _IconTemplate = (icon: TemplateResult | string) => { + if (typeof icon === 'string') { + return html`favicon`; + } + return icon; + }; + + override render() { + const citationIdentifierClasses = classMap({ + 'citation-identifier': true, + active: this.active, + }); + return html` +
+
+ ${this.icon + ? html`
+ ${this._IconTemplate(this.icon)} +
` + : nothing} +
${this.citationTitle}
+
+ ${this.citationIdentifier} +
+
+ ${this.citationContent + ? html`
${this.citationContent}
` + : nothing} +
+ `; + } + + @property({ attribute: false }) + accessor icon: TemplateResult | string | undefined = undefined; + + @property({ attribute: false }) + accessor citationTitle: string = ''; + + @property({ attribute: false }) + accessor citationContent: string | undefined = undefined; + + @property({ attribute: false }) + accessor citationIdentifier: string = ''; + + @property({ attribute: false }) + accessor onClickCallback: ((e: MouseEvent) => void) | undefined = undefined; + + @property({ attribute: false }) + accessor onDoubleClickCallback: ((e: MouseEvent) => void) | undefined = + undefined; + + @property({ attribute: false }) + accessor active: boolean = false; +} diff --git a/blocksuite/affine/components/src/citation/index.ts b/blocksuite/affine/components/src/citation/index.ts new file mode 100644 index 0000000000..6a270a3319 --- /dev/null +++ b/blocksuite/affine/components/src/citation/index.ts @@ -0,0 +1,7 @@ +import { CitationCard } from './citation'; + +export * from './citation'; + +export function effects() { + customElements.define('affine-citation-card', CitationCard); +} diff --git a/blocksuite/affine/components/src/toggle-button/toggle-button.ts b/blocksuite/affine/components/src/toggle-button/toggle-button.ts index ad2215f208..196ce3cbcb 100644 --- a/blocksuite/affine/components/src/toggle-button/toggle-button.ts +++ b/blocksuite/affine/components/src/toggle-button/toggle-button.ts @@ -1,3 +1,4 @@ +import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { WithDisposable } from '@blocksuite/global/lit'; import { ToggleDownIcon, ToggleRightIcon } from '@blocksuite/icons/lit'; import { ShadowlessElement } from '@blocksuite/std'; @@ -43,6 +44,12 @@ export class ToggleButton extends WithDisposable(ShadowlessElement) { .with-drag-handle .affine-block-children-container .toggle-icon { opacity: 0; } + + .toggle-icon { + svg { + color: ${unsafeCSSVarV2('icon/primary', '#77757D')}; + } + } `; override render() { @@ -55,7 +62,6 @@ export class ToggleButton extends WithDisposable(ShadowlessElement) { ${ToggleDownIcon({ width: '16px', height: '16px', - style: 'color: #77757D', })} `; @@ -70,7 +76,6 @@ export class ToggleButton extends WithDisposable(ShadowlessElement) { ${ToggleRightIcon({ width: '16px', height: '16px', - style: 'color: #77757D', })} `; diff --git a/blocksuite/affine/foundation/src/effects.ts b/blocksuite/affine/foundation/src/effects.ts index ac8c5897b2..5d5b266d71 100644 --- a/blocksuite/affine/foundation/src/effects.ts +++ b/blocksuite/affine/foundation/src/effects.ts @@ -2,6 +2,7 @@ import { BlockSelection } from '@blocksuite/affine-components/block-selection'; import { BlockZeroWidth } from '@blocksuite/affine-components/block-zero-width'; import { effects as componentCaptionEffects } from '@blocksuite/affine-components/caption'; import { effects as componentCardStyleDropdownMenuEffects } from '@blocksuite/affine-components/card-style-dropdown-menu'; +import { effects as componentCitationEffects } from '@blocksuite/affine-components/citation'; import { effects as componentColorPickerEffects } from '@blocksuite/affine-components/color-picker'; import { effects as componentContextMenuEffects } from '@blocksuite/affine-components/context-menu'; import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker'; @@ -46,6 +47,7 @@ export function effects() { componentLinkPreviewEffects(); componentLinkedDocTitleEffects(); componentCardStyleDropdownMenuEffects(); + componentCitationEffects(); componentHighlightDropdownMenuEffects(); componentViewDropdownMenuEffects(); componentTooltipContentWithShortcutEffects(); diff --git a/blocksuite/affine/inlines/footnote/src/footnote-node/footnote-node.ts b/blocksuite/affine/inlines/footnote/src/footnote-node/footnote-node.ts index aa1ae13a03..9fb53b1ff8 100644 --- a/blocksuite/affine/inlines/footnote/src/footnote-node/footnote-node.ts +++ b/blocksuite/affine/inlines/footnote/src/footnote-node/footnote-node.ts @@ -54,6 +54,7 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) { text-overflow: ellipsis; font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; transition: background 0.3s ease-in-out; + transform: translateY(-0.2em); } } @@ -137,10 +138,6 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) { }; private readonly _handleDocReference = (docId: string) => { - if (this.readonly) { - return; - } - this.std .getOptional(PeekViewProvider) ?.peek({ diff --git a/blocksuite/affine/model/src/blocks/attachment/attachment-model.ts b/blocksuite/affine/model/src/blocks/attachment/attachment-model.ts index 6cb36591a6..4bf1d98762 100644 --- a/blocksuite/affine/model/src/blocks/attachment/attachment-model.ts +++ b/blocksuite/affine/model/src/blocks/attachment/attachment-model.ts @@ -55,6 +55,8 @@ export type AttachmentBlockProps = { embed: boolean | BackwardCompatibleUndefined; style?: (typeof AttachmentBlockStyles)[number]; + + footnoteIdentifier: string | null; } & Omit & BlockMeta; @@ -74,6 +76,7 @@ export const defaultAttachmentProps: AttachmentBlockProps = { 'meta:updatedAt': undefined, 'meta:createdBy': undefined, 'meta:updatedBy': undefined, + footnoteIdentifier: null, }; export const AttachmentBlockSchema = defineBlockSchema({ diff --git a/blocksuite/affine/model/src/blocks/bookmark/bookmark-model.ts b/blocksuite/affine/model/src/blocks/bookmark/bookmark-model.ts index fb198d5def..84bd990188 100644 --- a/blocksuite/affine/model/src/blocks/bookmark/bookmark-model.ts +++ b/blocksuite/affine/model/src/blocks/bookmark/bookmark-model.ts @@ -20,12 +20,14 @@ export const BookmarkStyles: EmbedCardStyle[] = [ 'horizontal', 'list', 'cube', + 'citation', ] as const; export type BookmarkBlockProps = { style: (typeof BookmarkStyles)[number]; url: string; caption: string | null; + footnoteIdentifier: string | null; } & LinkPreviewData & Omit & BlockMeta; @@ -48,6 +50,8 @@ const defaultBookmarkProps: BookmarkBlockProps = { 'meta:updatedAt': undefined, 'meta:createdBy': undefined, 'meta:updatedBy': undefined, + + footnoteIdentifier: null, }; export const BookmarkBlockSchema = defineBlockSchema({ diff --git a/blocksuite/affine/model/src/blocks/embed/linked-doc/linked-doc-model.ts b/blocksuite/affine/model/src/blocks/embed/linked-doc/linked-doc-model.ts index dfca91366b..e119040d73 100644 --- a/blocksuite/affine/model/src/blocks/embed/linked-doc/linked-doc-model.ts +++ b/blocksuite/affine/model/src/blocks/embed/linked-doc/linked-doc-model.ts @@ -10,11 +10,13 @@ export const EmbedLinkedDocStyles: EmbedCardStyle[] = [ 'list', 'cube', 'horizontalThin', + 'citation', ]; export type EmbedLinkedDocBlockProps = { style: EmbedCardStyle; caption: string | null; + footnoteIdentifier: string | null; } & ReferenceInfo; export class EmbedLinkedDocModel extends defineEmbedModel( diff --git a/blocksuite/affine/model/src/blocks/embed/linked-doc/linked-doc-schema.ts b/blocksuite/affine/model/src/blocks/embed/linked-doc/linked-doc-schema.ts index 43055d0cc5..cde9c1d50c 100644 --- a/blocksuite/affine/model/src/blocks/embed/linked-doc/linked-doc-schema.ts +++ b/blocksuite/affine/model/src/blocks/embed/linked-doc/linked-doc-schema.ts @@ -14,6 +14,8 @@ const defaultEmbedLinkedDocBlockProps: EmbedLinkedDocBlockProps = { // title & description aliases title: undefined, description: undefined, + + footnoteIdentifier: null, }; export const EmbedLinkedDocBlockSchema = createEmbedBlockSchema({ diff --git a/blocksuite/affine/model/src/utils/types.ts b/blocksuite/affine/model/src/utils/types.ts index e696a5e644..720021b4e5 100644 --- a/blocksuite/affine/model/src/utils/types.ts +++ b/blocksuite/affine/model/src/utils/types.ts @@ -17,7 +17,8 @@ export type EmbedCardStyle = | 'figma' | 'html' | 'syncedDoc' - | 'pdf'; + | 'pdf' + | 'citation'; export type LinkPreviewData = { description: string | null; diff --git a/blocksuite/affine/shared/src/adapters/index.ts b/blocksuite/affine/shared/src/adapters/index.ts index 90f90b0778..7f828f421a 100644 --- a/blocksuite/affine/shared/src/adapters/index.ts +++ b/blocksuite/affine/shared/src/adapters/index.ts @@ -22,10 +22,12 @@ export { type BlockMarkdownAdapterMatcher, BlockMarkdownAdapterMatcherIdentifier, FOOTNOTE_DEFINITION_PREFIX, + getFootnoteDefinitionText, IN_PARAGRAPH_NODE_CONTEXT_KEY, InlineDeltaToMarkdownAdapterExtension, type InlineDeltaToMarkdownAdapterMatcher, InlineDeltaToMarkdownAdapterMatcherIdentifier, + isFootnoteDefinitionNode, isMarkdownAST, type Markdown, MarkdownAdapter, diff --git a/blocksuite/affine/shared/src/adapters/markdown/type.ts b/blocksuite/affine/shared/src/adapters/markdown/type.ts index 70a006ab0c..be1cad15b7 100644 --- a/blocksuite/affine/shared/src/adapters/markdown/type.ts +++ b/blocksuite/affine/shared/src/adapters/markdown/type.ts @@ -1,4 +1,4 @@ -import type { Root, RootContentMap } from 'mdast'; +import type { FootnoteDefinition, Root, RootContentMap } from 'mdast'; export type Markdown = string; @@ -16,5 +16,17 @@ export const isMarkdownAST = (node: unknown): node is MarkdownAST => 'type' in (node as object) && (node as MarkdownAST).type !== undefined; +export const isFootnoteDefinitionNode = ( + node: MarkdownAST +): node is FootnoteDefinition => node.type === 'footnoteDefinition'; + +export const getFootnoteDefinitionText = (node: FootnoteDefinition) => { + const childNode = node.children[0]; + if (childNode.type !== 'paragraph') return ''; + const paragraph = childNode.children[0]; + if (paragraph.type !== 'text') return ''; + return paragraph.value; +}; + export const FOOTNOTE_DEFINITION_PREFIX = 'footnoteDefinition:'; export const IN_PARAGRAPH_NODE_CONTEXT_KEY = 'mdast:paragraph'; diff --git a/blocksuite/affine/shared/src/consts/index.ts b/blocksuite/affine/shared/src/consts/index.ts index a2c4dfe839..1b78dc0158 100644 --- a/blocksuite/affine/shared/src/consts/index.ts +++ b/blocksuite/affine/shared/src/consts/index.ts @@ -31,6 +31,7 @@ export const EMBED_CARD_WIDTH: Record = { html: 752, syncedDoc: 800, pdf: 537 + 24 + 2, + citation: 752, }; export const EMBED_CARD_HEIGHT: Record = { @@ -45,6 +46,7 @@ export const EMBED_CARD_HEIGHT: Record = { html: 544, syncedDoc: 455, pdf: 759 + 46 + 24 + 2, + citation: 52, }; export const EMBED_BLOCK_FLAVOUR_LIST = [ diff --git a/blocksuite/affine/shared/src/services/feature-flag-service.ts b/blocksuite/affine/shared/src/services/feature-flag-service.ts index 895e812e42..ab03fe7216 100644 --- a/blocksuite/affine/shared/src/services/feature-flag-service.ts +++ b/blocksuite/affine/shared/src/services/feature-flag-service.ts @@ -20,6 +20,7 @@ export interface BlockSuiteFlags { enable_edgeless_scribbled_style: boolean; enable_embed_doc_with_alias: boolean; enable_turbo_renderer: boolean; + enable_citation: boolean; } export class FeatureFlagService extends StoreExtension { @@ -44,6 +45,7 @@ export class FeatureFlagService extends StoreExtension { enable_edgeless_scribbled_style: false, enable_embed_doc_with_alias: false, enable_turbo_renderer: false, + enable_citation: false, }); setFlag(key: keyof BlockSuiteFlags, value: boolean) { diff --git a/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts b/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts index b07c97097c..b415bd0018 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts @@ -121,10 +121,6 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) { font-weight: 400; } - .affine-paragraph-block-container { - line-height: 22px; - } - .ai-answer-text-editor { .affine-note-block-container { > .affine-block-children-container { @@ -138,6 +134,23 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) { } } } + + .affine-paragraph-block-container { + line-height: 22px; + + .h6 { + padding-left: 16px; + color: ${unsafeCSSVarV2('text/link')}; + font-size: var(--affine-font-base); + + .toggle-icon { + transform: translateX(0); + svg { + color: ${unsafeCSSVarV2('text/link')}; + } + } + } + } } .text-renderer-container { @@ -216,6 +229,9 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) { 'affine:list', 'affine:divider', 'affine:latex', + 'affine:bookmark', + 'affine:attachment', + 'affine:embed-linked-doc', ].map(flavour => ({ flavour, viewType: 'display' })), }; diff --git a/packages/frontend/core/src/modules/feature-flag/constant.ts b/packages/frontend/core/src/modules/feature-flag/constant.ts index d4755e6679..ce0516b9a9 100644 --- a/packages/frontend/core/src/modules/feature-flag/constant.ts +++ b/packages/frontend/core/src/modules/feature-flag/constant.ts @@ -105,6 +105,16 @@ export const AFFINE_FLAGS = { configurable: isCanaryBuild, defaultState: isCanaryBuild, }, + enable_citation: { + category: 'blocksuite', + bsFlag: 'enable_citation', + displayName: + 'com.affine.settings.workspace.experimental-features.enable-citation.name', + description: + 'com.affine.settings.workspace.experimental-features.enable-citation.description', + configurable: isCanaryBuild, + defaultState: isCanaryBuild, + }, enable_emoji_folder_icon: { category: 'affine', diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index 2becd71bbc..6c275a1f8f 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -5617,6 +5617,14 @@ export function useAFFiNEI18N(): { * `Let your words stand out. This also include the callout in the transcription block.` */ ["com.affine.settings.workspace.experimental-features.enable-callout.description"](): string; + /** + * `Citation` + */ + ["com.affine.settings.workspace.experimental-features.enable-citation.name"](): string; + /** + * `Enable citation feature.` + */ + ["com.affine.settings.workspace.experimental-features.enable-citation.description"](): string; /** * `Embed Iframe Block` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index e91270956e..19e115cdf7 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1402,6 +1402,8 @@ "com.affine.settings.workspace.experimental-features.enable-block-meta.description": "Once enabled, all blocks will have created time, updated time, created by and updated by.", "com.affine.settings.workspace.experimental-features.enable-callout.name": "Callout", "com.affine.settings.workspace.experimental-features.enable-callout.description": "Let your words stand out. This also include the callout in the transcription block.", + "com.affine.settings.workspace.experimental-features.enable-citation.name": "Citation", + "com.affine.settings.workspace.experimental-features.enable-citation.description": "Enable citation feature.", "com.affine.settings.workspace.experimental-features.enable-embed-iframe-block.name": "Embed Iframe Block", "com.affine.settings.workspace.experimental-features.enable-embed-iframe-block.description": "Enables Embed Iframe Block.", "com.affine.settings.workspace.experimental-features.enable-emoji-folder-icon.name": "Emoji Folder Icon", diff --git a/tests/blocksuite/snapshots/attachment.spec.ts/can-insert-attachment-from-slash-menu.json b/tests/blocksuite/snapshots/attachment.spec.ts/can-insert-attachment-from-slash-menu.json index 71b75acb65..6d4cb17925 100644 --- a/tests/blocksuite/snapshots/attachment.spec.ts/can-insert-attachment-from-slash-menu.json +++ b/tests/blocksuite/snapshots/attachment.spec.ts/can-insert-attachment-from-slash-menu.json @@ -9,6 +9,7 @@ "id": "3", "props": { "embed": false, + "footnoteIdentifier": null, "index": "a0", "lockedBySelf": false, "name": "test-card-1.png", diff --git a/tests/blocksuite/snapshots/attachment.spec.ts/should-turn-attachment-to-image-works-2.json b/tests/blocksuite/snapshots/attachment.spec.ts/should-turn-attachment-to-image-works-2.json index ff5c129dca..931521f9de 100644 --- a/tests/blocksuite/snapshots/attachment.spec.ts/should-turn-attachment-to-image-works-2.json +++ b/tests/blocksuite/snapshots/attachment.spec.ts/should-turn-attachment-to-image-works-2.json @@ -10,6 +10,7 @@ "props": { "caption": "", "embed": false, + "footnoteIdentifier": null, "index": "a0", "lockedBySelf": false, "name": "test-card-1.png", diff --git a/tests/blocksuite/snapshots/attachment.spec.ts/should-undo-redo-works-for-attachment-1.json b/tests/blocksuite/snapshots/attachment.spec.ts/should-undo-redo-works-for-attachment-1.json index 71b75acb65..6d4cb17925 100644 --- a/tests/blocksuite/snapshots/attachment.spec.ts/should-undo-redo-works-for-attachment-1.json +++ b/tests/blocksuite/snapshots/attachment.spec.ts/should-undo-redo-works-for-attachment-1.json @@ -9,6 +9,7 @@ "id": "3", "props": { "embed": false, + "footnoteIdentifier": null, "index": "a0", "lockedBySelf": false, "name": "test-card-1.png", diff --git a/tests/blocksuite/snapshots/attachment.spec.ts/should-undo-redo-works-for-attachment-3.json b/tests/blocksuite/snapshots/attachment.spec.ts/should-undo-redo-works-for-attachment-3.json index 71b75acb65..6d4cb17925 100644 --- a/tests/blocksuite/snapshots/attachment.spec.ts/should-undo-redo-works-for-attachment-3.json +++ b/tests/blocksuite/snapshots/attachment.spec.ts/should-undo-redo-works-for-attachment-3.json @@ -9,6 +9,7 @@ "id": "3", "props": { "embed": false, + "footnoteIdentifier": null, "index": "a0", "lockedBySelf": false, "name": "test-card-1.png", diff --git a/tests/blocksuite/snapshots/attachment.spec.ts/support-dragging-attachment-block-directly-1.json b/tests/blocksuite/snapshots/attachment.spec.ts/support-dragging-attachment-block-directly-1.json index 71b75acb65..6d4cb17925 100644 --- a/tests/blocksuite/snapshots/attachment.spec.ts/support-dragging-attachment-block-directly-1.json +++ b/tests/blocksuite/snapshots/attachment.spec.ts/support-dragging-attachment-block-directly-1.json @@ -9,6 +9,7 @@ "id": "3", "props": { "embed": false, + "footnoteIdentifier": null, "index": "a0", "lockedBySelf": false, "name": "test-card-1.png", diff --git a/tests/blocksuite/snapshots/attachment.spec.ts/support-dragging-attachment-block-directly-2.json b/tests/blocksuite/snapshots/attachment.spec.ts/support-dragging-attachment-block-directly-2.json index 3a3423ff58..de9cffbe41 100644 --- a/tests/blocksuite/snapshots/attachment.spec.ts/support-dragging-attachment-block-directly-2.json +++ b/tests/blocksuite/snapshots/attachment.spec.ts/support-dragging-attachment-block-directly-2.json @@ -9,6 +9,7 @@ "id": "3", "props": { "embed": false, + "footnoteIdentifier": null, "index": "a0", "lockedBySelf": false, "name": "test-card-1.png", diff --git a/tests/blocksuite/snapshots/attachment.spec.ts/support-dragging-attachment-block-directly-3.json b/tests/blocksuite/snapshots/attachment.spec.ts/support-dragging-attachment-block-directly-3.json index 8be3c0b234..f8cc82b932 100644 --- a/tests/blocksuite/snapshots/attachment.spec.ts/support-dragging-attachment-block-directly-3.json +++ b/tests/blocksuite/snapshots/attachment.spec.ts/support-dragging-attachment-block-directly-3.json @@ -49,6 +49,7 @@ "id": "3", "props": { "embed": false, + "footnoteIdentifier": null, "index": "a0", "lockedBySelf": false, "name": "test-card-1.png", diff --git a/tests/blocksuite/snapshots/bookmark.spec.ts/copy-bookmark-url-by-copy-button-final.json b/tests/blocksuite/snapshots/bookmark.spec.ts/copy-bookmark-url-by-copy-button-final.json index 136bd94760..be05c74bb5 100644 --- a/tests/blocksuite/snapshots/bookmark.spec.ts/copy-bookmark-url-by-copy-button-final.json +++ b/tests/blocksuite/snapshots/bookmark.spec.ts/copy-bookmark-url-by-copy-button-final.json @@ -45,6 +45,7 @@ "url": "http://localhost", "caption": null, "description": null, + "footnoteIdentifier": null, "icon": null, "image": null, "title": null, diff --git a/tests/blocksuite/snapshots/bookmark.spec.ts/copy-url-to-create-bookmark-in-edgeless-mode-final.json b/tests/blocksuite/snapshots/bookmark.spec.ts/copy-url-to-create-bookmark-in-edgeless-mode-final.json index 33c96a66e9..749a8b5c0b 100644 --- a/tests/blocksuite/snapshots/bookmark.spec.ts/copy-url-to-create-bookmark-in-edgeless-mode-final.json +++ b/tests/blocksuite/snapshots/bookmark.spec.ts/copy-url-to-create-bookmark-in-edgeless-mode-final.json @@ -42,6 +42,7 @@ "props": { "caption": null, "description": null, + "footnoteIdentifier": null, "icon": null, "image": null, "index": "a0", diff --git a/tests/blocksuite/snapshots/bookmark.spec.ts/copy-url-to-create-bookmark-in-page-mode-final.json b/tests/blocksuite/snapshots/bookmark.spec.ts/copy-url-to-create-bookmark-in-page-mode-final.json index f0fde12a51..fbdef73f4d 100644 --- a/tests/blocksuite/snapshots/bookmark.spec.ts/copy-url-to-create-bookmark-in-page-mode-final.json +++ b/tests/blocksuite/snapshots/bookmark.spec.ts/copy-url-to-create-bookmark-in-page-mode-final.json @@ -30,6 +30,7 @@ "props": { "caption": null, "description": null, + "footnoteIdentifier": null, "icon": null, "image": null, "index": "a0", diff --git a/tests/blocksuite/snapshots/bookmark.spec.ts/create-bookmark-by-slash-menu-final.json b/tests/blocksuite/snapshots/bookmark.spec.ts/create-bookmark-by-slash-menu-final.json index 08119762bf..6897e50d17 100644 --- a/tests/blocksuite/snapshots/bookmark.spec.ts/create-bookmark-by-slash-menu-final.json +++ b/tests/blocksuite/snapshots/bookmark.spec.ts/create-bookmark-by-slash-menu-final.json @@ -10,6 +10,7 @@ "props": { "caption": null, "description": null, + "footnoteIdentifier": null, "icon": null, "image": null, "index": "a0", diff --git a/tests/blocksuite/snapshots/bookmark.spec.ts/horizontal-figma.json b/tests/blocksuite/snapshots/bookmark.spec.ts/horizontal-figma.json index cd0d1e0124..b1ae3bb8b8 100644 --- a/tests/blocksuite/snapshots/bookmark.spec.ts/horizontal-figma.json +++ b/tests/blocksuite/snapshots/bookmark.spec.ts/horizontal-figma.json @@ -51,7 +51,8 @@ "index": "a0", "xywh": "[0,0,0,0]", "lockedBySelf": false, - "rotate": 0 + "rotate": 0, + "footnoteIdentifier": null }, "children": [] } diff --git a/tests/blocksuite/snapshots/bookmark.spec.ts/horizontal-youtube.json b/tests/blocksuite/snapshots/bookmark.spec.ts/horizontal-youtube.json index d1d3f3c5b6..c1e31833d7 100644 --- a/tests/blocksuite/snapshots/bookmark.spec.ts/horizontal-youtube.json +++ b/tests/blocksuite/snapshots/bookmark.spec.ts/horizontal-youtube.json @@ -51,7 +51,8 @@ "index": "a0", "xywh": "[0,0,0,0]", "lockedBySelf": false, - "rotate": 0 + "rotate": 0, + "footnoteIdentifier": null }, "children": [] } diff --git a/tests/blocksuite/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-after-add-paragraph.json b/tests/blocksuite/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-after-add-paragraph.json index c40930225c..2a1f54c3e2 100644 --- a/tests/blocksuite/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-after-add-paragraph.json +++ b/tests/blocksuite/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-after-add-paragraph.json @@ -44,6 +44,7 @@ "url": "http://localhost", "caption": null, "description": null, + "footnoteIdentifier": null, "icon": null, "image": null, "title": null, diff --git a/tests/blocksuite/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-after-drag.json b/tests/blocksuite/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-after-drag.json index 0aca45a174..15122a9ed0 100644 --- a/tests/blocksuite/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-after-drag.json +++ b/tests/blocksuite/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-after-drag.json @@ -82,6 +82,7 @@ "url": "http://localhost", "caption": null, "description": null, + "footnoteIdentifier": null, "icon": null, "image": null, "title": null, diff --git a/tests/blocksuite/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-init.json b/tests/blocksuite/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-init.json index d292befbe1..351cba88f5 100644 --- a/tests/blocksuite/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-init.json +++ b/tests/blocksuite/snapshots/bookmark.spec.ts/support-dragging-bookmark-block-directly-init.json @@ -44,6 +44,7 @@ "url": "http://localhost", "caption": null, "description": null, + "footnoteIdentifier": null, "icon": null, "image": null, "title": null, diff --git a/tests/blocksuite/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-drag.json b/tests/blocksuite/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-drag.json index 04c67ff074..87476843a0 100644 --- a/tests/blocksuite/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-drag.json +++ b/tests/blocksuite/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-drag.json @@ -11,6 +11,7 @@ "id": "11", "props": { "caption": null, + "footnoteIdentifier": null, "index": "a0", "lockedBySelf": false, "pageId": "6", diff --git a/tests/blocksuite/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-link-to-card-min-width.json b/tests/blocksuite/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-link-to-card-min-width.json index 12d349c192..d2d125beb4 100644 --- a/tests/blocksuite/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-link-to-card-min-width.json +++ b/tests/blocksuite/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-link-to-card-min-width.json @@ -11,6 +11,7 @@ "id": "11", "props": { "caption": null, + "footnoteIdentifier": null, "index": "a0", "lockedBySelf": false, "pageId": "6", diff --git a/tests/blocksuite/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-link-to-card.json b/tests/blocksuite/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-link-to-card.json index df95c1370b..059a76109a 100644 --- a/tests/blocksuite/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-link-to-card.json +++ b/tests/blocksuite/snapshots/edgeless/edgeless-text.spec.ts/min-width-limit-for-embed-block-link-to-card.json @@ -11,6 +11,7 @@ "id": "11", "props": { "caption": null, + "footnoteIdentifier": null, "index": "a0", "lockedBySelf": false, "pageId": "6", diff --git a/tests/blocksuite/snapshots/format-bar.spec.ts/create-linked-doc-from-block-selection-with-format-bar.json b/tests/blocksuite/snapshots/format-bar.spec.ts/create-linked-doc-from-block-selection-with-format-bar.json index a44193b38d..d0c7d8b77a 100644 --- a/tests/blocksuite/snapshots/format-bar.spec.ts/create-linked-doc-from-block-selection-with-format-bar.json +++ b/tests/blocksuite/snapshots/format-bar.spec.ts/create-linked-doc-from-block-selection-with-format-bar.json @@ -9,6 +9,7 @@ "id": "12", "props": { "caption": null, + "footnoteIdentifier": null, "index": "a0", "lockedBySelf": false, "pageId": "5",