mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
feat(server): parse ydoc to markdown (#12812)
close AI-190 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced an endpoint to retrieve a document's markdown content and title. - Added backend support for parsing document snapshots directly into markdown format. - **Tests** - Added comprehensive tests and snapshot files for markdown retrieval, including success and error scenarios. - Improved test coverage for content type validation and markdown parsing utilities. - **Documentation** - Enhanced internal documentation through detailed test cases and snapshot references for new markdown features. <!-- end of auto-generated comment: release notes by coderabbit.ai --> #### PR Dependency Tree * **PR #12812** 👈 * **PR #12846** * **PR #12811** This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
@@ -175,6 +175,7 @@ test('should get doc content in json format', async t => {
|
||||
await app
|
||||
.GET(`/rpc/workspaces/${workspace.id}/docs/${docId}/content`)
|
||||
.set('x-access-token', t.context.crypto.sign(docId))
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect({
|
||||
title: 'test title',
|
||||
summary: 'test summary',
|
||||
@@ -184,6 +185,7 @@ test('should get doc content in json format', async t => {
|
||||
await app
|
||||
.GET(`/rpc/workspaces/${workspace.id}/docs/${docId}/content?full=false`)
|
||||
.set('x-access-token', t.context.crypto.sign(docId))
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect({
|
||||
title: 'test title',
|
||||
summary: 'test summary',
|
||||
@@ -205,6 +207,7 @@ test('should get full doc content in json format', async t => {
|
||||
await app
|
||||
.GET(`/rpc/workspaces/${workspace.id}/docs/${docId}/content?full=true`)
|
||||
.set('x-access-token', t.context.crypto.sign(docId))
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect({
|
||||
title: 'test title',
|
||||
summary: 'test summary full',
|
||||
@@ -251,3 +254,44 @@ test('should get workspace content in json format', async t => {
|
||||
});
|
||||
t.pass();
|
||||
});
|
||||
|
||||
test('should get doc markdown in json format', async t => {
|
||||
const { app } = t.context;
|
||||
mock.method(t.context.databaseDocReader, 'getDocMarkdown', async () => {
|
||||
return {
|
||||
title: 'test title',
|
||||
markdown: 'test markdown',
|
||||
};
|
||||
});
|
||||
|
||||
const docId = randomUUID();
|
||||
await app
|
||||
.GET(`/rpc/workspaces/${workspace.id}/docs/${docId}/markdown`)
|
||||
.set('x-access-token', t.context.crypto.sign(docId))
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect(200)
|
||||
.expect({
|
||||
title: 'test title',
|
||||
markdown: 'test markdown',
|
||||
});
|
||||
t.pass();
|
||||
});
|
||||
|
||||
test('should 404 when doc markdown not found', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const workspaceId = '123';
|
||||
const docId = '123';
|
||||
await app
|
||||
.GET(`/rpc/workspaces/${workspaceId}/docs/${docId}/markdown`)
|
||||
.set('x-access-token', t.context.crypto.sign(docId))
|
||||
.expect({
|
||||
status: 404,
|
||||
code: 'Not Found',
|
||||
type: 'RESOURCE_NOT_FOUND',
|
||||
name: 'NOT_FOUND',
|
||||
message: 'Doc not found',
|
||||
})
|
||||
.expect(404);
|
||||
t.pass();
|
||||
});
|
||||
|
||||
@@ -42,6 +42,20 @@ export class DocRpcController {
|
||||
res.send(doc.bin);
|
||||
}
|
||||
|
||||
@SkipThrottle()
|
||||
@Internal()
|
||||
@Get('/workspaces/:workspaceId/docs/:docId/markdown')
|
||||
async getDocMarkdown(
|
||||
@Param('workspaceId') workspaceId: string,
|
||||
@Param('docId') docId: string
|
||||
) {
|
||||
const result = await this.docReader.getDocMarkdown(workspaceId, docId);
|
||||
if (!result) {
|
||||
throw new NotFound('Doc not found');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SkipThrottle()
|
||||
@Internal()
|
||||
@Post('/workspaces/:workspaceId/docs/:docId/diff')
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# Snapshot report for `src/core/doc/__tests__/reader-from-database.spec.ts`
|
||||
|
||||
The actual snapshot is saved in `reader-from-database.spec.ts.snap`.
|
||||
|
||||
Generated by [AVA](https://avajs.dev).
|
||||
|
||||
## should return doc markdown success
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
{
|
||||
markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
# You own your data, with no compromises␊
|
||||
␊
|
||||
␊
|
||||
## Local-first & Real-time collaborative␊
|
||||
␊
|
||||
␊
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
|
||||
␊
|
||||
␊
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
|
||||
␊
|
||||
␊
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
|
||||
␊
|
||||
␊
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
|
||||
␊
|
||||
␊
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
|
||||
␊
|
||||
␊
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
|
||||
␊
|
||||
␊
|
||||
## A true canvas for blocks in any form␊
|
||||
␊
|
||||
␊
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
|
||||
␊
|
||||
␊
|
||||
* Quip & Notion with their great concept of "everything is a block"␊
|
||||
␊
|
||||
␊
|
||||
* Trello with their Kanban␊
|
||||
␊
|
||||
␊
|
||||
* Airtable & Miro with their no-code programable datasheets␊
|
||||
␊
|
||||
␊
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard␊
|
||||
␊
|
||||
␊
|
||||
* Remnote & Capacities with their object-based tag system␊
|
||||
␊
|
||||
␊
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
|
||||
␊
|
||||
␊
|
||||
## Self Host␊
|
||||
␊
|
||||
␊
|
||||
Self host AFFiNE␊
|
||||
␊
|
||||
␊
|
||||
||Title|Tag|␊
|
||||
|---|---|---|␊
|
||||
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
|
||||
|For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc|<span data-affine-option data-value="0jh9gNw4Yl" data-option-color="var(--affine-tag-orange)">Developers</span>|␊
|
||||
|Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Trello with their Kanban|Trello with their Kanban|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
|
||||
␊
|
||||
␊
|
||||
## Affine Development␊
|
||||
␊
|
||||
␊
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
`,
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,106 @@
|
||||
# Snapshot report for `src/core/doc/__tests__/reader-from-rpc.spec.ts`
|
||||
|
||||
The actual snapshot is saved in `reader-from-rpc.spec.ts.snap`.
|
||||
|
||||
Generated by [AVA](https://avajs.dev).
|
||||
|
||||
## should return doc markdown success
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
{
|
||||
markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
# You own your data, with no compromises␊
|
||||
␊
|
||||
␊
|
||||
## Local-first & Real-time collaborative␊
|
||||
␊
|
||||
␊
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
|
||||
␊
|
||||
␊
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
|
||||
␊
|
||||
␊
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
|
||||
␊
|
||||
␊
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
|
||||
␊
|
||||
␊
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
|
||||
␊
|
||||
␊
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
|
||||
␊
|
||||
␊
|
||||
## A true canvas for blocks in any form␊
|
||||
␊
|
||||
␊
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
|
||||
␊
|
||||
␊
|
||||
* Quip & Notion with their great concept of "everything is a block"␊
|
||||
␊
|
||||
␊
|
||||
* Trello with their Kanban␊
|
||||
␊
|
||||
␊
|
||||
* Airtable & Miro with their no-code programable datasheets␊
|
||||
␊
|
||||
␊
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard␊
|
||||
␊
|
||||
␊
|
||||
* Remnote & Capacities with their object-based tag system␊
|
||||
␊
|
||||
␊
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
|
||||
␊
|
||||
␊
|
||||
## Self Host␊
|
||||
␊
|
||||
␊
|
||||
Self host AFFiNE␊
|
||||
␊
|
||||
␊
|
||||
||Title|Tag|␊
|
||||
|---|---|---|␊
|
||||
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
|
||||
|For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc|<span data-affine-option data-value="0jh9gNw4Yl" data-option-color="var(--affine-tag-orange)">Developers</span>|␊
|
||||
|Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Trello with their Kanban|Trello with their Kanban|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
|
||||
␊
|
||||
␊
|
||||
## Affine Development␊
|
||||
␊
|
||||
␊
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
`,
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
Binary file not shown.
@@ -257,3 +257,28 @@ test('should get workspace content with custom avatar', async t => {
|
||||
avatarUrl: `http://localhost:3010/api/workspaces/${workspace.id}/blobs/${avatarKey}`,
|
||||
});
|
||||
});
|
||||
|
||||
test('should return doc markdown success', async t => {
|
||||
const workspace = await module.create(Mockers.Workspace, {
|
||||
owner: user,
|
||||
name: '',
|
||||
});
|
||||
|
||||
const docSnapshot = await module.create(Mockers.DocSnapshot, {
|
||||
workspaceId: workspace.id,
|
||||
user,
|
||||
});
|
||||
|
||||
const result = await docReader.getDocMarkdown(workspace.id, docSnapshot.id);
|
||||
t.snapshot(result);
|
||||
});
|
||||
|
||||
test('should read markdown return null when doc not exists', async t => {
|
||||
const workspace = await module.create(Mockers.Workspace, {
|
||||
owner: user,
|
||||
name: '',
|
||||
});
|
||||
|
||||
const result = await docReader.getDocMarkdown(workspace.id, randomUUID());
|
||||
t.is(result, null);
|
||||
});
|
||||
|
||||
@@ -5,13 +5,24 @@ import { User, Workspace } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import { applyUpdate, Doc as YDoc } from 'yjs';
|
||||
|
||||
import { createModule } from '../../../__tests__/create-module';
|
||||
import { Mockers } from '../../../__tests__/mocks';
|
||||
import { createTestingApp, type TestingApp } from '../../../__tests__/utils';
|
||||
import { UserFriendlyError } from '../../../base';
|
||||
import { ConfigFactory } from '../../../base/config';
|
||||
import { Models } from '../../../models';
|
||||
import { DatabaseDocReader, DocReader, PgWorkspaceDocStorageAdapter } from '..';
|
||||
import {
|
||||
DatabaseDocReader,
|
||||
DocReader,
|
||||
DocStorageModule,
|
||||
PgWorkspaceDocStorageAdapter,
|
||||
} from '..';
|
||||
import { RpcDocReader } from '../reader';
|
||||
|
||||
const module = await createModule({
|
||||
imports: [DocStorageModule],
|
||||
});
|
||||
|
||||
const test = ava as TestFn<{
|
||||
models: Models;
|
||||
app: TestingApp;
|
||||
@@ -68,6 +79,12 @@ test.afterEach.always(() => {
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
await t.context.docApp.close();
|
||||
await module.close();
|
||||
});
|
||||
|
||||
test('should be rpc reader', async t => {
|
||||
const { docReader } = t.context;
|
||||
t.true(docReader instanceof RpcDocReader);
|
||||
});
|
||||
|
||||
test('should return null when doc not found', async t => {
|
||||
@@ -144,7 +161,6 @@ test('should fallback to database doc reader when endpoint network error', async
|
||||
|
||||
test('should return doc when found', async t => {
|
||||
const { docReader } = t.context;
|
||||
t.true(docReader instanceof RpcDocReader);
|
||||
|
||||
const docId = randomUUID();
|
||||
const timestamp = Date.now();
|
||||
@@ -359,3 +375,32 @@ test('should return null when workspace bin meta not exists', async t => {
|
||||
const notExists = await docReader.getWorkspaceContent(randomUUID());
|
||||
t.is(notExists, null);
|
||||
});
|
||||
|
||||
test('should return doc markdown success', async t => {
|
||||
const { docReader } = t.context;
|
||||
|
||||
const workspace = await module.create(Mockers.Workspace, {
|
||||
owner: user,
|
||||
name: '',
|
||||
});
|
||||
|
||||
const docSnapshot = await module.create(Mockers.DocSnapshot, {
|
||||
workspaceId: workspace.id,
|
||||
user,
|
||||
});
|
||||
|
||||
const result = await docReader.getDocMarkdown(workspace.id, docSnapshot.id);
|
||||
t.snapshot(result);
|
||||
});
|
||||
|
||||
test('should read markdown return null when doc not exists', async t => {
|
||||
const { docReader } = t.context;
|
||||
|
||||
const workspace = await module.create(Mockers.Workspace, {
|
||||
owner: user,
|
||||
name: '',
|
||||
});
|
||||
|
||||
const result = await docReader.getDocMarkdown(workspace.id, randomUUID());
|
||||
t.is(result, null);
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Models } from '../../models';
|
||||
import { WorkspaceBlobStorage } from '../storage';
|
||||
import {
|
||||
type PageDocContent,
|
||||
parseDocToMarkdownFromDocSnapshot,
|
||||
parsePageDoc,
|
||||
parseWorkspaceDoc,
|
||||
} from '../utils/blocksuite';
|
||||
@@ -33,6 +34,11 @@ export interface WorkspaceDocInfo {
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export interface DocMarkdown {
|
||||
title: string;
|
||||
markdown: string;
|
||||
}
|
||||
|
||||
export abstract class DocReader {
|
||||
protected readonly logger = new Logger(DocReader.name);
|
||||
|
||||
@@ -59,6 +65,11 @@ export abstract class DocReader {
|
||||
docId: string
|
||||
): Promise<DocRecord | null>;
|
||||
|
||||
abstract getDocMarkdown(
|
||||
workspaceId: string,
|
||||
docId: string
|
||||
): Promise<DocMarkdown | null>;
|
||||
|
||||
abstract getDocDiff(
|
||||
spaceId: string,
|
||||
docId: string,
|
||||
@@ -171,6 +182,17 @@ export class DatabaseDocReader extends DocReader {
|
||||
return await this.workspace.getDoc(workspaceId, docId);
|
||||
}
|
||||
|
||||
async getDocMarkdown(
|
||||
workspaceId: string,
|
||||
docId: string
|
||||
): Promise<DocMarkdown | null> {
|
||||
const doc = await this.workspace.getDoc(workspaceId, docId);
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
return parseDocToMarkdownFromDocSnapshot(workspaceId, docId, doc.bin);
|
||||
}
|
||||
|
||||
async getDocDiff(
|
||||
spaceId: string,
|
||||
docId: string,
|
||||
@@ -304,6 +326,33 @@ export class RpcDocReader extends DatabaseDocReader {
|
||||
}
|
||||
}
|
||||
|
||||
override async getDocMarkdown(
|
||||
workspaceId: string,
|
||||
docId: string
|
||||
): Promise<DocMarkdown | null> {
|
||||
const url = `${this.config.docService.endpoint}/rpc/workspaces/${workspaceId}/docs/${docId}/markdown`;
|
||||
const accessToken = this.crypto.sign(docId);
|
||||
try {
|
||||
const res = await this.fetch(accessToken, url, 'GET');
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
return (await res.json()) as DocMarkdown;
|
||||
} catch (e) {
|
||||
if (e instanceof UserFriendlyError) {
|
||||
throw e;
|
||||
}
|
||||
const err = e as Error;
|
||||
// other error
|
||||
this.logger.error(
|
||||
`Failed to fetch doc markdown ${url}, fallback to database doc reader`,
|
||||
err
|
||||
);
|
||||
// fallback to database doc reader if the error is not user friendly, like network error
|
||||
return await super.getDocMarkdown(workspaceId, docId);
|
||||
}
|
||||
}
|
||||
|
||||
override async getDocDiff(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
|
||||
@@ -1366,3 +1366,104 @@ Generated by [AVA](https://avajs.dev).
|
||||
summary: 'AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro. You own your data, with no compromisesLocal-first & Real-time collaborativeWe love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.Blocks that assemble your next docs, tasks kanban or whiteboardThere is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further. ',
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
|
||||
## can parse doc to markdown from doc snapshot
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
{
|
||||
markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
# You own your data, with no compromises␊
|
||||
␊
|
||||
␊
|
||||
## Local-first & Real-time collaborative␊
|
||||
␊
|
||||
␊
|
||||
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
|
||||
␊
|
||||
␊
|
||||
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
|
||||
␊
|
||||
␊
|
||||
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
|
||||
␊
|
||||
␊
|
||||
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
|
||||
␊
|
||||
␊
|
||||
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
|
||||
␊
|
||||
␊
|
||||
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
|
||||
␊
|
||||
␊
|
||||
## A true canvas for blocks in any form␊
|
||||
␊
|
||||
␊
|
||||
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
|
||||
␊
|
||||
␊
|
||||
* Quip & Notion with their great concept of "everything is a block"␊
|
||||
␊
|
||||
␊
|
||||
* Trello with their Kanban␊
|
||||
␊
|
||||
␊
|
||||
* Airtable & Miro with their no-code programable datasheets␊
|
||||
␊
|
||||
␊
|
||||
* Miro & Whimiscal with their edgeless visual whiteboard␊
|
||||
␊
|
||||
␊
|
||||
* Remnote & Capacities with their object-based tag system␊
|
||||
␊
|
||||
␊
|
||||
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
|
||||
␊
|
||||
␊
|
||||
## Self Host␊
|
||||
␊
|
||||
␊
|
||||
Self host AFFiNE␊
|
||||
␊
|
||||
␊
|
||||
||Title|Tag|␊
|
||||
|---|---|---|␊
|
||||
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
|
||||
|For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc|<span data-affine-option data-value="0jh9gNw4Yl" data-option-color="var(--affine-tag-orange)">Developers</span>|␊
|
||||
|Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Trello with their Kanban|Trello with their Kanban|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|
||||
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
|
||||
␊
|
||||
␊
|
||||
## Affine Development␊
|
||||
␊
|
||||
␊
|
||||
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
␊
|
||||
`,
|
||||
title: 'Write, Draw, Plan all at Once.',
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -5,6 +5,7 @@ import { createModule } from '../../../__tests__/create-module';
|
||||
import { Mockers } from '../../../__tests__/mocks';
|
||||
import { Models } from '../../../models';
|
||||
import {
|
||||
parseDocToMarkdownFromDocSnapshot,
|
||||
readAllBlocksFromDocSnapshot,
|
||||
readAllDocIdsFromWorkspaceSnapshot,
|
||||
} from '../blocksuite';
|
||||
@@ -88,3 +89,13 @@ test('can read all blocks from doc snapshot without workspace snapshot', async t
|
||||
blocks: result!.blocks.map(block => omit(block, ['yblock'])),
|
||||
});
|
||||
});
|
||||
|
||||
test('can parse doc to markdown from doc snapshot', async t => {
|
||||
const result = parseDocToMarkdownFromDocSnapshot(
|
||||
workspace.id,
|
||||
docSnapshot.id,
|
||||
docSnapshot.blob
|
||||
);
|
||||
|
||||
t.snapshot(result);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports -- import from bundle
|
||||
import {
|
||||
parsePageDoc as parseDocToMarkdown,
|
||||
readAllBlocksFromDoc,
|
||||
readAllDocIdsFromRootDoc,
|
||||
} from '@affine/reader/dist';
|
||||
@@ -196,3 +197,30 @@ export async function readAllBlocksFromDocSnapshot(
|
||||
maxSummaryLength,
|
||||
});
|
||||
}
|
||||
|
||||
export function parseDocToMarkdownFromDocSnapshot(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
docSnapshot: Uint8Array
|
||||
) {
|
||||
const ydoc = new YDoc({
|
||||
guid: docId,
|
||||
});
|
||||
applyUpdate(ydoc, docSnapshot);
|
||||
|
||||
const parsed = parseDocToMarkdown({
|
||||
workspaceId,
|
||||
doc: ydoc,
|
||||
buildBlobUrl: (blobId: string) => {
|
||||
return `/${workspaceId}/blobs/${blobId}`;
|
||||
},
|
||||
buildDocUrl: (docId: string) => {
|
||||
return `/workspace/${workspaceId}/${docId}`;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
title: parsed.title,
|
||||
markdown: parsed.md,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user