feat(server): parse ydoc to ai editable markdown format (#12846)

close AI-213









#### PR Dependency Tree


* **PR #12846** 👈
  * **PR #12811**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## 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.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fengmk2
2025-06-19 11:27:04 +08:00
committed by GitHub
parent 380f40ebed
commit d80bfac1d2
9 changed files with 649 additions and 6 deletions
@@ -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 = `<!-- block_id=${id} flavour=${flavour} placeholder -->\n`;
result.children = [];
} else {
result.content = `<!-- block_id=${id} flavour=${flavour} -->\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: '',
@@ -149,4 +149,5 @@ export interface ParserContext {
buildBlobUrl: (blobId: string) => string;
buildDocUrl: (docId: string) => string;
renderDocTitle?: (docId: string) => string;
aiEditable?: boolean;
}