From d80bfac1d24699f3e02b11fc4f2e1a2e3f10b5da Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Thu, 19 Jun 2025 11:27:04 +0800 Subject: [PATCH] feat(server): parse ydoc to ai editable markdown format (#12846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit close AI-213 #### PR Dependency Tree * **PR #12846** πŸ‘ˆ * **PR #12811** This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) ## Summary by CodeRabbit - **New Features** - Introduced support for AI-editable blocks in document parsing, allowing blocks to include metadata for AI-based editing. - Added rendering for todo list items with markdown checkbox syntax. - Unsupported block types are now marked with placeholders in the parsed output. - **Tests** - Added new test cases and snapshots to verify parsing behavior with AI-editable content enabled. --- .../__snapshots__/blocksute.spec.ts.md | 119 +++++ .../__snapshots__/blocksute.spec.ts.snap | Bin 12701 -> 13076 bytes .../core/utils/__tests__/blocksute.spec.ts | 11 + .../server/src/core/utils/blocksuite.ts | 4 +- .../test-doc-with-ai-editable.snapshot.bin | Bin 0 -> 62048 bytes .../__snapshots__/reader.spec.ts.snap | 444 ++++++++++++++++++ .../common/reader/__tests__/reader.spec.ts | 42 ++ .../common/reader/src/doc-parser/parser.ts | 34 +- .../common/reader/src/doc-parser/types.ts | 1 + 9 files changed, 649 insertions(+), 6 deletions(-) create mode 100644 packages/common/reader/__tests__/__fixtures__/test-doc-with-ai-editable.snapshot.bin diff --git a/packages/backend/server/src/core/utils/__tests__/__snapshots__/blocksute.spec.ts.md b/packages/backend/server/src/core/utils/__tests__/__snapshots__/blocksute.spec.ts.md index 258c0c252e..de7fb65f3e 100644 --- a/packages/backend/server/src/core/utils/__tests__/__snapshots__/blocksute.spec.ts.md +++ b/packages/backend/server/src/core/utils/__tests__/__snapshots__/blocksute.spec.ts.md @@ -1467,3 +1467,122 @@ Generated by [AVA](https://avajs.dev). `, title: 'Write, Draw, Plan all at Once.', } + +## can parse doc to markdown from doc snapshot with ai editable + +> Snapshot 1 + + { + markdown: `␊ + AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + # You own your data, with no compromises␊ + ␊ + ␊ + ␊ + ## Local-first & Real-time collaborative␊ + ␊ + ␊ + ␊ + We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊ + ␊ + ␊ + ␊ + AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ### Blocks that assemble your next docs, tasks kanban or whiteboard␊ + ␊ + ␊ + ␊ + There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊ + ␊ + ␊ + ␊ + We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊ + ␊ + ␊ + ␊ + If you want to learn more about the product design of AFFiNE, here goes the concepts:␊ + ␊ + ␊ + ␊ + To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊ + ␊ + ␊ + ␊ + ## A true canvas for blocks in any form␊ + ␊ + ␊ + ␊ + [Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + "We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊ + ␊ + ␊ + ␊ + * Quip & Notion with their great concept of "everything is a block"␊ + ␊ + ␊ + ␊ + * Trello with their Kanban␊ + ␊ + ␊ + ␊ + * Airtable & Miro with their no-code programable datasheets␊ + ␊ + ␊ + ␊ + * Miro & Whimiscal with their edgeless visual whiteboard␊ + ␊ + ␊ + ␊ + * Remnote & Capacities with their object-based tag system␊ + ␊ + ␊ + ␊ + For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊ + ␊ + ␊ + ␊ + ## Self Host␊ + ␊ + ␊ + ␊ + Self host AFFiNE␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ## Affine Development␊ + ␊ + ␊ + ␊ + For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + `, + title: 'Write, Draw, Plan all at Once.', + } diff --git a/packages/backend/server/src/core/utils/__tests__/__snapshots__/blocksute.spec.ts.snap b/packages/backend/server/src/core/utils/__tests__/__snapshots__/blocksute.spec.ts.snap index 7c77db4c2a96a57b327c050c3bcdef90315051af..4d486998690864e8033bebc0760fc7c5a222dafc 100644 GIT binary patch literal 13076 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 diff --git a/packages/backend/server/src/core/utils/__tests__/blocksute.spec.ts b/packages/backend/server/src/core/utils/__tests__/blocksute.spec.ts index 90e2a0947f..e337a07ce5 100644 --- a/packages/backend/server/src/core/utils/__tests__/blocksute.spec.ts +++ b/packages/backend/server/src/core/utils/__tests__/blocksute.spec.ts @@ -99,3 +99,14 @@ test('can parse doc to markdown from doc snapshot', async t => { t.snapshot(result); }); + +test('can parse doc to markdown from doc snapshot with ai editable', async t => { + const result = parseDocToMarkdownFromDocSnapshot( + workspace.id, + docSnapshot.id, + docSnapshot.blob, + true + ); + + t.snapshot(result); +}); diff --git a/packages/backend/server/src/core/utils/blocksuite.ts b/packages/backend/server/src/core/utils/blocksuite.ts index ff3f64597c..d522660dac 100644 --- a/packages/backend/server/src/core/utils/blocksuite.ts +++ b/packages/backend/server/src/core/utils/blocksuite.ts @@ -201,7 +201,8 @@ export async function readAllBlocksFromDocSnapshot( export function parseDocToMarkdownFromDocSnapshot( workspaceId: string, docId: string, - docSnapshot: Uint8Array + docSnapshot: Uint8Array, + aiEditable = false ) { const ydoc = new YDoc({ guid: docId, @@ -217,6 +218,7 @@ export function parseDocToMarkdownFromDocSnapshot( buildDocUrl: (docId: string) => { return `/workspace/${workspaceId}/${docId}`; }, + aiEditable, }); return { diff --git a/packages/common/reader/__tests__/__fixtures__/test-doc-with-ai-editable.snapshot.bin b/packages/common/reader/__tests__/__fixtures__/test-doc-with-ai-editable.snapshot.bin new file mode 100644 index 0000000000000000000000000000000000000000..96bedb2424f47975cabf2169330915ad679bd3cc GIT binary patch literal 62048 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 +AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro. + + + + + + + +# You own your data, with no compromises + + + +## Local-first & Real-time collaborative + + + +We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience. + + + +AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time. + + + + + + + +### Blocks that assemble your next docs, tasks kanban or whiteboard + + + +There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further. + + + +We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too. + + + +If you want to learn more about the product design of AFFiNE, here goes the concepts: + + + +To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools. + + + +## A true canvas for blocks in any form + + + +[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers. + + + + + + + +"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.: + + + +* Quip & Notion with their great concept of "everything is a block" + + + +* Trello with their Kanban + + + +* Airtable & Miro with their no-code programable datasheets + + + +* Miro & Whimiscal with their edgeless visual whiteboard + + + +* Remnote & Capacities with their object-based tag system + + + +For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap) + + + +## Self Host + + + +Self host AFFiNE + + + + + +## Affine Development + + + +For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start) + + + + + + +" +`; + +exports[`should parse page full doc work with ai editable 1`] = ` +" +# H1 text + + + +List all flavours in one document. + + + +## H2 ~ H6 + + + +### H3 + + + +#### H4 with emoji πŸ˜„ + + + +##### H5 + + + +###### H6 + + + +max is H6 + + + +## List + + + +* item 1 + + + +* item 2 + + + * sub item 1 + + + * sub item 2 + + + * super sub item 1 + + + * sub item 3 + + + +* item 3 + + + + + + + + + + + + + + + +sort list + + + +1. item 1 + + + +1. item 2 + + + +1. item 3 + + + 1. sub item 1 + + + 1. sub item 2 + + + 1. super item 1 + + + 1. super item 2 + + + 1. sub item 3 + + + +1. item 4 + + + + + + + + + + + +Table + + + +|c1|c2|c3|c4| +|---|---|---|---| +|v1|v2|v3|| +||||v4| +||v6||v5| + + + + + + + + + + + +Database + + + + + +Code + + + +\`\`\`javascript +console.log('hello world'); +\`\`\` + + + + + + + +Image + + + + +![-HjsQksaVuEaM0KeTEbBYDgWVOmoVzrAeVK0kn4Jfr0=](blob://-HjsQksaVuEaM0KeTEbBYDgWVOmoVzrAeVK0kn4Jfr0=) + + + + + + + +File + + + + +![IrsiJ9XonFXF8fw9tLQXbSsBbUYFfzLgoFpodeidQOU=](blob://IrsiJ9XonFXF8fw9tLQXbSsBbUYFfzLgoFpodeidQOU=) + + + + + + + +> foo bar quote text + + + + + + + + + + + + +--- + + + + + + + +TeX + + + + + + + + + + + +2025-06-18 13:15 + + + + + + + + + +Mind Map + + + + + + + + + + + +A Link + + + + +[null](doc://FmHFPAPzp51JjFP89aZ-b) + + + +Todo List + + + +- [ ] abc + + + +- [ ] edf + + + - [x] done1 + + + +- [ ] end + + + + + + + +~~delete text~~ + + + + + + + +**Bold text** + + + + + + + +Underline + + + + + + + +Youtube + + + + + + + + + + + + +## end + + + +this is end + + + + + + +" +`; + exports[`should read all doc ids from root doc snapshot work 1`] = ` [ "5nS9BSp3Px", diff --git a/packages/common/reader/__tests__/reader.spec.ts b/packages/common/reader/__tests__/reader.spec.ts index 5323915051..6ca3a81d46 100644 --- a/packages/common/reader/__tests__/reader.spec.ts +++ b/packages/common/reader/__tests__/reader.spec.ts @@ -17,6 +17,12 @@ const rootDocSnapshot = readFileSync( const docSnapshot = readFileSync( path.join(import.meta.dirname, './__fixtures__/test-doc.snapshot.bin') ); +const docSnapshotWithAiEditable = readFileSync( + path.join( + import.meta.dirname, + './__fixtures__/test-doc-with-ai-editable.snapshot.bin' + ) +); test('should read doc blocks work', async () => { const rootDoc = new YDoc({ @@ -118,3 +124,39 @@ test('should parse page doc work', () => { expect(result).toMatchSnapshot(); }); + +test('should parse page doc work with ai editable', () => { + const doc = new YDoc({ + guid: 'test-doc', + }); + applyUpdate(doc, docSnapshot); + + const result = parsePageDoc({ + workspaceId: 'test-space', + doc, + buildBlobUrl: id => `blob://${id}`, + buildDocUrl: id => `doc://${id}`, + renderDocTitle: id => `Doc Title ${id}`, + aiEditable: true, + }); + + expect(result.md).toMatchSnapshot(); +}); + +test('should parse page full doc work with ai editable', () => { + const doc = new YDoc({ + guid: 'test-doc', + }); + applyUpdate(doc, docSnapshotWithAiEditable); + + const result = parsePageDoc({ + workspaceId: 'test-space', + doc, + buildBlobUrl: id => `blob://${id}`, + buildDocUrl: id => `doc://${id}`, + renderDocTitle: id => `Doc Title ${id}`, + aiEditable: true, + }); + + expect(result.md).toMatchSnapshot(); +}); diff --git a/packages/common/reader/src/doc-parser/parser.ts b/packages/common/reader/src/doc-parser/parser.ts index d3e92cd380..3f36f779cb 100644 --- a/packages/common/reader/src/doc-parser/parser.ts +++ b/packages/common/reader/src/doc-parser/parser.ts @@ -34,7 +34,9 @@ export const parseBlockToMd = ( export function parseBlock( context: ParserContext, yBlock: YBlock | undefined, - yBlocks: YBlocks // all blocks + yBlocks: YBlocks, // all blocks + aiEditable = false, + blockLevel = 0 ): ParsedBlock | null { if (!yBlock) { return null; @@ -73,6 +75,8 @@ export function parseBlock( return result; } + let placeholder = false; + try { switch (flavour) { case 'affine:paragraph': { @@ -100,7 +104,12 @@ export function parseBlock( break; } case 'affine:list': { - result.content = (type === 'bulleted' ? '* ' : '1. ') + toMd() + '\n'; + let prefix = type === 'bulleted' ? '* ' : '1. '; + if (type === 'todo') { + const checked = yBlock.get('prop:checked') as boolean; + prefix = checked ? '- [x] ' : '- [ ] '; + } + result.content = prefix + toMd() + '\n'; break; } case 'affine:code': { @@ -218,7 +227,9 @@ export function parseBlock( const child = parseBlock( context, yBlocks.get(cid) as YBlock | undefined, - yBlocks + yBlocks, + aiEditable, + blockLevel + 1 ); if (!child) { return [cid, '']; @@ -375,6 +386,7 @@ export function parseBlock( } default: { // console.warn("Unknown or unsupported flavour", flavour); + placeholder = true; } } @@ -385,7 +397,9 @@ export function parseBlock( parseBlock( context, yBlocks.get(cid) as YBlock | undefined, - yBlocks + yBlocks, + aiEditable, + blockLevel + 1 ) ) .filter( @@ -397,6 +411,16 @@ export function parseBlock( } catch (e) { console.warn('Error converting block to md', e); } + + if (result.content && aiEditable && blockLevel === 2) { + // add a placeholder comment for the block level 2 + if (flavour === 'affine:database' || placeholder) { + result.content = `\n`; + result.children = []; + } else { + result.content = `\n${result.content}`; + } + } return result; } @@ -416,7 +440,7 @@ export const parsePageDoc = (ctx: ParserContext): ParsedDoc => { } else { const yPage = yBlocks.get(maybePageBlock[0]) as YBlock; const title = yPage.get('prop:title') as YText; - const rootBlock = parseBlock(ctx, yPage, yBlocks); + const rootBlock = parseBlock(ctx, yPage, yBlocks, ctx.aiEditable); if (!rootBlock) { return { title: '', diff --git a/packages/common/reader/src/doc-parser/types.ts b/packages/common/reader/src/doc-parser/types.ts index c88ce3e7bb..e95ba6b5fa 100644 --- a/packages/common/reader/src/doc-parser/types.ts +++ b/packages/common/reader/src/doc-parser/types.ts @@ -149,4 +149,5 @@ export interface ParserContext { buildBlobUrl: (blobId: string) => string; buildDocUrl: (docId: string) => string; renderDocTitle?: (docId: string) => string; + aiEditable?: boolean; }