mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +00:00
Compare commits
2 Commits
v0.23.2
...
feat/apply
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8024172569 | ||
|
|
b434b95548 |
@@ -23,7 +23,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"file-type": "^21.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"lit": "^3.2.0",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -42,6 +42,7 @@ import { computed, signal } from '@preact/signals-core';
|
||||
import { css, nothing, unsafeCSS } from 'lit';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { BlockQueryDataSource } from './data-source.js';
|
||||
import type { DataViewBlockModel } from './data-view-model.js';
|
||||
|
||||
@@ -303,9 +304,16 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
|
||||
},
|
||||
});
|
||||
override renderBlock() {
|
||||
const widgets = html`${repeat(
|
||||
Object.entries(this.widgets),
|
||||
([id]) => id,
|
||||
([_, widget]) => widget
|
||||
)}`;
|
||||
|
||||
return html`
|
||||
<div contenteditable="false" style="position: relative">
|
||||
${this.dataViewRootLogic.render()}
|
||||
${widgets}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"date-fns": "^4.0.0",
|
||||
"lit": "^3.2.0",
|
||||
|
||||
@@ -45,6 +45,7 @@ import { autoUpdate } from '@floating-ui/dom';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { html, nothing } from 'lit';
|
||||
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { popSideDetail } from './components/layout.js';
|
||||
import { DatabaseConfigExtension } from './config.js';
|
||||
import { EditorHostKey } from './context/host-context.js';
|
||||
@@ -428,9 +429,15 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
|
||||
})
|
||||
);
|
||||
override renderBlock() {
|
||||
const widgets = html`${repeat(
|
||||
Object.entries(this.widgets),
|
||||
([id]) => id,
|
||||
([_, widget]) => widget
|
||||
)}`;
|
||||
|
||||
return html`
|
||||
<div contenteditable="false" class="${databaseContentStyles}">
|
||||
${this.dataViewRootLogic.value.render()}
|
||||
${this.dataViewRootLogic.value.render()} ${widgets}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"file-type": "^21.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"katex": "^0.16.11",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -23,6 +23,7 @@ import { effect } from '@preact/signals-core';
|
||||
import { html, nothing, type TemplateResult } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { correctNumberedListsOrderToPrev } from './commands/utils.js';
|
||||
@@ -138,6 +139,11 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
|
||||
|
||||
override renderBlock(): TemplateResult<1> {
|
||||
const { model, _onClickIcon } = this;
|
||||
const widgets = html`${repeat(
|
||||
Object.entries(this.widgets),
|
||||
([id]) => id,
|
||||
([_, widget]) => widget
|
||||
)}`;
|
||||
const collapsed = this.store.readonly
|
||||
? this._readonlyCollapsed
|
||||
: model.props.collapsed;
|
||||
@@ -199,7 +205,7 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
|
||||
></rich-text>
|
||||
</div>
|
||||
|
||||
${children}
|
||||
${children} ${widgets}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -26,6 +26,7 @@ import { computed, effect, signal } from '@preact/signals-core';
|
||||
import { html, nothing, type TemplateResult } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
|
||||
@@ -227,6 +228,12 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
}
|
||||
|
||||
override renderBlock(): TemplateResult<1> {
|
||||
const widgets = html`${repeat(
|
||||
Object.entries(this.widgets),
|
||||
([id]) => id,
|
||||
([_, widget]) => widget
|
||||
)}`;
|
||||
|
||||
const { type$ } = this.model.props;
|
||||
const collapsed = this.store.readonly
|
||||
? this._readonlyCollapsed
|
||||
@@ -340,7 +347,7 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
`}
|
||||
</div>
|
||||
|
||||
${children}
|
||||
${children} ${widgets}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"dompurify": "^3.2.4",
|
||||
"html2canvas": "^1.4.1",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"fractional-indexing": "^3.2.0",
|
||||
"lit": "^3.2.0",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"fractional-indexing": "^3.2.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lottiefiles/dotlottie-wc": "^0.5.0",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.0.0",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"lit": "^3.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"collapse-white-space": "^2.1.0",
|
||||
"date-fns": "^4.0.0",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"collapse-white-space": "^2.1.0",
|
||||
"date-fns": "^4.0.0",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"collapse-white-space": "^2.1.0",
|
||||
"date-fns": "^4.0.0",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"collapse-white-space": "^2.1.0",
|
||||
"date-fns": "^4.0.0",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"fractional-indexing": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"collapse-white-space": "^2.1.0",
|
||||
"date-fns": "^4.0.0",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/bytes": "^3.1.5",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
@@ -63,7 +63,8 @@
|
||||
"./theme": "./src/theme/index.ts",
|
||||
"./styles": "./src/styles/index.ts",
|
||||
"./services": "./src/services/index.ts",
|
||||
"./adapters": "./src/adapters/index.ts"
|
||||
"./adapters": "./src/adapters/index.ts",
|
||||
"./test-utils": "./src/test-utils/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getFirstBlockCommand } from '../../../commands/block-crud/get-first-content-block';
|
||||
import { affine } from '../../helpers/affine-template';
|
||||
import { affine } from '../../../test-utils';
|
||||
|
||||
describe('commands/block-crud', () => {
|
||||
describe('getFirstBlockCommand', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getLastBlockCommand } from '../../../commands/block-crud/get-last-content-block';
|
||||
import { affine } from '../../helpers/affine-template';
|
||||
import { affine } from '../../../test-utils';
|
||||
|
||||
describe('commands/block-crud', () => {
|
||||
describe('getLastBlockCommand', () => {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import '../../helpers/affine-test-utils';
|
||||
|
||||
import type { TextSelection } from '@blocksuite/std';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { replaceSelectedTextWithBlocksCommand } from '../../../commands/model-crud/replace-selected-text-with-blocks';
|
||||
import { affine, block } from '../../helpers/affine-template';
|
||||
import { affine, block } from '../../../test-utils';
|
||||
|
||||
describe('commands/model-crud', () => {
|
||||
describe('replaceSelectedTextWithBlocksCommand', () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { isNothingSelectedCommand } from '../../../commands/selection/is-nothing-selected';
|
||||
import { ImageSelection } from '../../../selection';
|
||||
import { affine } from '../../helpers/affine-template';
|
||||
import { affine } from '../../../test-utils';
|
||||
|
||||
describe('commands/selection', () => {
|
||||
describe('isNothingSelectedCommand', () => {
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
import {
|
||||
CodeBlockSchemaExtension,
|
||||
DatabaseBlockSchemaExtension,
|
||||
ImageBlockSchemaExtension,
|
||||
ListBlockSchemaExtension,
|
||||
NoteBlockSchemaExtension,
|
||||
ParagraphBlockSchemaExtension,
|
||||
RootBlockSchemaExtension,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { TextSelection } from '@blocksuite/std';
|
||||
import { type Block, type Store } from '@blocksuite/store';
|
||||
import { Text } from '@blocksuite/store';
|
||||
import { TestWorkspace } from '@blocksuite/store/test';
|
||||
|
||||
import { createTestHost } from './create-test-host';
|
||||
|
||||
// Extensions array
|
||||
const extensions = [
|
||||
RootBlockSchemaExtension,
|
||||
NoteBlockSchemaExtension,
|
||||
ParagraphBlockSchemaExtension,
|
||||
ListBlockSchemaExtension,
|
||||
ImageBlockSchemaExtension,
|
||||
DatabaseBlockSchemaExtension,
|
||||
CodeBlockSchemaExtension,
|
||||
];
|
||||
|
||||
// Mapping from tag names to flavours
|
||||
const tagToFlavour: Record<string, string> = {
|
||||
'affine-page': 'affine:page',
|
||||
'affine-note': 'affine:note',
|
||||
'affine-paragraph': 'affine:paragraph',
|
||||
'affine-list': 'affine:list',
|
||||
'affine-image': 'affine:image',
|
||||
'affine-database': 'affine:database',
|
||||
'affine-code': 'affine:code',
|
||||
};
|
||||
|
||||
interface SelectionInfo {
|
||||
anchorBlockId?: string;
|
||||
anchorOffset?: number;
|
||||
focusBlockId?: string;
|
||||
focusOffset?: number;
|
||||
cursorBlockId?: string;
|
||||
cursorOffset?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse template strings and build BlockSuite document structure,
|
||||
* then create a host object with the document
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* const host = affine`
|
||||
* <affine-page id="page">
|
||||
* <affine-note id="note">
|
||||
* <affine-paragraph id="paragraph-1">Hello, world<anchor /></affine-paragraph>
|
||||
* <affine-paragraph id="paragraph-2">Hello, world<focus /></affine-paragraph>
|
||||
* </affine-note>
|
||||
* </affine-page>
|
||||
* `;
|
||||
* ```
|
||||
*/
|
||||
export function affine(strings: TemplateStringsArray, ...values: any[]) {
|
||||
// Merge template strings and values
|
||||
let htmlString = '';
|
||||
strings.forEach((str, i) => {
|
||||
htmlString += str;
|
||||
if (i < values.length) {
|
||||
htmlString += values[i];
|
||||
}
|
||||
});
|
||||
|
||||
// Create a new doc
|
||||
const workspace = new TestWorkspace({});
|
||||
workspace.meta.initialize();
|
||||
const doc = workspace.createDoc('test-doc');
|
||||
const store = doc.getStore({ extensions });
|
||||
|
||||
let selectionInfo: SelectionInfo = {};
|
||||
|
||||
// Use DOMParser to parse HTML string
|
||||
doc.load(() => {
|
||||
const parser = new DOMParser();
|
||||
const dom = parser.parseFromString(htmlString.trim(), 'text/html');
|
||||
const root = dom.body.firstElementChild;
|
||||
|
||||
if (!root) {
|
||||
throw new Error('Template must contain a root element');
|
||||
}
|
||||
|
||||
buildDocFromElement(store, root, null, selectionInfo);
|
||||
});
|
||||
|
||||
// Create host object
|
||||
const host = createTestHost(store);
|
||||
|
||||
// Set selection if needed
|
||||
if (selectionInfo.anchorBlockId && selectionInfo.focusBlockId) {
|
||||
const anchorBlock = store.getBlock(selectionInfo.anchorBlockId);
|
||||
const anchorTextLength = anchorBlock?.model?.text?.length ?? 0;
|
||||
const focusOffset = selectionInfo.focusOffset ?? 0;
|
||||
const anchorOffset = selectionInfo.anchorOffset ?? 0;
|
||||
|
||||
if (selectionInfo.anchorBlockId === selectionInfo.focusBlockId) {
|
||||
const selection = host.selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId: selectionInfo.anchorBlockId,
|
||||
index: anchorOffset,
|
||||
length: focusOffset,
|
||||
},
|
||||
to: null,
|
||||
});
|
||||
host.selection.setGroup('note', [selection]);
|
||||
} else {
|
||||
const selection = host.selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId: selectionInfo.anchorBlockId,
|
||||
index: anchorOffset,
|
||||
length: anchorTextLength - anchorOffset,
|
||||
},
|
||||
to: {
|
||||
blockId: selectionInfo.focusBlockId,
|
||||
index: 0,
|
||||
length: focusOffset,
|
||||
},
|
||||
});
|
||||
host.selection.setGroup('note', [selection]);
|
||||
}
|
||||
} else if (selectionInfo.cursorBlockId) {
|
||||
const selection = host.selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId: selectionInfo.cursorBlockId,
|
||||
index: selectionInfo.cursorOffset ?? 0,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
});
|
||||
host.selection.setGroup('note', [selection]);
|
||||
}
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single block from template string
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* const block = block`<affine-note />`
|
||||
* ```
|
||||
*/
|
||||
export function block(
|
||||
strings: TemplateStringsArray,
|
||||
...values: any[]
|
||||
): Block | null {
|
||||
// Merge template strings and values
|
||||
let htmlString = '';
|
||||
strings.forEach((str, i) => {
|
||||
htmlString += str;
|
||||
if (i < values.length) {
|
||||
htmlString += values[i];
|
||||
}
|
||||
});
|
||||
|
||||
// Create a temporary doc to hold the block
|
||||
const workspace = new TestWorkspace({});
|
||||
workspace.meta.initialize();
|
||||
const doc = workspace.createDoc('temp-doc');
|
||||
const store = doc.getStore({ extensions });
|
||||
|
||||
let blockId: string | null = null;
|
||||
const selectionInfo: SelectionInfo = {};
|
||||
|
||||
// Use DOMParser to parse HTML string
|
||||
doc.load(() => {
|
||||
const parser = new DOMParser();
|
||||
const dom = parser.parseFromString(htmlString.trim(), 'text/html');
|
||||
const root = dom.body.firstElementChild;
|
||||
|
||||
if (!root) {
|
||||
throw new Error('Template must contain a root element');
|
||||
}
|
||||
|
||||
// Create a root block if needed
|
||||
const flavour = tagToFlavour[root.tagName.toLowerCase()];
|
||||
if (
|
||||
flavour === 'affine:paragraph' ||
|
||||
flavour === 'affine:list' ||
|
||||
flavour === 'affine:code'
|
||||
) {
|
||||
const pageId = store.addBlock('affine:page', {});
|
||||
const noteId = store.addBlock('affine:note', {}, pageId);
|
||||
blockId = buildDocFromElement(store, root, noteId, selectionInfo);
|
||||
} else {
|
||||
blockId = buildDocFromElement(store, root, null, selectionInfo);
|
||||
}
|
||||
});
|
||||
|
||||
// Return the created block
|
||||
return blockId ? (store.getBlock(blockId) ?? null) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively build document structure
|
||||
* @param doc
|
||||
* @param element
|
||||
* @param parentId
|
||||
* @param selectionInfo
|
||||
* @returns
|
||||
*/
|
||||
function buildDocFromElement(
|
||||
doc: Store,
|
||||
element: Element,
|
||||
parentId: string | null,
|
||||
selectionInfo: SelectionInfo
|
||||
): string {
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
|
||||
// Handle selection tags
|
||||
if (tagName === 'anchor') {
|
||||
if (!parentId) return '';
|
||||
const parentBlock = doc.getBlock(parentId);
|
||||
if (parentBlock) {
|
||||
const textBeforeCursor = element.previousSibling?.textContent ?? '';
|
||||
selectionInfo.anchorBlockId = parentId;
|
||||
selectionInfo.anchorOffset = textBeforeCursor.length;
|
||||
}
|
||||
return parentId;
|
||||
} else if (tagName === 'focus') {
|
||||
if (!parentId) return '';
|
||||
const parentBlock = doc.getBlock(parentId);
|
||||
if (parentBlock) {
|
||||
const textBeforeCursor = element.previousSibling?.textContent ?? '';
|
||||
selectionInfo.focusBlockId = parentId;
|
||||
selectionInfo.focusOffset = textBeforeCursor.length;
|
||||
}
|
||||
return parentId;
|
||||
} else if (tagName === 'cursor') {
|
||||
if (!parentId) return '';
|
||||
const parentBlock = doc.getBlock(parentId);
|
||||
if (parentBlock) {
|
||||
const textBeforeCursor = element.previousSibling?.textContent ?? '';
|
||||
selectionInfo.cursorBlockId = parentId;
|
||||
selectionInfo.cursorOffset = textBeforeCursor.length;
|
||||
}
|
||||
return parentId;
|
||||
}
|
||||
|
||||
const flavour = tagToFlavour[tagName];
|
||||
|
||||
if (!flavour) {
|
||||
throw new Error(`Unknown tag name: ${tagName}`);
|
||||
}
|
||||
|
||||
const props: Record<string, any> = {};
|
||||
|
||||
const customId = element.getAttribute('id');
|
||||
|
||||
// If ID is specified, add it to props
|
||||
if (customId) {
|
||||
props.id = customId;
|
||||
}
|
||||
|
||||
// Process element attributes
|
||||
Array.from(element.attributes).forEach(attr => {
|
||||
if (attr.name !== 'id') {
|
||||
// Skip id attribute, we already handled it
|
||||
props[attr.name] = attr.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Special handling for different block types based on their flavours
|
||||
switch (flavour) {
|
||||
case 'affine:paragraph':
|
||||
case 'affine:list':
|
||||
if (element.textContent) {
|
||||
props.text = new Text(element.textContent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Create block
|
||||
const blockId = doc.addBlock(flavour, props, parentId);
|
||||
|
||||
// Process all child nodes, including text nodes
|
||||
Array.from(element.children).forEach(child => {
|
||||
if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
// Handle element nodes
|
||||
buildDocFromElement(doc, child as Element, blockId, selectionInfo);
|
||||
} else if (child.nodeType === Node.TEXT_NODE) {
|
||||
// Handle text nodes
|
||||
console.log('buildDocFromElement text node:', child.textContent);
|
||||
}
|
||||
});
|
||||
|
||||
return blockId;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TextSelection } from '@blocksuite/std';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { affine } from './affine-template';
|
||||
import { affine } from '../../test-utils';
|
||||
|
||||
describe('helpers/affine-template', () => {
|
||||
it('should create a basic document structure from template', () => {
|
||||
@@ -7,7 +7,7 @@
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { affine } from '../__tests__/utils/affine-template';
|
||||
import { affine } from '@blocksuite/affine-shared/test-utils';
|
||||
|
||||
// Create a simple document
|
||||
const doc = affine`
|
||||
316
blocksuite/affine/shared/src/test-utils/affine-template.ts
Normal file
316
blocksuite/affine/shared/src/test-utils/affine-template.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import {
|
||||
CodeBlockSchemaExtension,
|
||||
DatabaseBlockSchemaExtension,
|
||||
ImageBlockSchemaExtension,
|
||||
ListBlockSchemaExtension,
|
||||
NoteBlockSchemaExtension,
|
||||
ParagraphBlockSchemaExtension,
|
||||
RootBlockSchemaExtension,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { Container } from '@blocksuite/global/di';
|
||||
import { TextSelection } from '@blocksuite/std';
|
||||
import {
|
||||
type Block,
|
||||
type ExtensionType,
|
||||
type Store,
|
||||
Text,
|
||||
} from '@blocksuite/store';
|
||||
import { TestWorkspace } from '@blocksuite/store/test';
|
||||
|
||||
import { createTestHost } from './create-test-host';
|
||||
|
||||
const DEFAULT_EXTENSIONS = [
|
||||
RootBlockSchemaExtension,
|
||||
NoteBlockSchemaExtension,
|
||||
ParagraphBlockSchemaExtension,
|
||||
ListBlockSchemaExtension,
|
||||
ImageBlockSchemaExtension,
|
||||
DatabaseBlockSchemaExtension,
|
||||
CodeBlockSchemaExtension,
|
||||
];
|
||||
|
||||
// Mapping from tag names to flavours
|
||||
const tagToFlavour: Record<string, string> = {
|
||||
'affine-page': 'affine:page',
|
||||
'affine-note': 'affine:note',
|
||||
'affine-paragraph': 'affine:paragraph',
|
||||
'affine-list': 'affine:list',
|
||||
'affine-image': 'affine:image',
|
||||
'affine-database': 'affine:database',
|
||||
'affine-code': 'affine:code',
|
||||
};
|
||||
|
||||
interface SelectionInfo {
|
||||
anchorBlockId?: string;
|
||||
anchorOffset?: number;
|
||||
focusBlockId?: string;
|
||||
focusOffset?: number;
|
||||
cursorBlockId?: string;
|
||||
cursorOffset?: number;
|
||||
}
|
||||
|
||||
export function createAffineTemplate(
|
||||
extensions: ExtensionType[] = DEFAULT_EXTENSIONS
|
||||
) {
|
||||
/**
|
||||
* Parse template strings and build BlockSuite document structure,
|
||||
* then create a host object with the document
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* const host = affine`
|
||||
* <affine-page id="page">
|
||||
* <affine-note id="note">
|
||||
* <affine-paragraph id="paragraph-1">Hello, world<anchor /></affine-paragraph>
|
||||
* <affine-paragraph id="paragraph-2">Hello, world<focus /></affine-paragraph>
|
||||
* </affine-note>
|
||||
* </affine-page>
|
||||
* `;
|
||||
* ```
|
||||
*/
|
||||
function affine(strings: TemplateStringsArray, ...values: any[]) {
|
||||
// Merge template strings and values
|
||||
let htmlString = '';
|
||||
strings.forEach((str, i) => {
|
||||
htmlString += str;
|
||||
if (i < values.length) {
|
||||
htmlString += values[i];
|
||||
}
|
||||
});
|
||||
|
||||
// Create a new doc
|
||||
const workspace = new TestWorkspace({});
|
||||
workspace.meta.initialize();
|
||||
const doc = workspace.createDoc('test-doc');
|
||||
const container = new Container();
|
||||
extensions.forEach(extension => {
|
||||
extension.setup(container);
|
||||
});
|
||||
const store = doc.getStore({ extensions, provider: container.provider() });
|
||||
let selectionInfo: SelectionInfo = {};
|
||||
|
||||
// Use DOMParser to parse HTML string
|
||||
doc.load(() => {
|
||||
const parser = new DOMParser();
|
||||
const dom = parser.parseFromString(htmlString.trim(), 'text/html');
|
||||
const root = dom.body.firstElementChild;
|
||||
|
||||
if (!root) {
|
||||
throw new Error('Template must contain a root element');
|
||||
}
|
||||
|
||||
buildDocFromElement(store, root, null, selectionInfo);
|
||||
});
|
||||
|
||||
// Create host object
|
||||
const host = createTestHost(store);
|
||||
|
||||
// Set selection if needed
|
||||
if (selectionInfo.anchorBlockId && selectionInfo.focusBlockId) {
|
||||
const anchorBlock = store.getBlock(selectionInfo.anchorBlockId);
|
||||
const anchorTextLength = anchorBlock?.model?.text?.length ?? 0;
|
||||
const focusOffset = selectionInfo.focusOffset ?? 0;
|
||||
const anchorOffset = selectionInfo.anchorOffset ?? 0;
|
||||
|
||||
if (selectionInfo.anchorBlockId === selectionInfo.focusBlockId) {
|
||||
const selection = host.selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId: selectionInfo.anchorBlockId,
|
||||
index: anchorOffset,
|
||||
length: focusOffset,
|
||||
},
|
||||
to: null,
|
||||
});
|
||||
host.selection.setGroup('note', [selection]);
|
||||
} else {
|
||||
const selection = host.selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId: selectionInfo.anchorBlockId,
|
||||
index: anchorOffset,
|
||||
length: anchorTextLength - anchorOffset,
|
||||
},
|
||||
to: {
|
||||
blockId: selectionInfo.focusBlockId,
|
||||
index: 0,
|
||||
length: focusOffset,
|
||||
},
|
||||
});
|
||||
host.selection.setGroup('note', [selection]);
|
||||
}
|
||||
} else if (selectionInfo.cursorBlockId) {
|
||||
const selection = host.selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId: selectionInfo.cursorBlockId,
|
||||
index: selectionInfo.cursorOffset ?? 0,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
});
|
||||
host.selection.setGroup('note', [selection]);
|
||||
}
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single block from template string
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* const block = block`<affine-note />`
|
||||
* ```
|
||||
*/
|
||||
function block(
|
||||
strings: TemplateStringsArray,
|
||||
...values: any[]
|
||||
): Block | null {
|
||||
// Merge template strings and values
|
||||
let htmlString = '';
|
||||
strings.forEach((str, i) => {
|
||||
htmlString += str;
|
||||
if (i < values.length) {
|
||||
htmlString += values[i];
|
||||
}
|
||||
});
|
||||
|
||||
// Create a temporary doc to hold the block
|
||||
const workspace = new TestWorkspace({});
|
||||
workspace.meta.initialize();
|
||||
const doc = workspace.createDoc('temp-doc');
|
||||
const store = doc.getStore({ extensions });
|
||||
|
||||
let blockId: string | null = null;
|
||||
const selectionInfo: SelectionInfo = {};
|
||||
|
||||
// Use DOMParser to parse HTML string
|
||||
doc.load(() => {
|
||||
const parser = new DOMParser();
|
||||
const dom = parser.parseFromString(htmlString.trim(), 'text/html');
|
||||
const root = dom.body.firstElementChild;
|
||||
|
||||
if (!root) {
|
||||
throw new Error('Template must contain a root element');
|
||||
}
|
||||
|
||||
// Create a root block if needed
|
||||
const flavour = tagToFlavour[root.tagName.toLowerCase()];
|
||||
if (
|
||||
flavour === 'affine:paragraph' ||
|
||||
flavour === 'affine:list' ||
|
||||
flavour === 'affine:code'
|
||||
) {
|
||||
const pageId = store.addBlock('affine:page', {});
|
||||
const noteId = store.addBlock('affine:note', {}, pageId);
|
||||
blockId = buildDocFromElement(store, root, noteId, selectionInfo);
|
||||
} else {
|
||||
blockId = buildDocFromElement(store, root, null, selectionInfo);
|
||||
}
|
||||
});
|
||||
|
||||
// Return the created block
|
||||
return blockId ? (store.getBlock(blockId) ?? null) : null;
|
||||
}
|
||||
|
||||
return {
|
||||
affine,
|
||||
block,
|
||||
};
|
||||
}
|
||||
|
||||
export const { affine, block } = createAffineTemplate();
|
||||
|
||||
/**
|
||||
* Recursively build document structure
|
||||
* @param doc
|
||||
* @param element
|
||||
* @param parentId
|
||||
* @param selectionInfo
|
||||
* @returns
|
||||
*/
|
||||
function buildDocFromElement(
|
||||
doc: Store,
|
||||
element: Element,
|
||||
parentId: string | null,
|
||||
selectionInfo: SelectionInfo
|
||||
): string {
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
|
||||
// Handle selection tags
|
||||
if (tagName === 'anchor') {
|
||||
if (!parentId) return '';
|
||||
const parentBlock = doc.getBlock(parentId);
|
||||
if (parentBlock) {
|
||||
const textBeforeCursor = element.previousSibling?.textContent ?? '';
|
||||
selectionInfo.anchorBlockId = parentId;
|
||||
selectionInfo.anchorOffset = textBeforeCursor.length;
|
||||
}
|
||||
return parentId;
|
||||
} else if (tagName === 'focus') {
|
||||
if (!parentId) return '';
|
||||
const parentBlock = doc.getBlock(parentId);
|
||||
if (parentBlock) {
|
||||
const textBeforeCursor = element.previousSibling?.textContent ?? '';
|
||||
selectionInfo.focusBlockId = parentId;
|
||||
selectionInfo.focusOffset = textBeforeCursor.length;
|
||||
}
|
||||
return parentId;
|
||||
} else if (tagName === 'cursor') {
|
||||
if (!parentId) return '';
|
||||
const parentBlock = doc.getBlock(parentId);
|
||||
if (parentBlock) {
|
||||
const textBeforeCursor = element.previousSibling?.textContent ?? '';
|
||||
selectionInfo.cursorBlockId = parentId;
|
||||
selectionInfo.cursorOffset = textBeforeCursor.length;
|
||||
}
|
||||
return parentId;
|
||||
}
|
||||
|
||||
const flavour = tagToFlavour[tagName];
|
||||
|
||||
if (!flavour) {
|
||||
throw new Error(`Unknown tag name: ${tagName}`);
|
||||
}
|
||||
|
||||
const props: Record<string, any> = {};
|
||||
|
||||
const customId = element.getAttribute('id');
|
||||
|
||||
// If ID is specified, add it to props
|
||||
if (customId) {
|
||||
props.id = customId;
|
||||
}
|
||||
|
||||
// Process element attributes
|
||||
Array.from(element.attributes).forEach(attr => {
|
||||
if (attr.name !== 'id') {
|
||||
// Skip id attribute, we already handled it
|
||||
props[attr.name] = attr.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Special handling for different block types based on their flavours
|
||||
switch (flavour) {
|
||||
case 'affine:paragraph':
|
||||
case 'affine:list':
|
||||
if (element.textContent) {
|
||||
props.text = new Text(element.textContent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Create block
|
||||
const blockId = doc.addBlock(flavour, props, parentId);
|
||||
|
||||
// Process all child nodes, including text nodes
|
||||
Array.from(element.children).forEach(child => {
|
||||
if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
// Handle element nodes
|
||||
buildDocFromElement(doc, child as Element, blockId, selectionInfo);
|
||||
} else if (child.nodeType === Node.TEXT_NODE) {
|
||||
// Handle text nodes
|
||||
console.log('buildDocFromElement text node:', child.textContent);
|
||||
}
|
||||
});
|
||||
|
||||
return blockId;
|
||||
}
|
||||
@@ -63,10 +63,8 @@ function compareBlocks(
|
||||
if (JSON.stringify(actualProps) !== JSON.stringify(expectedProps))
|
||||
return false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
||||
for (let i = 0; i < actual.children.length; i++) {
|
||||
if (!compareBlocks(actual.children[i], expected.children[i], compareId))
|
||||
return false;
|
||||
for (const [i, child] of actual.children.entries()) {
|
||||
if (!compareBlocks(child, expected.children[i], compareId)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -240,7 +240,7 @@ export function createTestHost(doc: Store): EditorHost {
|
||||
std.selection = new MockSelectionStore();
|
||||
|
||||
std.command = new CommandManager(std as any);
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error dev-only
|
||||
host.command = std.command;
|
||||
host.selection = std.selection;
|
||||
|
||||
3
blocksuite/affine/shared/src/test-utils/index.ts
Normal file
3
blocksuite/affine/shared/src/test-utils/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './affine-template';
|
||||
export * from './affine-test-utils';
|
||||
export * from './create-test-host';
|
||||
@@ -27,7 +27,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@blocksuite/icons": "^2.2.12",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"lit": "^3.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"lit": "^3.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"yjs": "^13.6.21"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"lit": "^3.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"yjs": "^13.6.21"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"lit": "^3.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"fflate": "^0.8.2",
|
||||
"lit": "^3.2.0",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"fflate": "^0.8.2",
|
||||
"lit": "^3.2.0",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"lit": "^3.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"yjs": "^13.6.21"
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"fflate": "^0.8.2",
|
||||
"lit": "^3.2.0",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@blocksuite/icons": "^2.2.12",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"lit": "^3.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -28,6 +28,21 @@ import { ShadowlessElement } from './shadowless-element.js';
|
||||
export const storeContext = createContext<Store>('store');
|
||||
export const stdContext = createContext<BlockStdScope>('std');
|
||||
|
||||
function isMatchFlavour(widgetFlavour: string, block: BlockModel) {
|
||||
if (widgetFlavour.endsWith('/*')) {
|
||||
const path = widgetFlavour.slice(0, -2).split('/');
|
||||
let current: BlockModel | null = block.parent;
|
||||
for (let i = path.length - 1; i >= 0; i--) {
|
||||
if (!current || current.flavour !== path[i]) {
|
||||
return false;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return block.flavour === widgetFlavour;
|
||||
}
|
||||
|
||||
@requiredProperties({
|
||||
store: PropTypes.instanceOf(Store),
|
||||
std: PropTypes.object,
|
||||
@@ -61,7 +76,7 @@ export class EditorHost extends SignalWatcher(
|
||||
const widgets = Array.from(widgetViews.entries()).reduce(
|
||||
(mapping, [key, tag]) => {
|
||||
const [widgetFlavour, id] = key.split('|');
|
||||
if (widgetFlavour === flavour) {
|
||||
if (isMatchFlavour(widgetFlavour, model)) {
|
||||
const template = html`<${tag} ${unsafeStatic(WIDGET_ID_ATTR)}=${id}></${tag}>`;
|
||||
mapping[id] = template;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@lit/context": "^1.1.3",
|
||||
"@lottiefiles/dotlottie-wc": "^0.5.0",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
"lit": "^3.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@sentry/react": "^9.2.0",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"cmdk": "^1.0.4",
|
||||
"embla-carousel-react": "^8.5.1",
|
||||
"input-otp": "^1.4.1",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@sentry/react": "^9.2.0",
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
"async-call-rpc": "^6.4.2",
|
||||
"next-themes": "^0.4.4",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"@radix-ui/react-toast": "^1.2.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.5",
|
||||
"@radix-ui/react-visually-hidden": "^1.1.1",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@vanilla-extract/dynamic": "^2.1.2",
|
||||
"bytes": "^3.1.2",
|
||||
"check-password-strength": "^3.0.0",
|
||||
|
||||
@@ -306,3 +306,15 @@ math {
|
||||
'STIX Two Math' /* mac */,
|
||||
math;
|
||||
}
|
||||
|
||||
/* AI Block Diff */
|
||||
.ai-block-diff-deleted {
|
||||
background-color: var(--aI-applyDeleteHighlight, #ffeaea) !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 8px 0px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
.ai-block-diff-deleted .affine-block-component {
|
||||
margin: 0 !important;
|
||||
}
|
||||
@@ -19,6 +19,9 @@
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/track": "workspace:*",
|
||||
"@blocksuite/affine": "workspace:*",
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/icons": "^2.2.13",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@dotlottie/player-component": "^2.7.12",
|
||||
@@ -39,7 +42,7 @@
|
||||
"@sentry/react": "^9.2.0",
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"@toeverything/pdf-viewer": "^0.1.1",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"@vanilla-extract/dynamic": "^2.1.2",
|
||||
"@webcontainer/api": "^1.6.1",
|
||||
"animejs": "^4.0.0",
|
||||
@@ -88,6 +91,7 @@
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@types/animejs": "^3.1.12",
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { getInternalStoreExtensions } from '@blocksuite/affine/extensions/store';
|
||||
import { StoreExtensionManager } from '@blocksuite/affine-ext-loader';
|
||||
import { createAffineTemplate } from '@blocksuite/affine-shared/test-utils';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { applyPatchToDoc } from '../../../../blocksuite/ai/utils/apply-model/apply-patch-to-doc';
|
||||
import type { PatchOp } from '../../../../blocksuite/ai/utils/apply-model/markdown-diff';
|
||||
|
||||
const manager = new StoreExtensionManager(getInternalStoreExtensions());
|
||||
const { affine } = createAffineTemplate(manager.get('store'));
|
||||
|
||||
describe('applyPatchToDoc', () => {
|
||||
it('should delete a block', async () => {
|
||||
const host = affine`
|
||||
<affine-page id="page">
|
||||
<affine-note id="note">
|
||||
<affine-paragraph id="paragraph-1">Hello</affine-paragraph>
|
||||
<affine-paragraph id="paragraph-2">World</affine-paragraph>
|
||||
</affine-note>
|
||||
</affine-page>
|
||||
`;
|
||||
|
||||
const patch: PatchOp[] = [{ op: 'delete', id: 'paragraph-1' }];
|
||||
await applyPatchToDoc(host.store, patch);
|
||||
|
||||
const expected = affine`
|
||||
<affine-page id="page">
|
||||
<affine-note id="note">
|
||||
<affine-paragraph id="paragraph-2">World</affine-paragraph>
|
||||
</affine-note>
|
||||
</affine-page>
|
||||
`;
|
||||
|
||||
expect(host.store).toEqualDoc(expected.store, {
|
||||
compareId: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace a block', async () => {
|
||||
const host = affine`
|
||||
<affine-page id="page">
|
||||
<affine-note id="note">
|
||||
<affine-paragraph id="paragraph-1">Hello</affine-paragraph>
|
||||
<affine-paragraph id="paragraph-2">World</affine-paragraph>
|
||||
</affine-note>
|
||||
</affine-page>
|
||||
`;
|
||||
|
||||
const patch: PatchOp[] = [
|
||||
{
|
||||
op: 'replace',
|
||||
id: 'paragraph-1',
|
||||
content: 'New content',
|
||||
},
|
||||
];
|
||||
|
||||
await applyPatchToDoc(host.store, patch);
|
||||
|
||||
const expected = affine`
|
||||
<affine-page id="page">
|
||||
<affine-note id="note">
|
||||
<affine-paragraph id="paragraph-1">New content</affine-paragraph>
|
||||
<affine-paragraph id="paragraph-2">World</affine-paragraph>
|
||||
</affine-note>
|
||||
</affine-page>
|
||||
`;
|
||||
|
||||
expect(host.store).toEqualDoc(expected.store, {
|
||||
compareId: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should insert a block at index', async () => {
|
||||
const host = affine`
|
||||
<affine-page id="page">
|
||||
<affine-note id="note">
|
||||
<affine-paragraph id="paragraph-1">Hello</affine-paragraph>
|
||||
<affine-paragraph id="paragraph-2">World</affine-paragraph>
|
||||
</affine-note>
|
||||
</affine-page>
|
||||
`;
|
||||
|
||||
const patch: PatchOp[] = [
|
||||
{
|
||||
op: 'insert',
|
||||
index: 2,
|
||||
block: {
|
||||
id: 'paragraph-3',
|
||||
type: 'affine:paragraph',
|
||||
content: 'Inserted',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await applyPatchToDoc(host.store, patch);
|
||||
|
||||
const expected = affine`
|
||||
<affine-page id="page">
|
||||
<affine-note id="note">
|
||||
<affine-paragraph id="paragraph-1">Hello</affine-paragraph>
|
||||
<affine-paragraph id="paragraph-2">World</affine-paragraph>
|
||||
<affine-paragraph id="paragraph-3">Inserted</affine-paragraph>
|
||||
</affine-note>
|
||||
</affine-page>
|
||||
`;
|
||||
|
||||
expect(host.store).toEqualDoc(expected.store, {
|
||||
compareId: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,337 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { generateRenderDiff } from '../../../../blocksuite/ai/utils/apply-model/generate-render-diff';
|
||||
|
||||
describe('generateRenderDiff', () => {
|
||||
test('should handle block insertion', () => {
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
This is a new paragraph.
|
||||
`;
|
||||
const diff = generateRenderDiff(oldMd, newMd);
|
||||
expect(diff).toEqual({
|
||||
deletes: [],
|
||||
inserts: {
|
||||
'block-001': [
|
||||
{
|
||||
id: 'block-002',
|
||||
type: 'paragraph',
|
||||
content: 'This is a new paragraph.',
|
||||
},
|
||||
],
|
||||
},
|
||||
updates: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle block deletion', () => {
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
This paragraph will be deleted.
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
`;
|
||||
const diff = generateRenderDiff(oldMd, newMd);
|
||||
expect(diff).toEqual({
|
||||
deletes: ['block-002'],
|
||||
inserts: {},
|
||||
updates: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle block replacement', () => {
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Old Title
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# New Title
|
||||
`;
|
||||
const diff = generateRenderDiff(oldMd, newMd);
|
||||
expect(diff).toEqual({
|
||||
deletes: [],
|
||||
inserts: {},
|
||||
updates: {
|
||||
'block-001': '# New Title',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle mixed changes', () => {
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
Old paragraph.
|
||||
|
||||
<!-- block_id=block-003 flavour=paragraph -->
|
||||
To be deleted.
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
Updated paragraph.
|
||||
|
||||
<!-- block_id=block-004 flavour=paragraph -->
|
||||
New paragraph.
|
||||
`;
|
||||
const diff = generateRenderDiff(oldMd, newMd);
|
||||
expect(diff).toEqual({
|
||||
deletes: ['block-003'],
|
||||
inserts: {
|
||||
'block-002': [
|
||||
{
|
||||
id: 'block-004',
|
||||
type: 'paragraph',
|
||||
content: 'New paragraph.',
|
||||
},
|
||||
],
|
||||
},
|
||||
updates: {
|
||||
'block-002': 'Updated paragraph.',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle consecutive block insertions', () => {
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
First inserted paragraph.
|
||||
|
||||
<!-- block_id=block-003 flavour=paragraph -->
|
||||
Second inserted paragraph.
|
||||
`;
|
||||
const diff = generateRenderDiff(oldMd, newMd);
|
||||
expect(diff).toEqual({
|
||||
deletes: [],
|
||||
inserts: {
|
||||
'block-001': [
|
||||
{
|
||||
id: 'block-002',
|
||||
type: 'paragraph',
|
||||
content: 'First inserted paragraph.',
|
||||
},
|
||||
{
|
||||
id: 'block-003',
|
||||
type: 'paragraph',
|
||||
content: 'Second inserted paragraph.',
|
||||
},
|
||||
],
|
||||
},
|
||||
updates: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle consecutive block deletions', () => {
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
First paragraph to be deleted.
|
||||
|
||||
<!-- block_id=block-003 flavour=paragraph -->
|
||||
Second paragraph to be deleted.
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
`;
|
||||
const diff = generateRenderDiff(oldMd, newMd);
|
||||
expect(diff).toEqual({
|
||||
deletes: ['block-002', 'block-003'],
|
||||
inserts: {},
|
||||
updates: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle block insertion at the head', () => {
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-000 flavour=paragraph -->
|
||||
Head paragraph.
|
||||
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
`;
|
||||
const diff = generateRenderDiff(oldMd, newMd);
|
||||
expect(diff).toEqual({
|
||||
deletes: [],
|
||||
inserts: {
|
||||
HEAD: [
|
||||
{
|
||||
id: 'block-000',
|
||||
type: 'paragraph',
|
||||
content: 'Head paragraph.',
|
||||
},
|
||||
],
|
||||
},
|
||||
updates: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle block insertion at the tail', () => {
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
Tail paragraph.
|
||||
`;
|
||||
const diff = generateRenderDiff(oldMd, newMd);
|
||||
expect(diff).toEqual({
|
||||
deletes: [],
|
||||
inserts: {
|
||||
'block-001': [
|
||||
{
|
||||
id: 'block-002',
|
||||
type: 'paragraph',
|
||||
content: 'Tail paragraph.',
|
||||
},
|
||||
],
|
||||
},
|
||||
updates: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle delete then insert after', () => {
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
To be deleted.
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-003 flavour=paragraph -->
|
||||
Inserted after delete.
|
||||
`;
|
||||
const diff = generateRenderDiff(oldMd, newMd);
|
||||
expect(diff).toEqual({
|
||||
deletes: ['block-002'],
|
||||
inserts: {
|
||||
'block-001': [
|
||||
{
|
||||
id: 'block-003',
|
||||
type: 'paragraph',
|
||||
content: 'Inserted after delete.',
|
||||
},
|
||||
],
|
||||
},
|
||||
updates: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle consecutive insertions', () => {
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
First insert.
|
||||
|
||||
<!-- block_id=block-003 flavour=paragraph -->
|
||||
Second insert.
|
||||
`;
|
||||
const diff = generateRenderDiff(oldMd, newMd);
|
||||
expect(diff).toEqual({
|
||||
deletes: [],
|
||||
inserts: {
|
||||
'block-001': [
|
||||
{
|
||||
id: 'block-002',
|
||||
type: 'paragraph',
|
||||
content: 'First insert.',
|
||||
},
|
||||
{
|
||||
id: 'block-003',
|
||||
type: 'paragraph',
|
||||
content: 'Second insert.',
|
||||
},
|
||||
],
|
||||
},
|
||||
updates: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle interval insertions', () => {
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
Paragraph.
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-003 flavour=paragraph -->
|
||||
Inserted between.
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
Paragraph.
|
||||
|
||||
<!-- block_id=block-004 flavour=paragraph -->
|
||||
Inserted at tail.
|
||||
`;
|
||||
const diff = generateRenderDiff(oldMd, newMd);
|
||||
expect(diff).toEqual({
|
||||
deletes: [],
|
||||
inserts: {
|
||||
'block-001': [
|
||||
{
|
||||
id: 'block-003',
|
||||
type: 'paragraph',
|
||||
content: 'Inserted between.',
|
||||
},
|
||||
],
|
||||
'block-002': [
|
||||
{
|
||||
id: 'block-004',
|
||||
type: 'paragraph',
|
||||
content: 'Inserted at tail.',
|
||||
},
|
||||
],
|
||||
},
|
||||
updates: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,228 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { diffMarkdown } from '../../../../blocksuite/ai/utils/apply-model/markdown-diff';
|
||||
|
||||
describe('diffMarkdown', () => {
|
||||
test('should diff block insertion', () => {
|
||||
// Only a new block is inserted
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
This is a new paragraph.
|
||||
`;
|
||||
const { patches } = diffMarkdown(oldMd, newMd);
|
||||
expect(patches).toEqual([
|
||||
{
|
||||
op: 'insert',
|
||||
index: 1,
|
||||
block: {
|
||||
id: 'block-002',
|
||||
type: 'paragraph',
|
||||
content: 'This is a new paragraph.',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should diff block deletion', () => {
|
||||
// A block is deleted
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
This paragraph will be deleted.
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
`;
|
||||
const { patches } = diffMarkdown(oldMd, newMd);
|
||||
expect(patches).toEqual([
|
||||
{
|
||||
op: 'delete',
|
||||
id: 'block-002',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should diff block replacement', () => {
|
||||
// Only content of a block is changed
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Old Title
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# New Title
|
||||
`;
|
||||
const { patches } = diffMarkdown(oldMd, newMd);
|
||||
expect(patches).toEqual([
|
||||
{
|
||||
op: 'replace',
|
||||
id: 'block-001',
|
||||
content: '# New Title',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should diff mixed changes', () => {
|
||||
// Mixed: delete, insert, replace
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
Old paragraph.
|
||||
|
||||
<!-- block_id=block-003 flavour=paragraph -->
|
||||
To be deleted.
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
Updated paragraph.
|
||||
|
||||
<!-- block_id=block-004 flavour=paragraph -->
|
||||
New paragraph.
|
||||
`;
|
||||
const { patches } = diffMarkdown(oldMd, newMd);
|
||||
expect(patches).toEqual([
|
||||
{
|
||||
op: 'replace',
|
||||
id: 'block-002',
|
||||
content: 'Updated paragraph.',
|
||||
},
|
||||
{
|
||||
op: 'insert',
|
||||
index: 2,
|
||||
block: {
|
||||
id: 'block-004',
|
||||
type: 'paragraph',
|
||||
content: 'New paragraph.',
|
||||
},
|
||||
},
|
||||
{
|
||||
op: 'delete',
|
||||
id: 'block-003',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should diff consecutive block insertions', () => {
|
||||
// Two new blocks are inserted consecutively
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
First inserted paragraph.
|
||||
|
||||
<!-- block_id=block-003 flavour=paragraph -->
|
||||
Second inserted paragraph.
|
||||
`;
|
||||
const { patches } = diffMarkdown(oldMd, newMd);
|
||||
expect(patches).toEqual([
|
||||
{
|
||||
op: 'insert',
|
||||
index: 1,
|
||||
block: {
|
||||
id: 'block-002',
|
||||
type: 'paragraph',
|
||||
content: 'First inserted paragraph.',
|
||||
},
|
||||
},
|
||||
{
|
||||
op: 'insert',
|
||||
index: 2,
|
||||
block: {
|
||||
id: 'block-003',
|
||||
type: 'paragraph',
|
||||
content: 'Second inserted paragraph.',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should diff consecutive block deletions', () => {
|
||||
// Two blocks are deleted consecutively
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
First paragraph to be deleted.
|
||||
|
||||
<!-- block_id=block-003 flavour=paragraph -->
|
||||
Second paragraph to be deleted.
|
||||
`;
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
`;
|
||||
const { patches } = diffMarkdown(oldMd, newMd);
|
||||
expect(patches).toEqual([
|
||||
{
|
||||
op: 'delete',
|
||||
id: 'block-002',
|
||||
},
|
||||
{
|
||||
op: 'delete',
|
||||
id: 'block-003',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should diff deletion followed by insertion at the same position', () => {
|
||||
// A block is deleted and a new block is inserted at the end
|
||||
const oldMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-002 flavour=paragraph -->
|
||||
This paragraph will be deleted
|
||||
|
||||
<!-- block_id=block-003 flavour=paragraph -->
|
||||
HelloWorld
|
||||
`;
|
||||
|
||||
const newMd = `
|
||||
<!-- block_id=block-001 flavour=title -->
|
||||
# Title
|
||||
|
||||
<!-- block_id=block-003 flavour=paragraph -->
|
||||
HelloWorld
|
||||
|
||||
<!-- block_id=block-004 flavour=paragraph -->
|
||||
This is a new paragraph inserted after deletion.
|
||||
`;
|
||||
const { patches } = diffMarkdown(oldMd, newMd);
|
||||
expect(patches).toEqual([
|
||||
{
|
||||
op: 'insert',
|
||||
index: 2,
|
||||
block: {
|
||||
id: 'block-004',
|
||||
type: 'paragraph',
|
||||
content: 'This is a new paragraph inserted after deletion.',
|
||||
},
|
||||
},
|
||||
{
|
||||
op: 'delete',
|
||||
id: 'block-002',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
export const blockTagMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
|
||||
flavour: 'affine:page/affine:note/*',
|
||||
toMatch: () => false,
|
||||
fromMatch: o => {
|
||||
const block = o.node;
|
||||
const parent = o.parent;
|
||||
if (block.type === 'block' && parent?.node.flavour === 'affine:note') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
toBlockSnapshot: {},
|
||||
fromBlockSnapshot: {
|
||||
async enter(block, adapterContext) {
|
||||
adapterContext.walkerContext
|
||||
.openNode({
|
||||
type: 'html',
|
||||
value: `<!-- block_id=${block.node.id} flavour=${block.node.flavour} -->`,
|
||||
})
|
||||
.closeNode();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const BlockTagMarkdownAdapterExtension = BlockMarkdownAdapterExtension(
|
||||
blockTagMarkdownAdapterMatcher
|
||||
);
|
||||
@@ -77,6 +77,21 @@ import {
|
||||
} from './widgets/ai-panel/components';
|
||||
import { AIFinishTip } from './widgets/ai-panel/components/finish-tip';
|
||||
import { GeneratingPlaceholder } from './widgets/ai-panel/components/generating-placeholder';
|
||||
import {
|
||||
AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK,
|
||||
AffineBlockDiffWidgetForBlock,
|
||||
} from './widgets/block-diff/block';
|
||||
import { BlockDiffOptions } from './widgets/block-diff/options';
|
||||
import {
|
||||
AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE,
|
||||
AffineBlockDiffWidgetForPage,
|
||||
} from './widgets/block-diff/page';
|
||||
import {
|
||||
AFFINE_BLOCK_DIFF_PLAYGROUND,
|
||||
AFFINE_BLOCK_DIFF_PLAYGROUND_MODAL,
|
||||
BlockDiffPlayground,
|
||||
BlockDiffPlaygroundModal,
|
||||
} from './widgets/block-diff/playground';
|
||||
import {
|
||||
AFFINE_EDGELESS_COPILOT_WIDGET,
|
||||
EdgelessCopilotWidget,
|
||||
@@ -165,6 +180,12 @@ export function registerAIEffects() {
|
||||
customElements.define('chat-message-action', ChatMessageAction);
|
||||
customElements.define('chat-message-assistant', ChatMessageAssistant);
|
||||
customElements.define('chat-message-user', ChatMessageUser);
|
||||
customElements.define('ai-block-diff-options', BlockDiffOptions);
|
||||
customElements.define(AFFINE_BLOCK_DIFF_PLAYGROUND, BlockDiffPlayground);
|
||||
customElements.define(
|
||||
AFFINE_BLOCK_DIFF_PLAYGROUND_MODAL,
|
||||
BlockDiffPlaygroundModal
|
||||
);
|
||||
|
||||
customElements.define('tool-call-card', ToolCallCard);
|
||||
customElements.define('tool-result-card', ToolResultCard);
|
||||
@@ -174,6 +195,14 @@ export function registerAIEffects() {
|
||||
|
||||
customElements.define(AFFINE_AI_PANEL_WIDGET, AffineAIPanelWidget);
|
||||
customElements.define(AFFINE_EDGELESS_COPILOT_WIDGET, EdgelessCopilotWidget);
|
||||
customElements.define(
|
||||
AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK,
|
||||
AffineBlockDiffWidgetForBlock
|
||||
);
|
||||
customElements.define(
|
||||
AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE,
|
||||
AffineBlockDiffWidgetForPage
|
||||
);
|
||||
|
||||
customElements.define('edgeless-copilot-panel', EdgelessCopilotPanel);
|
||||
customElements.define(
|
||||
|
||||
435
packages/frontend/core/src/blocksuite/ai/services/block-diff.ts
Normal file
435
packages/frontend/core/src/blocksuite/ai/services/block-diff.ts
Normal file
@@ -0,0 +1,435 @@
|
||||
import { LifeCycleWatcher } from '@blocksuite/affine/std';
|
||||
import { Extension, type Store } from '@blocksuite/affine/store';
|
||||
import {
|
||||
BlockMarkdownAdapterMatcherIdentifier,
|
||||
MarkdownAdapter,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import {
|
||||
type Container,
|
||||
createIdentifier,
|
||||
type ServiceProvider,
|
||||
} from '@blocksuite/global/di';
|
||||
import { LiveData } from '@toeverything/infra';
|
||||
import type { Subscription } from 'rxjs';
|
||||
|
||||
import { blockTagMarkdownAdapterMatcher } from '../adapters/block-tag';
|
||||
import { applyPatchToDoc } from '../utils/apply-model/apply-patch-to-doc';
|
||||
import {
|
||||
generateRenderDiff,
|
||||
type RenderDiffs,
|
||||
} from '../utils/apply-model/generate-render-diff';
|
||||
|
||||
interface RejectMap {
|
||||
deletes: string[];
|
||||
inserts: string[];
|
||||
updates: string[];
|
||||
}
|
||||
|
||||
type AcceptDelete = {
|
||||
type: 'delete';
|
||||
payload: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
type AcceptUpdate = {
|
||||
type: 'update';
|
||||
payload: {
|
||||
id: string;
|
||||
content: string;
|
||||
};
|
||||
};
|
||||
|
||||
type AcceptInsert = {
|
||||
type: 'insert';
|
||||
payload: {
|
||||
from: string;
|
||||
offset: number;
|
||||
content: string;
|
||||
};
|
||||
};
|
||||
|
||||
type Accept = AcceptDelete | AcceptUpdate | AcceptInsert;
|
||||
|
||||
type RejectDelete = {
|
||||
type: 'delete';
|
||||
payload: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
type RejectUpdate = {
|
||||
type: 'update';
|
||||
payload: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
type RejectInsert = {
|
||||
type: 'insert';
|
||||
payload: {
|
||||
from: string;
|
||||
offset: number;
|
||||
};
|
||||
};
|
||||
|
||||
type Reject = RejectDelete | RejectUpdate | RejectInsert;
|
||||
|
||||
export interface BlockDiffProvider {
|
||||
diffMap$: LiveData<RenderDiffs>;
|
||||
rejects$: LiveData<RejectMap>;
|
||||
isBatchingApply: boolean;
|
||||
|
||||
/**
|
||||
* Set the original markdown
|
||||
* @param originalMarkdown - The original markdown
|
||||
*/
|
||||
setOriginalMarkdown(originalMarkdown: string): void;
|
||||
|
||||
/**
|
||||
* Set the changed markdown
|
||||
* @param changedMarkdown - The changed markdown
|
||||
*/
|
||||
setChangedMarkdown(changedMarkdown: string): void;
|
||||
|
||||
/**
|
||||
* Clear the diff map
|
||||
*/
|
||||
clearDiff(): void;
|
||||
|
||||
/**
|
||||
* Get the diff map
|
||||
*/
|
||||
getDiff(): RenderDiffs;
|
||||
|
||||
/**
|
||||
* Check if there is any diff
|
||||
*/
|
||||
hasDiff(): boolean;
|
||||
|
||||
/**
|
||||
* Accept all the diffs
|
||||
*/
|
||||
acceptAll(doc: Store): Promise<void>;
|
||||
|
||||
/**
|
||||
* Accept a diff
|
||||
*/
|
||||
accept(accept: Accept, doc: Store): Promise<void>;
|
||||
|
||||
/**
|
||||
* Reject all the diffs
|
||||
*/
|
||||
rejectAll(): void;
|
||||
|
||||
/**
|
||||
* Reject a diff
|
||||
*/
|
||||
reject(reject: Reject): void;
|
||||
|
||||
/**
|
||||
* Check if a diff is rejected
|
||||
*/
|
||||
isRejected(type: 'delete' | 'update' | 'insert', index: string): boolean;
|
||||
|
||||
/**
|
||||
* Get the total number of diffs
|
||||
*/
|
||||
getTotalDiffs(): number;
|
||||
|
||||
/**
|
||||
* Get the markdown from the doc
|
||||
* @param doc - The doc
|
||||
* @param provider - The provider
|
||||
*/
|
||||
getMarkdownFromDoc(doc: Store, provider: ServiceProvider): Promise<string>;
|
||||
|
||||
/**
|
||||
* Get the index of a block in the doc
|
||||
* @param doc - The doc
|
||||
* @param blockId - The id of the block
|
||||
*/
|
||||
getBlockIndexById(doc: Store, blockId: string): number;
|
||||
}
|
||||
|
||||
export const BlockDiffProvider = createIdentifier<BlockDiffProvider>(
|
||||
'AffineBlockDiffService'
|
||||
);
|
||||
|
||||
export class BlockDiffService extends Extension implements BlockDiffProvider {
|
||||
rejects$ = new LiveData<RejectMap>({
|
||||
deletes: [],
|
||||
inserts: [],
|
||||
updates: [],
|
||||
});
|
||||
|
||||
diffMap$ = new LiveData<RenderDiffs>({
|
||||
deletes: [],
|
||||
inserts: {},
|
||||
updates: {},
|
||||
});
|
||||
|
||||
private originalMarkdown: string | null = null;
|
||||
private changedMarkdown: string | null = null;
|
||||
|
||||
isBatchingApply = false;
|
||||
|
||||
static override setup(di: Container) {
|
||||
di.addImpl(BlockDiffProvider, BlockDiffService);
|
||||
}
|
||||
|
||||
getBlockIndexById(doc: Store, blockId: string): number {
|
||||
const notes = doc.getBlocksByFlavour('affine:note');
|
||||
if (notes.length === 0) return 0;
|
||||
const note = notes[0].model;
|
||||
return note.children.findIndex(child => child.id === blockId);
|
||||
}
|
||||
|
||||
hasDiff(): boolean {
|
||||
const { deletes, updates, inserts } = this.diffMap$.value;
|
||||
if (
|
||||
deletes.length > 0 ||
|
||||
Object.keys(updates).length > 0 ||
|
||||
Object.keys(inserts).length > 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setOriginalMarkdown(originalMarkdown: string) {
|
||||
this.originalMarkdown = originalMarkdown;
|
||||
this._refreshDiff();
|
||||
}
|
||||
|
||||
setChangedMarkdown(changedMarkdown: string) {
|
||||
this.changedMarkdown = changedMarkdown;
|
||||
this.clearRejects();
|
||||
this._refreshDiff();
|
||||
}
|
||||
|
||||
private _refreshDiff(): void {
|
||||
if (!this.originalMarkdown || !this.changedMarkdown) {
|
||||
this.clearDiff();
|
||||
return;
|
||||
}
|
||||
const diffMap = generateRenderDiff(
|
||||
this.originalMarkdown,
|
||||
this.changedMarkdown
|
||||
);
|
||||
this.diffMap$.next(diffMap);
|
||||
}
|
||||
|
||||
getDiff(): RenderDiffs {
|
||||
return this.diffMap$.value;
|
||||
}
|
||||
|
||||
clearDiff(): void {
|
||||
this.diffMap$.next({
|
||||
deletes: [],
|
||||
inserts: {},
|
||||
updates: {},
|
||||
});
|
||||
}
|
||||
|
||||
clearRejects(): void {
|
||||
this.rejects$.next({
|
||||
deletes: [],
|
||||
inserts: [],
|
||||
updates: [],
|
||||
});
|
||||
}
|
||||
|
||||
async acceptAll(doc: Store): Promise<void> {
|
||||
this.isBatchingApply = true;
|
||||
const { deletes, updates, inserts } = this.diffMap$.value;
|
||||
|
||||
try {
|
||||
for (const [id, content] of Object.entries(updates)) {
|
||||
await applyPatchToDoc(doc, [{ op: 'replace', id, content }]);
|
||||
}
|
||||
for (const [from, blocks] of Object.entries(inserts)) {
|
||||
let baseIndex = 0;
|
||||
if (from !== 'HEAD') {
|
||||
baseIndex = this.getBlockIndexById(doc, from) + 1;
|
||||
}
|
||||
for (const [offset, block] of blocks.entries()) {
|
||||
await applyPatchToDoc(doc, [
|
||||
{ op: 'insert', index: baseIndex + offset, block },
|
||||
]);
|
||||
}
|
||||
}
|
||||
for (const id of deletes) {
|
||||
await applyPatchToDoc(doc, [{ op: 'delete', id }]);
|
||||
}
|
||||
this.diffMap$.next({
|
||||
deletes: [],
|
||||
inserts: {},
|
||||
updates: {},
|
||||
});
|
||||
} finally {
|
||||
this.isBatchingApply = false;
|
||||
}
|
||||
}
|
||||
|
||||
async accept(accept: Accept, doc: Store) {
|
||||
const { type, payload } = accept;
|
||||
switch (type) {
|
||||
case 'delete': {
|
||||
await applyPatchToDoc(doc, [{ op: 'delete', id: payload.id }]);
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
await applyPatchToDoc(doc, [
|
||||
{ op: 'replace', id: payload.id, content: payload.content },
|
||||
]);
|
||||
break;
|
||||
}
|
||||
case 'insert': {
|
||||
const block = this.diffMap$.value.inserts[payload.from][payload.offset];
|
||||
let baseIndex = 0;
|
||||
if (payload.from !== 'HEAD') {
|
||||
baseIndex = this.getBlockIndexById(doc, payload.from) + 1;
|
||||
}
|
||||
await applyPatchToDoc(doc, [
|
||||
{ op: 'insert', index: baseIndex + payload.offset, block },
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rejectAll(): void {
|
||||
this.clearDiff();
|
||||
this.clearRejects();
|
||||
this.changedMarkdown = null;
|
||||
}
|
||||
|
||||
reject(reject: Reject): void {
|
||||
const rejects = this.rejects$.value;
|
||||
switch (reject.type) {
|
||||
case 'delete':
|
||||
this.rejects$.next({
|
||||
...rejects,
|
||||
deletes: [...rejects.deletes, reject.payload.id],
|
||||
});
|
||||
break;
|
||||
case 'update':
|
||||
this.rejects$.next({
|
||||
...rejects,
|
||||
updates: [...rejects.updates, reject.payload.id],
|
||||
});
|
||||
break;
|
||||
case 'insert':
|
||||
this.rejects$.next({
|
||||
...rejects,
|
||||
inserts: [
|
||||
...rejects.inserts,
|
||||
`${reject.payload.from}:${reject.payload.offset}`,
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
isRejected(type: 'delete' | 'update' | 'insert', index: string): boolean {
|
||||
const rejects = this.rejects$.value;
|
||||
if (type === 'delete') {
|
||||
return rejects.deletes.includes(index);
|
||||
}
|
||||
if (type === 'update') {
|
||||
return rejects.updates.includes(index);
|
||||
}
|
||||
if (type === 'insert') {
|
||||
return rejects.inserts.includes(index);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getTotalDiffs(): number {
|
||||
const rejects = this.rejects$.value;
|
||||
const { deletes, updates, inserts } = this.diffMap$.value;
|
||||
const insertCount = Object.values(inserts).reduce(
|
||||
(sum, arr) => sum + arr.length,
|
||||
0
|
||||
);
|
||||
const rejectDeleteCount = rejects.deletes.length;
|
||||
const rejectUpdateCount = rejects.updates.length;
|
||||
const rejectInsertCount = rejects.inserts.length;
|
||||
return (
|
||||
deletes.length +
|
||||
Object.keys(updates).length +
|
||||
insertCount -
|
||||
rejectDeleteCount -
|
||||
rejectUpdateCount -
|
||||
rejectInsertCount
|
||||
);
|
||||
}
|
||||
|
||||
getMarkdownFromDoc = async (doc: Store, provider: ServiceProvider) => {
|
||||
const job = doc.getTransformer();
|
||||
const snapshot = job.docToSnapshot(doc);
|
||||
const adapter = new MarkdownAdapter(job, provider);
|
||||
if (!snapshot) {
|
||||
return 'Failed to get markdown from doc';
|
||||
}
|
||||
// FIXME: reverse the block matchers to make the block tag adapter the first one
|
||||
adapter.blockMatchers.reverse();
|
||||
const markdown = await adapter.fromDocSnapshot({
|
||||
snapshot,
|
||||
assets: job.assetsManager,
|
||||
});
|
||||
return markdown.file;
|
||||
};
|
||||
}
|
||||
|
||||
export class BlockDiffWatcher extends LifeCycleWatcher {
|
||||
static override key = 'block-diff-watcher';
|
||||
|
||||
private _blockUpdatedSubscription: Subscription | null = null;
|
||||
|
||||
private _provider: ServiceProvider | null = null;
|
||||
|
||||
override created() {
|
||||
super.created();
|
||||
const cloned = this.std.store.provider.container.clone();
|
||||
cloned.addImpl(
|
||||
BlockMarkdownAdapterMatcherIdentifier,
|
||||
blockTagMarkdownAdapterMatcher
|
||||
);
|
||||
this._provider = cloned.provider();
|
||||
}
|
||||
|
||||
private readonly _refreshOriginalMarkdown = async () => {
|
||||
const diffService = this.std.get(BlockDiffProvider);
|
||||
if (
|
||||
!diffService.hasDiff() ||
|
||||
!this._provider ||
|
||||
diffService.isBatchingApply
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const markdown = await diffService.getMarkdownFromDoc(
|
||||
this.std.store,
|
||||
this._provider
|
||||
);
|
||||
if (markdown) {
|
||||
diffService.setOriginalMarkdown(markdown);
|
||||
}
|
||||
};
|
||||
|
||||
override mounted() {
|
||||
super.mounted();
|
||||
this._blockUpdatedSubscription =
|
||||
this.std.store.slots.blockUpdated.subscribe(() => {
|
||||
this._refreshOriginalMarkdown().catch(err => {
|
||||
console.error('Failed to refresh original markdown', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
super.unmounted();
|
||||
this._blockUpdatedSubscription?.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { Store } from '@blocksuite/store';
|
||||
|
||||
import { insertFromMarkdown, replaceFromMarkdown } from '../../../utils';
|
||||
import type { PatchOp } from './markdown-diff';
|
||||
|
||||
/**
|
||||
* Apply a list of PatchOp to the page doc (children of the first note block)
|
||||
* @param doc The page document Store
|
||||
* @param patch Array of PatchOp
|
||||
*/
|
||||
export async function applyPatchToDoc(
|
||||
doc: Store,
|
||||
patch: PatchOp[]
|
||||
): Promise<void> {
|
||||
// Get all note blocks
|
||||
const notes = doc.getBlocksByFlavour('affine:note');
|
||||
if (notes.length === 0) return;
|
||||
// Only handle the first note block
|
||||
const note = notes[0].model;
|
||||
|
||||
// Build a map from block_id to BlockModel for quick lookup
|
||||
const blockIdMap = new Map<string, any>();
|
||||
note.children.forEach(child => {
|
||||
blockIdMap.set(child.id, child);
|
||||
});
|
||||
|
||||
for (const op of patch) {
|
||||
if (op.op === 'delete') {
|
||||
// Delete block
|
||||
doc.deleteBlock(op.id);
|
||||
} else if (op.op === 'replace') {
|
||||
const oldBlock = blockIdMap.get(op.id);
|
||||
if (!oldBlock) continue;
|
||||
const parentId = note.id;
|
||||
const index = note.children.findIndex(child => child.id === op.id);
|
||||
if (index === -1) continue;
|
||||
|
||||
await replaceFromMarkdown(
|
||||
undefined,
|
||||
op.content,
|
||||
doc,
|
||||
parentId,
|
||||
index,
|
||||
op.id
|
||||
);
|
||||
} else if (op.op === 'insert') {
|
||||
// Insert new block
|
||||
const parentId = note.id;
|
||||
const index = op.index;
|
||||
await insertFromMarkdown(
|
||||
undefined,
|
||||
op.block.content,
|
||||
doc,
|
||||
parentId,
|
||||
index,
|
||||
op.block.id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { type Block, diffMarkdown } from './markdown-diff';
|
||||
|
||||
export interface RenderDiffs {
|
||||
deletes: string[];
|
||||
inserts: Record<string, Block[]>;
|
||||
updates: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example:
|
||||
*
|
||||
* Old markdown:
|
||||
* ```md
|
||||
* <!-- block_id=001 flavour=paragraph -->
|
||||
* This is the first paragraph
|
||||
*
|
||||
* <!-- block_id=002 flavour=paragraph -->
|
||||
* This is the second paragraph
|
||||
*
|
||||
* <!-- block_id=003 flavour=paragraph -->
|
||||
* This is the third paragraph
|
||||
*
|
||||
* <!-- block_id=004 flavour=paragraph -->
|
||||
* This is the fourth paragraph
|
||||
* ```
|
||||
*
|
||||
* New markdown:
|
||||
* ```md
|
||||
* <!-- block_id=001 flavour=paragraph -->
|
||||
* This is the first paragraph
|
||||
*
|
||||
* <!-- block_id=003 flavour=paragraph -->
|
||||
* This is the 3rd paragraph
|
||||
*
|
||||
* <!-- block_id=005 flavour=paragraph -->
|
||||
* New inserted paragraph 1
|
||||
*
|
||||
* <!-- block_id=006 flavour=paragraph -->
|
||||
* New inserted paragraph 2
|
||||
* ```
|
||||
*
|
||||
* The generated patches:
|
||||
* ```js
|
||||
* [
|
||||
* { op: 'insert', index: 2, block: { id: '005', ... } },
|
||||
* { op: 'insert', index: 3, bthirdlock: { id: '006', ... } },
|
||||
* { op: 'update', id: '003', content: 'This is the 3rd paragraph' },
|
||||
* { op: 'delete', id: '002' },
|
||||
* { op: 'delete', id: '004' }
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* UI expected:
|
||||
* ```
|
||||
* This is the first paragraph
|
||||
* [DELETE DIFF] This is the second paragraph
|
||||
* This is the third paragraph
|
||||
* [DELETE DIFF] This is the fourth paragraph
|
||||
* [INSERT DIFF] New inserted paragraph 1
|
||||
* [INSERT DIFF] New inserted paragraph 2
|
||||
* ```
|
||||
*
|
||||
* The resulting diffMap:
|
||||
* ```js
|
||||
* {
|
||||
* deletes: ['002', '004'],
|
||||
* inserts: { 3: [block_005, block_006] },
|
||||
* updates: {}
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function generateRenderDiff(
|
||||
originalMarkdown: string,
|
||||
changedMarkdown: string
|
||||
) {
|
||||
const { patches, oldBlocks } = diffMarkdown(
|
||||
originalMarkdown,
|
||||
changedMarkdown
|
||||
);
|
||||
|
||||
const diffMap: RenderDiffs = {
|
||||
deletes: [],
|
||||
inserts: {},
|
||||
updates: {},
|
||||
};
|
||||
|
||||
const indexToBlockId: Record<number, string> = {};
|
||||
oldBlocks.forEach((block, idx) => {
|
||||
indexToBlockId[idx] = block.id;
|
||||
});
|
||||
|
||||
function getPrevBlock(index: number) {
|
||||
let start = index - 1;
|
||||
while (!indexToBlockId[start] && start >= 0) {
|
||||
start--;
|
||||
}
|
||||
return indexToBlockId[start] || 'HEAD';
|
||||
}
|
||||
|
||||
const insertGroups: Record<string, Block[]> = {};
|
||||
let lastInsertKey: string | null = null;
|
||||
let lastInsertIndex: number | null = null;
|
||||
|
||||
for (const patch of patches) {
|
||||
switch (patch.op) {
|
||||
case 'delete':
|
||||
diffMap.deletes.push(patch.id);
|
||||
break;
|
||||
case 'insert': {
|
||||
const prevBlockId = getPrevBlock(patch.index);
|
||||
if (
|
||||
lastInsertKey !== null &&
|
||||
lastInsertIndex !== null &&
|
||||
patch.index === lastInsertIndex + 1
|
||||
) {
|
||||
insertGroups[lastInsertKey].push(patch.block);
|
||||
} else {
|
||||
insertGroups[prevBlockId] = [patch.block];
|
||||
lastInsertKey = prevBlockId;
|
||||
}
|
||||
lastInsertIndex = patch.index;
|
||||
break;
|
||||
}
|
||||
case 'replace':
|
||||
diffMap.updates[patch.id] = patch.content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
diffMap.inserts = insertGroups;
|
||||
|
||||
return diffMap;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
export type Block = {
|
||||
id: string;
|
||||
type: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type PatchOp =
|
||||
| { op: 'replace'; id: string; content: string }
|
||||
| { op: 'delete'; id: string }
|
||||
| { op: 'insert'; index: number; block: Block };
|
||||
|
||||
const BLOCK_MATCH_REGEXP = /^\s*<!--\s*block_id=(.*?)\s+flavour=(.*?)\s*-->/;
|
||||
|
||||
export function parseMarkdownToBlocks(markdown: string): Block[] {
|
||||
const lines = markdown.split(/\r?\n/);
|
||||
const blocks: Block[] = [];
|
||||
let currentBlockId: string | null = null;
|
||||
let currentType: string | null = null;
|
||||
let currentContent: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(BLOCK_MATCH_REGEXP);
|
||||
if (match) {
|
||||
// If there is a block being collected, push it into blocks first
|
||||
if (currentBlockId && currentType) {
|
||||
blocks.push({
|
||||
id: currentBlockId,
|
||||
type: currentType,
|
||||
content: currentContent.join('\n').trim(),
|
||||
});
|
||||
}
|
||||
// Start a new block
|
||||
currentBlockId = match[1];
|
||||
currentType = match[2];
|
||||
currentContent = [];
|
||||
} else {
|
||||
// Collect content
|
||||
if (currentBlockId && currentType) {
|
||||
currentContent.push(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Collect the last block
|
||||
if (currentBlockId && currentType) {
|
||||
blocks.push({
|
||||
id: currentBlockId,
|
||||
type: currentType,
|
||||
content: currentContent.join('\n').trim(),
|
||||
});
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function diffBlockLists(oldBlocks: Block[], newBlocks: Block[]): PatchOp[] {
|
||||
const patch: PatchOp[] = [];
|
||||
const oldMap = new Map<string, { block: Block; index: number }>();
|
||||
oldBlocks.forEach((b, i) => oldMap.set(b.id, { block: b, index: i }));
|
||||
const newMap = new Map<string, { block: Block; index: number }>();
|
||||
newBlocks.forEach((b, i) => newMap.set(b.id, { block: b, index: i }));
|
||||
|
||||
// Mark old blocks that have been handled
|
||||
const handledOld = new Set<string>();
|
||||
|
||||
// First process newBlocks in order
|
||||
newBlocks.forEach((newBlock, newIdx) => {
|
||||
const old = oldMap.get(newBlock.id);
|
||||
if (old) {
|
||||
handledOld.add(newBlock.id);
|
||||
if (old.block.content !== newBlock.content) {
|
||||
patch.push({
|
||||
op: 'replace',
|
||||
id: newBlock.id,
|
||||
content: newBlock.content,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
patch.push({
|
||||
op: 'insert',
|
||||
index: newIdx,
|
||||
block: {
|
||||
id: newBlock.id,
|
||||
type: newBlock.type,
|
||||
content: newBlock.content,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Then process deleted oldBlocks
|
||||
oldBlocks.forEach(oldBlock => {
|
||||
if (!newMap.has(oldBlock.id)) {
|
||||
patch.push({
|
||||
op: 'delete',
|
||||
id: oldBlock.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
export function diffMarkdown(oldMarkdown: string, newMarkdown: string) {
|
||||
const oldBlocks = parseMarkdownToBlocks(oldMarkdown);
|
||||
const newBlocks = parseMarkdownToBlocks(newMarkdown);
|
||||
|
||||
const patches: PatchOp[] = diffBlockLists(oldBlocks, newBlocks);
|
||||
|
||||
return { patches, newBlocks, oldBlocks };
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/affine/std';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { css, html, nothing, type TemplateResult } from 'lit';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import { BlockDiffProvider } from '../../services/block-diff';
|
||||
import type { Block } from '../../utils/apply-model/markdown-diff';
|
||||
import { blockDiffWidgetForPage } from './page';
|
||||
|
||||
export const AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK =
|
||||
'affine-block-diff-widget-for-block';
|
||||
|
||||
export class AffineBlockDiffWidgetForBlock extends WidgetComponent {
|
||||
static override styles = css`
|
||||
.ai-block-diff {
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
pointer-events: none;
|
||||
background-color: ${unsafeCSSVarV2('aI/applyTextHighlightBackground')};
|
||||
padding: 8px 0px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ai-block-diff.delete {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
private _setDeletedStyle(blockId: string) {
|
||||
const deleted = document.querySelector<HTMLDivElement>(
|
||||
`[data-block-id="${blockId}"]`
|
||||
);
|
||||
if (!deleted) {
|
||||
return;
|
||||
}
|
||||
deleted.classList.add('ai-block-diff-deleted');
|
||||
}
|
||||
|
||||
private _clearDeletedStyle(blockId: string) {
|
||||
const block = document.querySelector<HTMLDivElement>(
|
||||
`[data-block-id="${blockId}"]`
|
||||
);
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
block.classList.remove('ai-block-diff-deleted');
|
||||
}
|
||||
|
||||
private _renderDelete(blockId: string) {
|
||||
if (this.diffService.isRejected('delete', blockId)) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
this._setDeletedStyle(blockId);
|
||||
|
||||
return html`<div
|
||||
class="ai-block-diff delete"
|
||||
data-diff-id=${`delete-${blockId}`}
|
||||
>
|
||||
<ai-block-diff-options
|
||||
.onAccept=${() =>
|
||||
this.diffService.accept(
|
||||
{ type: 'delete', payload: { id: blockId } },
|
||||
this.std.store
|
||||
)}
|
||||
.onReject=${() =>
|
||||
this.diffService.reject({ type: 'delete', payload: { id: blockId } })}
|
||||
></ai-block-diff-options>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderInsert(from: string, blocks: Block[]) {
|
||||
return blocks
|
||||
.map((block, offset) => {
|
||||
return this.diffService.isRejected('insert', `${from}:${offset}`)
|
||||
? null
|
||||
: html`<div
|
||||
class="ai-block-diff insert"
|
||||
data-diff-id=${`insert-${block.id}-${offset}`}
|
||||
>
|
||||
<chat-content-rich-text
|
||||
.text=${block.content}
|
||||
.host=${this.host}
|
||||
.state="finished"
|
||||
.extensions=${this.userExtensions}
|
||||
></chat-content-rich-text>
|
||||
<ai-block-diff-options
|
||||
.onAccept=${() =>
|
||||
this.diffService.accept(
|
||||
{
|
||||
type: 'insert',
|
||||
payload: { from, offset, content: block.content },
|
||||
},
|
||||
this.std.store
|
||||
)}
|
||||
.onReject=${() =>
|
||||
this.diffService.reject({
|
||||
type: 'insert',
|
||||
payload: { from, offset },
|
||||
})}
|
||||
></ai-block-diff-options>
|
||||
</div>`;
|
||||
})
|
||||
.filter(Boolean) as TemplateResult[];
|
||||
}
|
||||
|
||||
private _renderUpdate(blockId: string, content: string) {
|
||||
if (this.diffService.isRejected('update', blockId)) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="ai-block-diff update" data-diff-id=${`update-${blockId}`}>
|
||||
<chat-content-rich-text
|
||||
.text=${content}
|
||||
.host=${this.host}
|
||||
.state="finished"
|
||||
.extensions=${this.userExtensions}
|
||||
></chat-content-rich-text>
|
||||
<ai-block-diff-options
|
||||
.onAccept=${() =>
|
||||
this.diffService.accept(
|
||||
{
|
||||
type: 'update',
|
||||
payload: { id: blockId, content },
|
||||
},
|
||||
this.std.store
|
||||
)}
|
||||
.onReject=${() =>
|
||||
this.diffService.reject({
|
||||
type: 'update',
|
||||
payload: { id: blockId },
|
||||
})}
|
||||
></ai-block-diff-options>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
get diffService() {
|
||||
return this.std.get(BlockDiffProvider);
|
||||
}
|
||||
|
||||
get userExtensions() {
|
||||
return this.std.userExtensions.filter(
|
||||
extension => extension !== blockDiffWidgetForPage
|
||||
);
|
||||
}
|
||||
|
||||
get blockIndex(): number {
|
||||
const attached = this.block?.blockId;
|
||||
if (!attached) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return this.diffService.getBlockIndexById(this.std.store, attached);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const attached = this.block?.blockId;
|
||||
const service = this.std.get(BlockDiffProvider);
|
||||
const blockIndex = this.blockIndex;
|
||||
|
||||
if (attached) {
|
||||
this._clearDeletedStyle(attached);
|
||||
}
|
||||
|
||||
if (!attached || blockIndex < 0 || !service.hasDiff()) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const { deletes, inserts, updates } = service.getDiff();
|
||||
|
||||
let deleteDiff: TemplateResult | symbol = nothing;
|
||||
let updateDiff: TemplateResult | symbol = nothing;
|
||||
let insertDiff: TemplateResult[] | symbol = nothing;
|
||||
|
||||
if (deletes.includes(attached)) {
|
||||
deleteDiff = this._renderDelete(attached);
|
||||
}
|
||||
|
||||
if (updates[attached]) {
|
||||
updateDiff = this._renderUpdate(attached, updates[attached]);
|
||||
}
|
||||
|
||||
if (inserts[attached]) {
|
||||
const blocks = inserts[attached];
|
||||
insertDiff = this._renderInsert(attached, blocks);
|
||||
}
|
||||
|
||||
return html`${deleteDiff} ${updateDiff} ${insertDiff}`;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.disposables.add(
|
||||
this.diffService.diffMap$.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
this.disposables.add(
|
||||
this.diffService.rejects$.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const blockDiffWidgetForBlock = WidgetViewExtension(
|
||||
'affine:note/*',
|
||||
AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK,
|
||||
literal`${unsafeStatic(AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK)}`
|
||||
);
|
||||
@@ -0,0 +1,84 @@
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
import { CloseIcon, DoneIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { PatchOp } from '../../utils/apply-model/markdown-diff';
|
||||
|
||||
export class BlockDiffOptions extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.ai-block-diff-option {
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
box-shadow: ${unsafeCSSVar('shadow1')};
|
||||
display: flex;
|
||||
background-color: ${unsafeCSSVarV2('layer/background/overlayPanel')};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ai-block-diff-option.accept {
|
||||
color: ${unsafeCSSVarV2('icon/activated')};
|
||||
}
|
||||
|
||||
.ai-block-diff-option.reject {
|
||||
color: ${unsafeCSSVarV2('icon/secondary')};
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onAccept!: (op: PatchOp) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor op!: PatchOp;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onReject!: (op: PatchOp) => void;
|
||||
|
||||
private readonly _handleAcceptClick = () => {
|
||||
console.log('accept', this.op);
|
||||
this.onAccept(this.op);
|
||||
};
|
||||
|
||||
private readonly _handleRejectClick = () => {
|
||||
console.log('reject', this.op);
|
||||
this.onReject(this.op);
|
||||
};
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div
|
||||
class="ai-block-diff-option accept"
|
||||
@click=${this._handleAcceptClick}
|
||||
>
|
||||
${DoneIcon()}
|
||||
</div>
|
||||
<div
|
||||
class="ai-block-diff-option reject"
|
||||
@click=${this._handleRejectClick}
|
||||
>
|
||||
${CloseIcon()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'ai-block-diff-options': BlockDiffOptions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/affine/std';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
ArrowUpSmallIcon,
|
||||
CloseIcon,
|
||||
DoneIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import { BlockDiffProvider } from '../../services/block-diff';
|
||||
|
||||
export const AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE =
|
||||
'affine-block-diff-widget-for-page';
|
||||
|
||||
export class AffineBlockDiffWidgetForPage extends WidgetComponent {
|
||||
static override styles = css`
|
||||
.ai-block-diff-scroller-container {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${unsafeCSSVarV2('layer/background/overlayPanel')};
|
||||
box-shadow: ${unsafeCSSVar('shadow1')};
|
||||
border-radius: 8px;
|
||||
width: 350px;
|
||||
padding: 8px 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ai-block-diff-scroller {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ai-block-diff-scroller span {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.ai-block-diff-scroller svg {
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
|
||||
.ai-block-diff-all-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor currentIndex = 0;
|
||||
|
||||
_handleScroll(dir: 'prev' | 'next') {
|
||||
const total = this.diffService.getTotalDiffs();
|
||||
|
||||
const diffWidgets = Array.from(
|
||||
this.std.host.querySelectorAll('affine-block-diff-widget-for-block')
|
||||
);
|
||||
const diffs = diffWidgets.reduce<Element[]>((acc, widget) => {
|
||||
const aiDiffs = widget.shadowRoot?.querySelectorAll('.ai-block-diff');
|
||||
if (aiDiffs && aiDiffs.length > 0) {
|
||||
acc.push(...aiDiffs);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
if (dir === 'prev') {
|
||||
this.currentIndex = Math.max(0, this.currentIndex - 1);
|
||||
} else {
|
||||
this.currentIndex = Math.min(total - 1, this.currentIndex + 1);
|
||||
}
|
||||
diffs[this.currentIndex].scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
get diffService() {
|
||||
return this.std.get(BlockDiffProvider);
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (!this.diffService.hasDiff()) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const total = this.diffService.getTotalDiffs();
|
||||
|
||||
return total === 0
|
||||
? null
|
||||
: html`
|
||||
<div class="ai-block-diff-scroller-container">
|
||||
<div class="ai-block-diff-scroller">
|
||||
<span @click=${() => this._handleScroll('next')}
|
||||
>${ArrowDownSmallIcon()}</span
|
||||
>
|
||||
<span class="ai-block-diff-scroller-current"
|
||||
>${Math.min(this.currentIndex + 1, total)}</span
|
||||
>
|
||||
<span>/</span>
|
||||
<span class="ai-block-diff-scroller-total">${total}</span>
|
||||
<span @click=${() => this._handleScroll('prev')}
|
||||
>${ArrowUpSmallIcon()}</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="ai-block-diff-all-option"
|
||||
@click=${() => this.diffService.rejectAll()}
|
||||
>
|
||||
${CloseIcon({
|
||||
style: `color: ${unsafeCSSVarV2('icon/secondary')}`,
|
||||
})}
|
||||
Reject all
|
||||
</div>
|
||||
<div
|
||||
class="ai-block-diff-all-option"
|
||||
@click=${() => this.diffService.acceptAll(this.std.store)}
|
||||
>
|
||||
${DoneIcon({
|
||||
style: `color: ${unsafeCSSVarV2('icon/activated')}`,
|
||||
})}
|
||||
Accept all
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.disposables.add(
|
||||
this.diffService.diffMap$.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.add(
|
||||
this.diffService.rejects$.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const blockDiffWidgetForPage = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE,
|
||||
literal`${unsafeStatic(AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE)}`
|
||||
);
|
||||
@@ -0,0 +1,241 @@
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/affine/std';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import { createLitPortal } from '@blocksuite/affine-components/portal';
|
||||
import { BlockMarkdownAdapterMatcherIdentifier } from '@blocksuite/affine-shared/adapters';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import { blockTagMarkdownAdapterMatcher } from '../../adapters/block-tag';
|
||||
import { BlockDiffProvider } from '../../services/block-diff';
|
||||
|
||||
export const AFFINE_BLOCK_DIFF_PLAYGROUND = 'affine-block-diff-playground';
|
||||
export const AFFINE_BLOCK_DIFF_PLAYGROUND_MODAL =
|
||||
'affine-block-diff-playground-modal';
|
||||
|
||||
export class BlockDiffPlaygroundModal extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
.playground-modal {
|
||||
z-index: 10000;
|
||||
width: 600px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.18);
|
||||
padding: 24px 20px 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.playground-textarea {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
resize: vertical;
|
||||
font-size: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.playground-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.playground-btn {
|
||||
padding: 6px 18px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.playground-btn.primary {
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
}
|
||||
.playground-btn.primary:hover {
|
||||
background: #1565c0;
|
||||
}
|
||||
.playground-btn:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
`;
|
||||
|
||||
@state()
|
||||
private accessor markdown = '';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor diffService!: BlockDiffProvider;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor store!: Store;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onClose!: () => void;
|
||||
|
||||
private readonly handleInput = (e: Event) => {
|
||||
this.markdown = (e.target as HTMLTextAreaElement).value;
|
||||
};
|
||||
|
||||
private readonly handleClear = () => {
|
||||
this.markdown = '';
|
||||
this.diffService.setChangedMarkdown('');
|
||||
};
|
||||
|
||||
private async getOriginalMarkdown() {
|
||||
const cloned = this.store.provider.container.clone();
|
||||
cloned.addImpl(
|
||||
BlockMarkdownAdapterMatcherIdentifier,
|
||||
blockTagMarkdownAdapterMatcher
|
||||
);
|
||||
const provider = cloned.provider();
|
||||
const markdown = await this.diffService.getMarkdownFromDoc(
|
||||
this.store,
|
||||
provider
|
||||
);
|
||||
return markdown;
|
||||
}
|
||||
|
||||
private readonly handleConfirm = async () => {
|
||||
const originalMarkdown = await this.getOriginalMarkdown();
|
||||
this.diffService.setOriginalMarkdown(originalMarkdown);
|
||||
this.diffService.setChangedMarkdown(this.markdown);
|
||||
this.onClose();
|
||||
};
|
||||
|
||||
private readonly handleInsertCurrentMarkdown = async () => {
|
||||
this.markdown = await this.getOriginalMarkdown();
|
||||
};
|
||||
|
||||
private readonly stopPropagation = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="playground-modal">
|
||||
<div class="playground-modal-title">Block Diff Playground</div>
|
||||
<div class="playground-modal-content">
|
||||
<textarea
|
||||
class="playground-textarea"
|
||||
placeholder="Please input the markdown you want to apply."
|
||||
.value=${this.markdown}
|
||||
@input=${this.handleInput}
|
||||
@focus=${(e: FocusEvent) => e.stopPropagation()}
|
||||
@pointerdown=${this.stopPropagation}
|
||||
@mousedown=${this.stopPropagation}
|
||||
@mouseup=${this.stopPropagation}
|
||||
@click=${this.stopPropagation}
|
||||
@keydown=${this.stopPropagation}
|
||||
@keyup=${this.stopPropagation}
|
||||
@copy=${this.stopPropagation}
|
||||
@cut=${this.stopPropagation}
|
||||
@paste=${this.stopPropagation}
|
||||
@blur=${(e: FocusEvent) => e.stopPropagation()}
|
||||
></textarea>
|
||||
<div class="playground-actions">
|
||||
<button
|
||||
class="playground-btn"
|
||||
@click=${this.handleInsertCurrentMarkdown}
|
||||
>
|
||||
Insert Current Doc MD
|
||||
</button>
|
||||
<button class="playground-btn" @click=${this.handleClear}>
|
||||
Clear
|
||||
</button>
|
||||
<button class="playground-btn primary" @click=${this.handleConfirm}>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export class BlockDiffPlayground extends WidgetComponent {
|
||||
static override styles = css`
|
||||
.playground-fab {
|
||||
position: fixed;
|
||||
right: 32px;
|
||||
bottom: 32px;
|
||||
z-index: 9999;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.playground-fab:hover {
|
||||
background: #1565c0;
|
||||
}
|
||||
`;
|
||||
|
||||
@query('.playground-fab')
|
||||
accessor fab!: HTMLDivElement;
|
||||
|
||||
private _abortController: AbortController | null = null;
|
||||
|
||||
private get diffService() {
|
||||
return this.std.get(BlockDiffProvider);
|
||||
}
|
||||
|
||||
private readonly handleOpen = () => {
|
||||
this._abortController?.abort();
|
||||
this._abortController = new AbortController();
|
||||
|
||||
createLitPortal({
|
||||
template: html`
|
||||
<affine-block-diff-playground-modal
|
||||
.diffService=${this.diffService}
|
||||
.store=${this.std.store}
|
||||
.onClose=${this.handleClose}
|
||||
></affine-block-diff-playground-modal>
|
||||
`,
|
||||
container: this.host,
|
||||
computePosition: {
|
||||
referenceElement: this.fab,
|
||||
placement: 'top-end',
|
||||
},
|
||||
closeOnClickAway: true,
|
||||
abortController: this._abortController,
|
||||
});
|
||||
};
|
||||
|
||||
private readonly handleClose = () => {
|
||||
this._abortController?.abort();
|
||||
};
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div>
|
||||
<div
|
||||
class="playground-fab"
|
||||
@click=${this.handleOpen}
|
||||
title="Block Diff Playground"
|
||||
>
|
||||
🧪
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export const blockDiffPlayground = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_BLOCK_DIFF_PLAYGROUND,
|
||||
literal`${unsafeStatic(AFFINE_BLOCK_DIFF_PLAYGROUND)}`
|
||||
);
|
||||
@@ -131,7 +131,8 @@ export async function insertFromMarkdown(
|
||||
markdown: string,
|
||||
doc: Store,
|
||||
parent?: string,
|
||||
index?: number
|
||||
index?: number,
|
||||
id?: string
|
||||
) {
|
||||
const { snapshot, transformer } = await markdownToSnapshot(
|
||||
markdown,
|
||||
@@ -144,6 +145,9 @@ export async function insertFromMarkdown(
|
||||
const models: BlockModel[] = [];
|
||||
for (let i = 0; i < snapshots.length; i++) {
|
||||
const blockSnapshot = snapshots[i];
|
||||
if (snapshots.length === 1 && id) {
|
||||
blockSnapshot.id = id;
|
||||
}
|
||||
const model = await transformer.snapshotToBlock(
|
||||
blockSnapshot,
|
||||
doc,
|
||||
@@ -158,6 +162,27 @@ export async function insertFromMarkdown(
|
||||
return models;
|
||||
}
|
||||
|
||||
export async function replaceFromMarkdown(
|
||||
host: EditorHost | undefined,
|
||||
markdown: string,
|
||||
doc: Store,
|
||||
parent: string,
|
||||
index: number,
|
||||
id: string
|
||||
) {
|
||||
doc.deleteBlock(id);
|
||||
const { snapshot, transformer } = await markdownToSnapshot(
|
||||
markdown,
|
||||
doc,
|
||||
host
|
||||
);
|
||||
|
||||
const snapshots = snapshot?.content.flatMap(x => x.children) ?? [];
|
||||
const blockSnapshot = snapshots[0];
|
||||
blockSnapshot.id = id;
|
||||
await transformer.snapshotToBlock(blockSnapshot, doc, parent, index);
|
||||
}
|
||||
|
||||
export async function markDownToDoc(
|
||||
provider: ServiceProvider,
|
||||
schema: Schema,
|
||||
|
||||
@@ -19,6 +19,13 @@ import { BlockFlavourIdentifier } from '@blocksuite/affine/std';
|
||||
import { FrameworkProvider } from '@toeverything/infra';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
BlockDiffService,
|
||||
BlockDiffWatcher,
|
||||
} from '../../ai/services/block-diff';
|
||||
import { blockDiffWidgetForBlock } from '../../ai/widgets/block-diff/block';
|
||||
import { blockDiffWidgetForPage } from '../../ai/widgets/block-diff/page';
|
||||
import { blockDiffPlayground } from '../../ai/widgets/block-diff/playground';
|
||||
import { EdgelessClipboardAIChatConfig } from './edgeless-clipboard';
|
||||
|
||||
const optionsSchema = z.object({
|
||||
@@ -50,6 +57,7 @@ export class AIViewExtension extends ViewExtensionProvider<AIViewOptions> {
|
||||
config: imageToolbarAIEntryConfig(),
|
||||
})
|
||||
);
|
||||
|
||||
if (context.scope === 'edgeless' || context.scope === 'page') {
|
||||
context.register([
|
||||
aiPanelWidget,
|
||||
@@ -73,7 +81,17 @@ export class AIViewExtension extends ViewExtensionProvider<AIViewOptions> {
|
||||
]);
|
||||
}
|
||||
if (context.scope === 'page') {
|
||||
context.register(getAIPageRootWatcher(framework));
|
||||
context.register([
|
||||
blockDiffWidgetForPage,
|
||||
blockDiffWidgetForBlock,
|
||||
getAIPageRootWatcher(framework),
|
||||
BlockDiffService,
|
||||
BlockDiffWatcher,
|
||||
]);
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
context.register([blockDiffPlayground]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
{ "path": "../../common/nbstore" },
|
||||
{ "path": "../track" },
|
||||
{ "path": "../../../blocksuite/affine/all" },
|
||||
{ "path": "../../../blocksuite/affine/shared" },
|
||||
{ "path": "../../../blocksuite/framework/std" },
|
||||
{ "path": "../../common/infra" }
|
||||
{ "path": "../../common/infra" },
|
||||
{ "path": "../../../blocksuite/affine/ext-loader" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"@blocksuite/affine": "workspace:*",
|
||||
"@blocksuite/integration-test": "workspace:*",
|
||||
"@playwright/test": "=1.52.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.16",
|
||||
"json-stable-stringify": "^1.2.1",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user