mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-16 22:07:09 +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();
|
||||
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',
|
||||
]);
|
||||
|
||||
const markdownType = [
|
||||
'text/markdown',
|
||||
'application/markdown',
|
||||
'text/x-markdown',
|
||||
];
|
||||
|
||||
@Controller('/workspace')
|
||||
export class DocRendererController {
|
||||
private readonly logger = new Logger(DocRendererController.name);
|
||||
@@ -68,6 +74,21 @@ export class DocRendererController {
|
||||
.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()
|
||||
@Get('/*path')
|
||||
async render(@Req() req: Request, @Res() res: Response) {
|
||||
@@ -81,28 +102,55 @@ export class DocRendererController {
|
||||
|
||||
let opts: RenderOptions | null = null;
|
||||
// /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
|
||||
if (workspaceId && !staticPaths.has(subPath) && restPaths.length === 0) {
|
||||
if (isWorkspace) {
|
||||
try {
|
||||
opts =
|
||||
workspaceId === subPath
|
||||
? await this.getWorkspaceContent(workspaceId)
|
||||
: await this.getPageContent(workspaceId, subPath);
|
||||
opts = isDocPath
|
||||
? await this.getPageContent(workspaceId, sub)
|
||||
: await this.getWorkspaceContent(workspaceId);
|
||||
metrics.doc.counter('render').add(1);
|
||||
|
||||
if (opts && workspaceId !== subPath) {
|
||||
if (opts && isDocPath) {
|
||||
void this.models.workspaceAnalytics
|
||||
.recordDocView({
|
||||
workspaceId,
|
||||
docId: subPath,
|
||||
visitorId: this.buildVisitorId(req, workspaceId, subPath),
|
||||
docId: sub,
|
||||
visitorId: this.buildVisitorId(req, workspaceId, sub),
|
||||
isGuest: true,
|
||||
})
|
||||
.catch(error => {
|
||||
this.logger.warn(
|
||||
`Failed to record shared page view: ${workspaceId}/${subPath}`,
|
||||
`Failed to record shared page view: ${workspaceId}/${sub}`,
|
||||
error as Error
|
||||
);
|
||||
});
|
||||
@@ -124,20 +172,7 @@ export class DocRendererController {
|
||||
workspaceId: string,
|
||||
docId: string
|
||||
): Promise<RenderOptions | null> {
|
||||
const allowSharing = await this.models.workspace.allowSharing(workspaceId);
|
||||
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) {
|
||||
if (await this.allowDocPreview(workspaceId, docId)) {
|
||||
return this.doc.getDocContent(workspaceId, docId);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user