From 0e8fa4fd48c2ea71d38dcdc224e74654ea402273 Mon Sep 17 00:00:00 2001 From: Yifeng Wang Date: Thu, 1 Feb 2024 02:31:25 +0800 Subject: [PATCH] docs: add blog setup (#6170) --- packages/docs/.vitepress/config.ts | 2 +- .../theme/components/blog-list-layout.vue | 77 +++++++++++++++++++ .../theme/components/blog-post-meta.vue | 35 +++++++++ .../theme/{ => components}/code-sandbox.vue | 0 .../theme/{ => components}/logo.vue | 0 .../theme/{ => components}/playground.vue | 0 .../theme/composables/posts.data.ts | 44 +++++++++++ .../.vitepress/theme/composables/use-posts.ts | 18 +++++ packages/docs/.vitepress/theme/index.ts | 10 ++- packages/docs/blog/crdt-native-data-flow.md | 44 ++++++++++- packages/docs/blog/document-centric.md | 9 ++- packages/docs/blog/index.md | 6 +- packages/docs/package.json | 1 + packages/docs/tsconfig.json | 4 + pnpm-lock.yaml | 9 ++- 15 files changed, 245 insertions(+), 14 deletions(-) create mode 100644 packages/docs/.vitepress/theme/components/blog-list-layout.vue create mode 100644 packages/docs/.vitepress/theme/components/blog-post-meta.vue rename packages/docs/.vitepress/theme/{ => components}/code-sandbox.vue (100%) rename packages/docs/.vitepress/theme/{ => components}/logo.vue (100%) rename packages/docs/.vitepress/theme/{ => components}/playground.vue (100%) create mode 100644 packages/docs/.vitepress/theme/composables/posts.data.ts create mode 100644 packages/docs/.vitepress/theme/composables/use-posts.ts create mode 100644 packages/docs/tsconfig.json diff --git a/packages/docs/.vitepress/config.ts b/packages/docs/.vitepress/config.ts index 1f5d3f7a4bd7..419f58d87ee4 100644 --- a/packages/docs/.vitepress/config.ts +++ b/packages/docs/.vitepress/config.ts @@ -62,7 +62,7 @@ export default defineConfig({ { text: 'API', link: '/api/' }, ], }, - // { text: 'Blog', link: '/blog/' }, + { text: 'Blog', link: '/blog/', activeMatch: '/blog/*' }, { text: 'Releases', link: 'https://github.com/toeverything/blocksuite/releases', diff --git a/packages/docs/.vitepress/theme/components/blog-list-layout.vue b/packages/docs/.vitepress/theme/components/blog-list-layout.vue new file mode 100644 index 000000000000..e079d17239bf --- /dev/null +++ b/packages/docs/.vitepress/theme/components/blog-list-layout.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/packages/docs/.vitepress/theme/components/blog-post-meta.vue b/packages/docs/.vitepress/theme/components/blog-post-meta.vue new file mode 100644 index 000000000000..b73785056bc3 --- /dev/null +++ b/packages/docs/.vitepress/theme/components/blog-post-meta.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/packages/docs/.vitepress/theme/code-sandbox.vue b/packages/docs/.vitepress/theme/components/code-sandbox.vue similarity index 100% rename from packages/docs/.vitepress/theme/code-sandbox.vue rename to packages/docs/.vitepress/theme/components/code-sandbox.vue diff --git a/packages/docs/.vitepress/theme/logo.vue b/packages/docs/.vitepress/theme/components/logo.vue similarity index 100% rename from packages/docs/.vitepress/theme/logo.vue rename to packages/docs/.vitepress/theme/components/logo.vue diff --git a/packages/docs/.vitepress/theme/playground.vue b/packages/docs/.vitepress/theme/components/playground.vue similarity index 100% rename from packages/docs/.vitepress/theme/playground.vue rename to packages/docs/.vitepress/theme/components/playground.vue diff --git a/packages/docs/.vitepress/theme/composables/posts.data.ts b/packages/docs/.vitepress/theme/composables/posts.data.ts new file mode 100644 index 000000000000..691cc0b8056c --- /dev/null +++ b/packages/docs/.vitepress/theme/composables/posts.data.ts @@ -0,0 +1,44 @@ +import { format, formatDistance } from 'date-fns'; +import { createContentLoader } from 'vitepress'; + +interface Post { + title: string; + authors: { name: string; link: string }[]; + url: string; + date: { + raw: string; + time: number; + formatted: string; + since: string; + }; +} + +function formatDate(raw: string) { + const date = new Date(raw); + date.setUTCHours(8); + return { + raw: date.toISOString().split('T')[0], + time: +date, + formatted: format(date, 'yyyy/MM/dd'), + since: formatDistance(date, new Date(), { addSuffix: true }), + }; +} + +const data = [] as Post[]; +export { data }; + +export default createContentLoader('blog/*.md', { + includeSrc: true, + transform(raw) { + return raw + .filter(item => item.url !== '/blog/') + .map(({ url, frontmatter }) => ({ + title: frontmatter.title, + authors: frontmatter.authors ?? [], + excerpt: frontmatter.excerpt ?? '', + url, + date: formatDate(frontmatter.date), + })) + .sort((a, b) => b.date.time - a.date.time); + }, +}); diff --git a/packages/docs/.vitepress/theme/composables/use-posts.ts b/packages/docs/.vitepress/theme/composables/use-posts.ts new file mode 100644 index 000000000000..755638e2e9c4 --- /dev/null +++ b/packages/docs/.vitepress/theme/composables/use-posts.ts @@ -0,0 +1,18 @@ +import { computed } from 'vue'; +import { useRoute } from 'vitepress'; +import { data as posts } from './posts.data'; + +export function usePosts() { + const route = useRoute(); + const path = route.path; + + function findCurrentIndex() { + const result = posts.findIndex(p => p.url === route.path); + if (result === -1) console.error(`blog post missing: ${route.path}`); + return result; + } + + const post = computed(() => posts[findCurrentIndex()]); + + return { posts, post, path }; +} diff --git a/packages/docs/.vitepress/theme/index.ts b/packages/docs/.vitepress/theme/index.ts index 4f7e555c5e52..478201a0374d 100644 --- a/packages/docs/.vitepress/theme/index.ts +++ b/packages/docs/.vitepress/theme/index.ts @@ -1,9 +1,11 @@ // https://vitepress.dev/guide/custom-theme import { h } from 'vue'; import Theme from 'vitepress/theme'; -import Logo from './logo.vue'; -import Playground from './playground.vue'; -import CodeSandbox from './code-sandbox.vue'; +import Logo from './components/logo.vue'; +import Playground from './components/playground.vue'; +import BlogListLayout from './components/blog-list-layout.vue'; +import BlogPostMeta from './components/blog-post-meta.vue'; +import CodeSandbox from './components/code-sandbox.vue'; import 'vitepress-plugin-sandpack/dist/style.css'; import './style.css'; @@ -17,6 +19,8 @@ export default { }); }, enhanceApp({ app, router, siteData }) { + app.component('BlogListLayout', BlogListLayout); + app.component('BlogPostMeta', BlogPostMeta); app.component('CodeSandbox', CodeSandbox); }, }; diff --git a/packages/docs/blog/crdt-native-data-flow.md b/packages/docs/blog/crdt-native-data-flow.md index 058bd210421f..ab27bb2a5beb 100644 --- a/packages/docs/blog/crdt-native-data-flow.md +++ b/packages/docs/blog/crdt-native-data-flow.md @@ -1,8 +1,21 @@ -# CRDT-Native Data Flow +--- +title: CRDT-Native Data Flow in BlockSuite +date: 2023-04-15 +authors: + - name: Yifeng Wang + link: 'https://twitter.com/ewind1994' + - name: Saul-Mirone + link: 'https://github.com/Saul-Mirone' +excerpt: To make editors intuitive and collaboration-ready, BlockSuite ensure that regardless of whether you are collaborating with others or not, the application code should be unaware of it. This article introduce how this is designed. +--- + +# CRDT-Native Data Flow in BlockSuite + + To make the editor logic based on BlockSuite intuitive and collaboration-ready, there is one major goal in BlockSuite: **Regardless of whether it is in a multi-user collaboration state, the application-layer code based on BlockSuite should be unaware of it**. -We will introduce how this design is embodied in BlockSuite. +We will introduce how this is designed in BlockSuite. ## CRDT as Single Source of Truth @@ -30,6 +43,33 @@ This design can be represented by the following diagram: The advantage of this approach is that the application-layer code can **completely ignore whether updates to the block model come from local editing, history stack, or collaboration with other users**. Just subscribing to model update events is adequate. +## Case Study + +As an example, suppose the current block tree structure is as follows: + +``` +PageBlock + NoteBlock + ParagraphBlock 0 + ParagraphBlock 1 + ParagraphBlock 2 +``` + +Now user A selects `ParagraphBlock 2` and presses the delete key to delete it. At this point, `page.deleteBlock` should be called to delete this block model instance: + +```ts +const blockModel = page.root.children[0].children[2]; +page.deleteBlock(blockModel); +``` + +At this point, BlockSuite does not directly modify the block tree under page.root, but instead first modifies the underlying YBlock. After the CRDT state is changed, Yjs generates a corresponding [Y.Event](https://docs.yjs.dev/api/y.event) data structure, which contains all the incremental state changes in this update (similar to patches in git and virtual DOM). BlockSuite will always use this as the basis + +At this point, BlockSuite does not directly modify the block tree under `page.root`, but will instead firstly modify the underlying YBlock. After the CRDT state is changed, Yjs will generate the corresponding `Y.Event`, which is similar to incremental patches in git and virtual DOM. BlockSuite will always use this as the basis to synchronize the block models, then trigger the corresponding slot events for UI updates. + +In this example, as the parent of `ParagraphBlock 2`, the `model.childrenUpdated` slot event of `NoteBlock` will be triggered. This will enable the corresponding component in the UI framework component tree to refresh itself. Since each child block has an ID, this is very conducive to combining the common list key optimizations in UI frameworks, achieving on-demand block component updates. + +But the real power lies in the fact that if this block tree is being concurrently edited by multiple people, when user B performs a similar operation, the corresponding update will be encoded by Yjs and distributed by the provider. **When User A receives and applies the update from User B, the same state update pipeline as local editing will be triggered**. This makes it unnecessary for the application to make any additional modifications or adaptations for collaboration scenarios, inherently gaining real-time collaboration capabilities. + ## Unidirectional Update Flow Besides the block tree that uses CRDT as its single source of truth, BlockSuite also manages shared states that do not require a history of changes, such as the awareness state of each user's cursor position. Additionally, some user metadata may not be shared among all users. diff --git a/packages/docs/blog/document-centric.md b/packages/docs/blog/document-centric.md index 1c97ef2df99e..edfb0f2d6708 100644 --- a/packages/docs/blog/document-centric.md +++ b/packages/docs/blog/document-centric.md @@ -1,9 +1,16 @@ --- -layout: doc +title: Building Document-Centric, CRDT-Native Editors +date: 2024-01-10 +authors: + - name: Yifeng Wang + link: 'https://twitter.com/ewind1994' +excerpt: 'This article presents the document-centric way for building editors, and why CRDT is required to make this happpen.' --- # Building Document-Centric, CRDT-Native Editors + + ## Motivation For years, web frameworks such as React and Vue have popularized the mental model of component based development. This approach allows us to break down complex front-end applications into components for better composition and maintenance. diff --git a/packages/docs/blog/index.md b/packages/docs/blog/index.md index 517608661a4a..2b245fef5159 100644 --- a/packages/docs/blog/index.md +++ b/packages/docs/blog/index.md @@ -1,6 +1,4 @@ --- -layout: doc +layout: BlogListLayout +title: Blog --- - -- [Building Document-Centric, CRDT-Native Editors](./document-centric) -- [CRDT-Native Data Flow](./crdt-native-data-flow) diff --git a/packages/docs/package.json b/packages/docs/package.json index 150aa36c3575..0fb49397e275 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -27,6 +27,7 @@ "@blocksuite/blocks": "workspace:*", "@blocksuite/presets": "workspace:*", "@blocksuite/store": "workspace:*", + "date-fns": "^3.3.0", "markdown-it-container": "^4.0.0", "vitepress-plugin-sandpack": "^1.1.4" } diff --git a/packages/docs/tsconfig.json b/packages/docs/tsconfig.json new file mode 100644 index 000000000000..ac1da8d95cc6 --- /dev/null +++ b/packages/docs/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./.vitepress"], +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d17e6e4d341d..6a1a69424f4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -287,6 +287,9 @@ importers: '@blocksuite/store': specifier: workspace:* version: link:../framework/store + date-fns: + specifier: ^3.3.0 + version: 3.3.0 markdown-it-container: specifier: ^4.0.0 version: 4.0.0 @@ -1687,8 +1690,8 @@ packages: engines: {node: '>=12.4.0'} dev: true - /@nolyfill/is-generator-function@1.0.24: - resolution: {integrity: sha512-1b5eOnqdmfMusjU5YNvojLlZguoENQfA3wa1AQNEvBsR5pi41i2Zn1WNvk4Ph1QI3hpW8xWOmAJS1ouMF5cNkA==} + /@nolyfill/is-generator-function@1.0.28: + resolution: {integrity: sha512-Lmb7ihogbV5G5S5FRQTvyiQWpPZmZp9UB4rW5J28pMv41eBFFK0PWfY1DpfHdzRzLS6mVuh9RECPyjVrrXiX5g==} engines: {node: '>=12.4.0'} dev: true @@ -5126,7 +5129,7 @@ packages: fresh: 0.5.2 http-assert: 1.5.0 http-errors: 1.8.1 - is-generator-function: /@nolyfill/is-generator-function@1.0.24 + is-generator-function: /@nolyfill/is-generator-function@1.0.28 koa-compose: 4.1.0 koa-convert: 2.0.0 on-finished: 2.4.1