mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08:00
feat: support markdown preview (#14447)
This commit is contained in:
@@ -109,3 +109,45 @@ test('should record page view when rendering shared page', async t => {
|
|||||||
docContent.restore();
|
docContent.restore();
|
||||||
record.restore();
|
record.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should return markdown content and skip page view when accept is text/markdown', async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
const { app, adapter, models, docReader } = t.context;
|
||||||
|
|
||||||
|
const doc = new YDoc();
|
||||||
|
const text = doc.getText('content');
|
||||||
|
const updates: Buffer[] = [];
|
||||||
|
|
||||||
|
doc.on('update', update => {
|
||||||
|
updates.push(Buffer.from(update));
|
||||||
|
});
|
||||||
|
|
||||||
|
text.insert(0, 'markdown');
|
||||||
|
await adapter.pushDocUpdates(workspace.id, docId, updates, user.id);
|
||||||
|
await models.doc.publish(workspace.id, docId);
|
||||||
|
|
||||||
|
const markdown = Sinon.stub(docReader, 'getDocMarkdown').resolves({
|
||||||
|
title: 'markdown-doc',
|
||||||
|
markdown: '# markdown-doc',
|
||||||
|
});
|
||||||
|
const docContent = Sinon.stub(docReader, 'getDocContent');
|
||||||
|
const record = Sinon.stub(
|
||||||
|
models.workspaceAnalytics,
|
||||||
|
'recordDocView'
|
||||||
|
).resolves();
|
||||||
|
|
||||||
|
const res = await app
|
||||||
|
.GET(`/workspace/${workspace.id}/${docId}`)
|
||||||
|
.set('accept', 'text/markdown')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
t.true(markdown.calledOnceWithExactly(workspace.id, docId, false));
|
||||||
|
t.is(res.text, '# markdown-doc');
|
||||||
|
t.true((res.headers['content-type'] as string).startsWith('text/markdown'));
|
||||||
|
t.true(docContent.notCalled);
|
||||||
|
t.true(record.notCalled);
|
||||||
|
|
||||||
|
markdown.restore();
|
||||||
|
docContent.restore();
|
||||||
|
record.restore();
|
||||||
|
});
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ const staticPaths = new Set([
|
|||||||
'trash',
|
'trash',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const markdownType = [
|
||||||
|
'text/markdown',
|
||||||
|
'application/markdown',
|
||||||
|
'text/x-markdown',
|
||||||
|
];
|
||||||
|
|
||||||
@Controller('/workspace')
|
@Controller('/workspace')
|
||||||
export class DocRendererController {
|
export class DocRendererController {
|
||||||
private readonly logger = new Logger(DocRendererController.name);
|
private readonly logger = new Logger(DocRendererController.name);
|
||||||
@@ -68,6 +74,21 @@ export class DocRendererController {
|
|||||||
.digest('hex');
|
.digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async allowDocPreview(workspaceId: string, docId: string) {
|
||||||
|
const allowSharing = await this.models.workspace.allowSharing(workspaceId);
|
||||||
|
if (!allowSharing) return false;
|
||||||
|
|
||||||
|
let allowUrlPreview = await this.models.doc.isPublic(workspaceId, docId);
|
||||||
|
|
||||||
|
if (!allowUrlPreview) {
|
||||||
|
// if page is private, but workspace url preview is on
|
||||||
|
allowUrlPreview =
|
||||||
|
await this.models.workspace.allowUrlPreview(workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowUrlPreview;
|
||||||
|
}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@Get('/*path')
|
@Get('/*path')
|
||||||
async render(@Req() req: Request, @Res() res: Response) {
|
async render(@Req() req: Request, @Res() res: Response) {
|
||||||
@@ -81,28 +102,55 @@ export class DocRendererController {
|
|||||||
|
|
||||||
let opts: RenderOptions | null = null;
|
let opts: RenderOptions | null = null;
|
||||||
// /workspace/:workspaceId/{:docId | staticPaths}
|
// /workspace/:workspaceId/{:docId | staticPaths}
|
||||||
const [, , workspaceId, subPath, ...restPaths] = req.path.split('/');
|
const [, , workspaceId, sub, ...rest] = req.path.split('/');
|
||||||
|
const isWorkspace =
|
||||||
|
workspaceId && sub && !staticPaths.has(sub) && rest.length === 0;
|
||||||
|
const isDocPath = isWorkspace && workspaceId !== sub;
|
||||||
|
|
||||||
|
if (
|
||||||
|
isDocPath &&
|
||||||
|
req.accepts().some(t => markdownType.includes(t.toLowerCase()))
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const allowPreview = await this.allowDocPreview(workspaceId, sub);
|
||||||
|
if (!allowPreview) {
|
||||||
|
res.status(404).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const markdown = await this.doc.getDocMarkdown(workspaceId, sub, false);
|
||||||
|
if (markdown) {
|
||||||
|
res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
|
||||||
|
res.send(markdown.markdown);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error('failed to render markdown page', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(404).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// /:workspaceId/:docId
|
// /:workspaceId/:docId
|
||||||
if (workspaceId && !staticPaths.has(subPath) && restPaths.length === 0) {
|
if (isWorkspace) {
|
||||||
try {
|
try {
|
||||||
opts =
|
opts = isDocPath
|
||||||
workspaceId === subPath
|
? await this.getPageContent(workspaceId, sub)
|
||||||
? await this.getWorkspaceContent(workspaceId)
|
: await this.getWorkspaceContent(workspaceId);
|
||||||
: await this.getPageContent(workspaceId, subPath);
|
|
||||||
metrics.doc.counter('render').add(1);
|
metrics.doc.counter('render').add(1);
|
||||||
|
|
||||||
if (opts && workspaceId !== subPath) {
|
if (opts && isDocPath) {
|
||||||
void this.models.workspaceAnalytics
|
void this.models.workspaceAnalytics
|
||||||
.recordDocView({
|
.recordDocView({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
docId: subPath,
|
docId: sub,
|
||||||
visitorId: this.buildVisitorId(req, workspaceId, subPath),
|
visitorId: this.buildVisitorId(req, workspaceId, sub),
|
||||||
isGuest: true,
|
isGuest: true,
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Failed to record shared page view: ${workspaceId}/${subPath}`,
|
`Failed to record shared page view: ${workspaceId}/${sub}`,
|
||||||
error as Error
|
error as Error
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -124,20 +172,7 @@ export class DocRendererController {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
docId: string
|
docId: string
|
||||||
): Promise<RenderOptions | null> {
|
): Promise<RenderOptions | null> {
|
||||||
const allowSharing = await this.models.workspace.allowSharing(workspaceId);
|
if (await this.allowDocPreview(workspaceId, docId)) {
|
||||||
if (!allowSharing) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let allowUrlPreview = await this.models.doc.isPublic(workspaceId, docId);
|
|
||||||
|
|
||||||
if (!allowUrlPreview) {
|
|
||||||
// if page is private, but workspace url preview is on
|
|
||||||
allowUrlPreview =
|
|
||||||
await this.models.workspace.allowUrlPreview(workspaceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowUrlPreview) {
|
|
||||||
return this.doc.getDocContent(workspaceId, docId);
|
return this.doc.getDocContent(workspaceId, docId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user