diff --git a/packages/blocks/src/__tests__/adapters/html.unit.spec.ts b/packages/blocks/src/__tests__/adapters/html.unit.spec.ts
index 00f44bdc68e44..f56bae76bf58a 100644
--- a/packages/blocks/src/__tests__/adapters/html.unit.spec.ts
+++ b/packages/blocks/src/__tests__/adapters/html.unit.spec.ts
@@ -571,7 +571,112 @@ describe('snapshot to html', () => {
],
};
const html = template(
- `
`
+ ``
+ );
+
+ const htmlAdapter = new HtmlAdapter(createJob());
+ const target = await htmlAdapter.fromBlockSnapshot({
+ snapshot: blockSnapshot,
+ });
+ expect(target.file).toBe(html);
+ });
+
+ test('different list', async () => {
+ const blockSnapshot: BlockSnapshot = {
+ type: 'block',
+ id: 'block:vu6SK6WJpW',
+ flavour: 'affine:page',
+ props: {
+ title: {
+ '$blocksuite:internal:text$': true,
+ delta: [],
+ },
+ },
+ children: [
+ {
+ type: 'block',
+ id: 'block:Tk4gSPocAt',
+ flavour: 'affine:surface',
+ props: {
+ elements: {},
+ },
+ children: [],
+ },
+ {
+ type: 'block',
+ id: 'block:WfnS5ZDCJT',
+ flavour: 'affine:note',
+ props: {
+ xywh: '[0,0,800,95]',
+ background: DEFAULT_NOTE_BACKGROUND_COLOR,
+ index: 'a0',
+ hidden: false,
+ displayMode: NoteDisplayMode.DocAndEdgeless,
+ },
+ children: [
+ {
+ type: 'block',
+ id: 'block:imiLDMKSkx',
+ flavour: 'affine:list',
+ props: {
+ type: 'bulleted',
+ text: {
+ '$blocksuite:internal:text$': true,
+ delta: [
+ {
+ insert: 'aaa',
+ },
+ ],
+ },
+ checked: false,
+ collapsed: false,
+ },
+ children: [],
+ },
+ {
+ type: 'block',
+ id: 'block:imiLDMKSkx',
+ flavour: 'affine:list',
+ props: {
+ type: 'todo',
+ text: {
+ '$blocksuite:internal:text$': true,
+ delta: [
+ {
+ insert: 'bbb',
+ },
+ ],
+ },
+ checked: false,
+ collapsed: false,
+ },
+ children: [],
+ },
+ {
+ type: 'block',
+ id: 'block:imiLDMKSkx',
+ flavour: 'affine:list',
+ props: {
+ type: 'bulleted',
+ text: {
+ '$blocksuite:internal:text$': true,
+ delta: [
+ {
+ insert: 'ccc',
+ },
+ ],
+ },
+ checked: false,
+ collapsed: false,
+ },
+ children: [],
+ },
+ ],
+ },
+ ],
+ };
+ const html = template(
+ ``
);
const htmlAdapter = new HtmlAdapter(createJob());
diff --git a/packages/blocks/src/__tests__/adapters/markdown.unit.spec.ts b/packages/blocks/src/__tests__/adapters/markdown.unit.spec.ts
index bf7d85e8c3e91..adf6d36fd785d 100644
--- a/packages/blocks/src/__tests__/adapters/markdown.unit.spec.ts
+++ b/packages/blocks/src/__tests__/adapters/markdown.unit.spec.ts
@@ -721,6 +721,170 @@ hhh
expect(target.file).toEqual(markdown);
});
+ test('different list', async () => {
+ const blockSnapshot: BlockSnapshot = {
+ type: 'block',
+ id: 'block:m5hvdXHXS2',
+ flavour: 'affine:page',
+ version: 2,
+ props: {
+ title: {
+ '$blocksuite:internal:text$': true,
+ delta: [],
+ },
+ },
+ children: [
+ {
+ type: 'block',
+ id: 'block:Y4J-oO9h9d',
+ flavour: 'affine:surface',
+ version: 5,
+ props: {
+ elements: {},
+ },
+ children: [],
+ },
+ {
+ type: 'block',
+ id: 'block:1Ll22zT992',
+ flavour: 'affine:note',
+ version: 1,
+ props: {
+ xywh: '[0,0,800,95]',
+ background: DEFAULT_NOTE_BACKGROUND_COLOR,
+ index: 'a0',
+ hidden: false,
+ displayMode: 'both',
+ edgeless: {
+ style: {
+ borderRadius: 8,
+ borderSize: 4,
+ borderStyle: 'solid',
+ shadowType: '--affine-note-shadow-box',
+ },
+ },
+ },
+ children: [
+ {
+ type: 'block',
+ id: 'block:Fd0ZCYB7a4',
+ flavour: 'affine:list',
+ version: 1,
+ props: {
+ type: 'numbered',
+ text: {
+ '$blocksuite:internal:text$': true,
+ delta: [
+ {
+ insert: 'aaa',
+ },
+ ],
+ },
+ checked: false,
+ collapsed: false,
+ },
+ children: [
+ {
+ type: 'block',
+ id: 'block:8-GeKDc06x',
+ flavour: 'affine:list',
+ version: 1,
+ props: {
+ type: 'numbered',
+ text: {
+ '$blocksuite:internal:text$': true,
+ delta: [
+ {
+ insert: 'bbb',
+ },
+ ],
+ },
+ checked: false,
+ collapsed: false,
+ },
+ children: [],
+ },
+ {
+ type: 'block',
+ id: 'block:f0c-9xKaEL',
+ flavour: 'affine:list',
+ version: 1,
+ props: {
+ type: 'bulleted',
+ text: {
+ '$blocksuite:internal:text$': true,
+ delta: [
+ {
+ insert: 'ccc',
+ },
+ ],
+ },
+ checked: false,
+ collapsed: false,
+ },
+ children: [],
+ },
+ {
+ type: 'block',
+ id: 'block:f0c-9xKaEL',
+ flavour: 'affine:list',
+ version: 1,
+ props: {
+ type: 'numbered',
+ text: {
+ '$blocksuite:internal:text$': true,
+ delta: [
+ {
+ insert: 'ddd',
+ },
+ ],
+ },
+ checked: false,
+ collapsed: false,
+ },
+ children: [],
+ },
+ ],
+ },
+ {
+ type: 'block',
+ id: 'block:Fd0ZCYB7a5',
+ flavour: 'affine:list',
+ version: 1,
+ props: {
+ type: 'numbered',
+ text: {
+ '$blocksuite:internal:text$': true,
+ delta: [
+ {
+ insert: 'eee',
+ },
+ ],
+ },
+ checked: false,
+ collapsed: false,
+ },
+ children: [],
+ },
+ ],
+ },
+ ],
+ };
+
+ const markdown = `1. aaa
+ 1. bbb
+ * ccc
+ 1. ddd
+2. eee
+`;
+
+ const mdAdapter = new MarkdownAdapter(createJob());
+ const target = await mdAdapter.fromBlockSnapshot({
+ snapshot: blockSnapshot,
+ });
+ expect(target.file).toEqual(markdown);
+ });
+
test('code inline', async () => {
const blockSnapshot: BlockSnapshot = {
type: 'block',
diff --git a/packages/blocks/src/_common/adapters/html.ts b/packages/blocks/src/_common/adapters/html.ts
index 9185afd79e0f1..8c9b4e5ef6dda 100644
--- a/packages/blocks/src/_common/adapters/html.ts
+++ b/packages/blocks/src/_common/adapters/html.ts
@@ -1,6 +1,6 @@
import type { AffineTextAttributes } from '@blocksuite/affine-components/rich-text';
import type { DeltaInsert } from '@blocksuite/inline';
-import type { Root } from 'hast';
+import type { Element, Root } from 'hast';
import {
ColorScheme,
@@ -12,6 +12,8 @@ import { getFilenameFromContentDisposition } from '@blocksuite/affine-shared/uti
import { sha } from '@blocksuite/global/utils';
import {
type AssetsManager,
+ ASTWalker,
+ BaseAdapter,
type BlockSnapshot,
BlockSnapshotSchema,
type DocSnapshot,
@@ -27,7 +29,6 @@ import {
type ToBlockSnapshotPayload,
type ToDocSnapshotPayload,
} from '@blocksuite/store';
-import { ASTWalker, BaseAdapter } from '@blocksuite/store';
import { collapseWhiteSpace } from 'collapse-white-space';
import rehypeParse from 'rehype-parse';
import rehypeStringify from 'rehype-stringify';
@@ -44,7 +45,7 @@ import {
hastQuerySelector,
type HtmlAST,
} from './hast.js';
-import { fetchable, fetchImage, mergeDeltas } from './utils.js';
+import { fetchable, fetchImage, isNullish, mergeDeltas } from './utils.js';
export type Html = string;
@@ -685,6 +686,7 @@ export class HtmlAdapter extends BaseAdapter {
const text = (o.node.props.text ?? { delta: [] }) as {
delta: DeltaInsert[];
};
+ const currentTNode = context.currentNode();
switch (o.node.flavour) {
case 'affine:page': {
context
@@ -935,32 +937,6 @@ export class HtmlAdapter extends BaseAdapter {
break;
}
case 'affine:list': {
- context
- .openNode(
- {
- type: 'element',
- tagName: 'div',
- properties: {
- className: ['affine-list-block-container'],
- },
- children: [],
- },
- 'children'
- )
- .openNode(
- {
- type: 'element',
- tagName: o.node.props.type === 'numbered' ? 'ol' : 'ul',
- properties: {
- style:
- o.node.props.type === 'todo'
- ? 'list-style-type: none;'
- : '',
- },
- children: [],
- },
- 'children'
- );
const liChildren = this._deltaToHast(text.delta);
if (o.node.props.type === 'todo') {
liChildren.unshift({
@@ -974,36 +950,62 @@ export class HtmlAdapter extends BaseAdapter {
{
type: 'element',
tagName: 'label',
- properties: {},
+ properties: {
+ style: 'margin-right: 3px;',
+ },
children: [],
},
],
});
}
- context
- .openNode(
- {
- type: 'element',
- tagName: 'li',
- properties: {},
- children: liChildren,
- },
- 'children'
- )
- .closeNode()
- .closeNode()
- .openNode(
+ // check if the list is of the same type
+ if (
+ context.getNodeContext('affine:list:parent') === o.parent &&
+ currentTNode.type === 'element' &&
+ currentTNode.tagName ===
+ (o.node.props.type === 'numbered' ? 'ol' : 'ul') &&
+ !(
+ Array.isArray(currentTNode.properties.className) &&
+ currentTNode.properties.className.includes('todo-list')
+ ) ===
+ isNullish(
+ o.node.props.type === 'todo'
+ ? (o.node.props.checked as boolean)
+ : undefined
+ )
+ ) {
+ // if true, add the list item to the list
+ } else {
+ // if false, create a new list
+ context.openNode(
{
type: 'element',
- tagName: 'div',
+ tagName: o.node.props.type === 'numbered' ? 'ol' : 'ul',
properties: {
- className: ['affine-block-children-container'],
- style: 'padding-left: 26px;',
+ style:
+ o.node.props.type === 'todo'
+ ? 'list-style-type: none; padding-inline-start: 18px;'
+ : null,
+ className: [o.node.props.type + '-list'],
},
children: [],
},
'children'
);
+ context.setNodeContext('affine:list:parent', o.parent);
+ }
+
+ context.openNode(
+ {
+ type: 'element',
+ tagName: 'li',
+ properties: {
+ className: ['affine-list-block-container'],
+ },
+ children: liChildren,
+ },
+ 'children'
+ );
break;
}
case 'affine:divider': {
@@ -1083,7 +1085,34 @@ export class HtmlAdapter extends BaseAdapter {
break;
}
case 'affine:list': {
- context.closeNode().closeNode();
+ const currentTNode = context.currentNode() as Element;
+ const previousTNode = context.previousNode() as Element;
+ if (
+ context.getPreviousNodeContext('affine:list:parent') === o.parent &&
+ currentTNode.tagName === 'li' &&
+ previousTNode.tagName ===
+ (o.node.props.type === 'numbered' ? 'ol' : 'ul') &&
+ !(
+ Array.isArray(previousTNode.properties.className) &&
+ previousTNode.properties.className.includes('todo-list')
+ ) ===
+ isNullish(
+ o.node.props.type === 'todo'
+ ? (o.node.props.checked as boolean)
+ : undefined
+ )
+ ) {
+ context.closeNode();
+ if (
+ o.next?.flavour !== 'affine:list' ||
+ o.next.props.type !== o.node.props.type
+ ) {
+ // If the next node is not a list or different type of list, close the list
+ context.closeNode();
+ }
+ } else {
+ context.closeNode().closeNode();
+ }
break;
}
}
diff --git a/packages/blocks/src/_common/adapters/markdown.ts b/packages/blocks/src/_common/adapters/markdown.ts
index f6392eb4dadeb..aa9fa95e66307 100644
--- a/packages/blocks/src/_common/adapters/markdown.ts
+++ b/packages/blocks/src/_common/adapters/markdown.ts
@@ -520,8 +520,6 @@ export class MarkdownAdapter extends BaseAdapter {
}
case 'affine:list': {
// check if the list is of the same type
- // if true, add the list item to the list
- // if false, create a new list
if (
context.getNodeContext('affine:list:parent') === o.parent &&
currentTNode.type === 'list' &&
@@ -533,28 +531,9 @@ export class MarkdownAdapter extends BaseAdapter {
: undefined
)
) {
- context
- .openNode(
- {
- type: 'listItem',
- checked:
- o.node.props.type === 'todo'
- ? (o.node.props.checked as boolean)
- : undefined,
- spread: false,
- children: [],
- },
- 'children'
- )
- .openNode(
- {
- type: 'paragraph',
- children: this._deltaToMdAST(text.delta),
- },
- 'children'
- )
- .closeNode();
+ // if true, add the list item to the list
} else {
+ // if false, create a new list
context
.openNode(
{
@@ -565,28 +544,29 @@ export class MarkdownAdapter extends BaseAdapter {
},
'children'
)
- .setNodeContext('affine:list:parent', o.parent)
- .openNode(
- {
- type: 'listItem',
- checked:
- o.node.props.type === 'todo'
- ? (o.node.props.checked as boolean)
- : undefined,
- spread: false,
- children: [],
- },
- 'children'
- )
- .openNode(
- {
- type: 'paragraph',
- children: this._deltaToMdAST(text.delta),
- },
- 'children'
- )
- .closeNode();
+ .setNodeContext('affine:list:parent', o.parent);
}
+ context
+ .openNode(
+ {
+ type: 'listItem',
+ checked:
+ o.node.props.type === 'todo'
+ ? (o.node.props.checked as boolean)
+ : undefined,
+ spread: false,
+ children: [],
+ },
+ 'children'
+ )
+ .openNode(
+ {
+ type: 'paragraph',
+ children: this._deltaToMdAST(text.delta),
+ },
+ 'children'
+ )
+ .closeNode();
break;
}
case 'affine:divider': {
@@ -877,8 +857,11 @@ export class MarkdownAdapter extends BaseAdapter {
)
) {
context.closeNode();
- if (o.next?.flavour !== 'affine:list') {
- // If the next node is not a list, close the list
+ if (
+ o.next?.flavour !== 'affine:list' ||
+ o.next.props.type !== o.node.props.type
+ ) {
+ // If the next node is not a list or different type of list, close the list
context.closeNode();
}
} else {
diff --git a/packages/framework/store/src/adapter/base.ts b/packages/framework/store/src/adapter/base.ts
index c279bcfd52795..7b1a81e55a855 100644
--- a/packages/framework/store/src/adapter/base.ts
+++ b/packages/framework/store/src/adapter/base.ts
@@ -1,4 +1,4 @@
-import { assertEquals } from '@blocksuite/global/utils';
+import { BlockSuiteError } from '@blocksuite/global/exceptions';
import type { Doc } from '../store/index.js';
import type { AssetsManager } from '../transformer/assets.js';
@@ -48,6 +48,20 @@ export type ToSliceSnapshotPayload = {
assets?: AssetsManager;
};
+export function wrapFakeNote(snapshot: SliceSnapshot) {
+ if (snapshot.content[0]?.flavour !== 'affine:note') {
+ snapshot.content = [
+ {
+ type: 'block',
+ id: '',
+ flavour: 'affine:note',
+ props: {},
+ children: snapshot.content,
+ },
+ ];
+ }
+}
+
export abstract class BaseAdapter {
job: Job;
@@ -105,6 +119,7 @@ export abstract class BaseAdapter {
try {
const sliceSnapshot = await this.job.sliceToSnapshot(slice);
if (!sliceSnapshot) return;
+ wrapFakeNote(sliceSnapshot);
return await this.fromSliceSnapshot({
snapshot: sliceSnapshot,
assets: this.job.assetsManager,
@@ -280,7 +295,10 @@ export class ASTWalker {
walk = async (oNode: ONode, tNode: TNode) => {
this.context.openNode(tNode);
await this._visit({ node: oNode, parent: null, prop: null, index: null });
- assertEquals(this.context.stack.length, 1, 'There are unclosed nodes');
+ if (this.context.stack.length !== 1) {
+ console.error(this.context.stack.map(n => n.node));
+ throw new BlockSuiteError(1, 'There are unclosed nodes');
+ }
return this.context.currentNode();
};
diff --git a/tests/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard.json b/tests/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard.json
index 9c082a376a85d..130047281934d 100644
--- a/tests/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard.json
+++ b/tests/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard.json
@@ -1,24 +1,13 @@
[
{
"type": "block",
- "id": "2",
- "flavour": "affine:paragraph",
- "version": 1,
- "props": {
- "type": "text",
- "text": {
- "$blocksuite:internal:text$": true,
- "delta": [
- {
- "insert": "bc"
- }
- ]
- }
- },
+ "id": "",
+ "flavour": "affine:note",
+ "props": {},
"children": [
{
"type": "block",
- "id": "3",
+ "id": "2",
"flavour": "affine:paragraph",
"version": 1,
"props": {
@@ -27,12 +16,31 @@
"$blocksuite:internal:text$": true,
"delta": [
{
- "insert": "d"
+ "insert": "bc"
}
]
}
},
- "children": []
+ "children": [
+ {
+ "type": "block",
+ "id": "3",
+ "flavour": "affine:paragraph",
+ "version": 1,
+ "props": {
+ "type": "text",
+ "text": {
+ "$blocksuite:internal:text$": true,
+ "delta": [
+ {
+ "insert": "d"
+ }
+ ]
+ }
+ },
+ "children": []
+ }
+ ]
}
]
}
diff --git a/tests/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard2.json b/tests/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard2.json
index 9bc963987482e..eff50e7ad92dc 100644
--- a/tests/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard2.json
+++ b/tests/snapshots/clipboard/clipboard.spec.ts/clipboard-copy-nested-items-clipboard2.json
@@ -1,38 +1,46 @@
[
{
"type": "block",
- "id": "4",
- "flavour": "affine:paragraph",
- "version": 1,
- "props": {
- "type": "text",
- "text": {
- "$blocksuite:internal:text$": true,
- "delta": [
- {
- "insert": "hi"
+ "id": "",
+ "flavour": "affine:note",
+ "props": {},
+ "children": [
+ {
+ "type": "block",
+ "id": "4",
+ "flavour": "affine:paragraph",
+ "version": 1,
+ "props": {
+ "type": "text",
+ "text": {
+ "$blocksuite:internal:text$": true,
+ "delta": [
+ {
+ "insert": "hi"
+ }
+ ]
}
- ]
- }
- },
- "children": []
- },
- {
- "type": "block",
- "id": "5",
- "flavour": "affine:paragraph",
- "version": 1,
- "props": {
- "type": "text",
- "text": {
- "$blocksuite:internal:text$": true,
- "delta": [
- {
- "insert": "j"
+ },
+ "children": []
+ },
+ {
+ "type": "block",
+ "id": "5",
+ "flavour": "affine:paragraph",
+ "version": 1,
+ "props": {
+ "type": "text",
+ "text": {
+ "$blocksuite:internal:text$": true,
+ "delta": [
+ {
+ "insert": "j"
+ }
+ ]
}
- ]
+ },
+ "children": []
}
- },
- "children": []
+ ]
}
]
\ No newline at end of file
diff --git a/tests/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.html b/tests/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.html
index 7a74fc0d0ac3a..b74b581cefa8d 100644
--- a/tests/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.html
+++ b/tests/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.html
@@ -1,20 +1,11 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.json b/tests/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.json
index 901af3391b2b1..43b78e52be343 100644
--- a/tests/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.json
+++ b/tests/snapshots/clipboard/list.spec.ts/copy-a-nested-list-by-clicking-button-the-clipboard-data-should-be-complete-clipboard.json
@@ -1,27 +1,13 @@
[
{
"type": "block",
- "id": "3",
- "flavour": "affine:list",
- "version": 1,
- "props": {
- "type": "bulleted",
- "text": {
- "$blocksuite:internal:text$": true,
- "delta": [
- {
- "insert": "aaa"
- }
- ]
- },
- "checked": false,
- "collapsed": false,
- "order": null
- },
+ "id": "",
+ "flavour": "affine:note",
+ "props": {},
"children": [
{
"type": "block",
- "id": "4",
+ "id": "3",
"flavour": "affine:list",
"version": 1,
"props": {
@@ -30,7 +16,7 @@
"$blocksuite:internal:text$": true,
"delta": [
{
- "insert": "bbb"
+ "insert": "aaa"
}
]
},
@@ -41,7 +27,7 @@
"children": [
{
"type": "block",
- "id": "5",
+ "id": "4",
"flavour": "affine:list",
"version": 1,
"props": {
@@ -50,7 +36,7 @@
"$blocksuite:internal:text$": true,
"delta": [
{
- "insert": "ccc"
+ "insert": "bbb"
}
]
},
@@ -58,7 +44,29 @@
"collapsed": false,
"order": null
},
- "children": []
+ "children": [
+ {
+ "type": "block",
+ "id": "5",
+ "flavour": "affine:list",
+ "version": 1,
+ "props": {
+ "type": "bulleted",
+ "text": {
+ "$blocksuite:internal:text$": true,
+ "delta": [
+ {
+ "insert": "ccc"
+ }
+ ]
+ },
+ "checked": false,
+ "collapsed": false,
+ "order": null
+ },
+ "children": []
+ }
+ ]
}
]
}