${document.title}
-本文渲染于 ${dayjs().format('DD/MM/YYYY')}
${markdown}diff --git a/package.json b/package.json index 5b40b891..99ea8662 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "fastify-multipart": "4.0.7", "fastify-swagger": "4.11.0", "graphql": "15.5.3", + "html-minifier": "^4.0.0", "image-size": "1.0.0", "inquirer": "*", "js-yaml": "*", @@ -110,6 +111,7 @@ "@types/bcrypt": "5.0.0", "@types/cache-manager": "3.4.2", "@types/ejs": "3.1.0", + "@types/html-minifier": "^4.0.1", "@types/ioredis": "4.27.2", "@types/jest": "27.0.1", "@types/lodash": "4.14.172", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2248cd68..44db9d4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,7 @@ specifiers: '@types/bcrypt': 5.0.0 '@types/cache-manager': 3.4.2 '@types/ejs': 3.1.0 + '@types/html-minifier': ^4.0.1 '@types/ioredis': 4.27.2 '@types/jest': 27.0.1 '@types/lodash': 4.14.172 @@ -57,6 +58,7 @@ specifiers: fastify-multipart: 4.0.7 fastify-swagger: 4.11.0 graphql: 15.5.3 + html-minifier: ^4.0.0 husky: 7.0.2 image-size: 1.0.0 inquirer: '*' @@ -133,6 +135,7 @@ dependencies: fastify-multipart: 4.0.7 fastify-swagger: 4.11.0 graphql: 15.5.3 + html-minifier: 4.0.0 image-size: 1.0.0 inquirer: 8.1.1 js-yaml: 4.1.0 @@ -171,6 +174,7 @@ devDependencies: '@types/bcrypt': 5.0.0 '@types/cache-manager': 3.4.2 '@types/ejs': 3.1.0 + '@types/html-minifier': 4.0.1 '@types/ioredis': 4.27.2 '@types/jest': 27.0.1 '@types/lodash': 4.14.172 @@ -1762,6 +1766,13 @@ packages: resolution: {integrity: sha512-1IwA74t5ID4KWo0Kndal16MhiPSZgMe1fGc+MLT6j5r+Ab7jku36PFTl4PP6MiWw0BJscM9QpZEo00qixNQoRg==} dev: true + /@types/clean-css/4.2.5: + resolution: {integrity: sha512-NEzjkGGpbs9S9fgC4abuBvTpVwE3i+Acu9BBod3PUyjDVZcNsGx61b8r2PphR61QGPnn0JHVs5ey6/I4eTrkxw==} + dependencies: + '@types/node': 16.9.1 + source-map: 0.6.1 + dev: true + /@types/component-emitter/1.2.10: resolution: {integrity: sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==} @@ -1832,6 +1843,14 @@ packages: '@types/node': 16.9.1 dev: true + /@types/html-minifier/4.0.1: + resolution: {integrity: sha512-6u58FWQbWP45bsxVeMJo0yk2LEsjjZsCwn0JDe/i5Edk3L+b9TR5eZ2FGaMCrLdtGYpME5AGxUqv8o+3hWKogw==} + dependencies: + '@types/clean-css': 4.2.5 + '@types/relateurl': 0.2.29 + '@types/uglify-js': 3.13.1 + dev: true + /@types/ioredis/4.27.2: resolution: {integrity: sha512-/HXAbeJOR4Ub1O0XVlOFxrRTf2Yeq7BSre3qGoBvTTxN29tSmQPPwIYYxyzm2SkNgvx0Re9ahqCVanOVHqAARg==} dependencies: @@ -1994,6 +2013,10 @@ packages: '@types/node': 16.9.0 dev: true + /@types/relateurl/0.2.29: + resolution: {integrity: sha512-QSvevZ+IRww2ldtfv1QskYsqVVVwCKQf1XbwtcyyoRvLIQzfyPhj/C+3+PKzSDRdiyejaiLgnq//XTkleorpLg==} + dev: true + /@types/serve-static/1.13.10: resolution: {integrity: sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==} dependencies: @@ -2022,6 +2045,12 @@ packages: resolution: {integrity: sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==} dev: true + /@types/uglify-js/3.13.1: + resolution: {integrity: sha512-O3MmRAk6ZuAKa9CHgg0Pr0+lUOqoMLpc9AS4R8ano2auvsg7IE8syF3Xh/NPr26TWklxYcqoEEFdzLLs1fV9PQ==} + dependencies: + source-map: 0.6.1 + dev: true + /@types/validator/13.6.3: resolution: {integrity: sha512-fWG42pMJOL4jKsDDZZREnXLjc3UE0R8LOJfARWYg6U966rxDT7TYejYzLnUF5cvSObGg34nd0+H2wHHU5Omdfw==} dev: false @@ -3061,6 +3090,13 @@ packages: engines: {node: '>=6'} dev: true + /camel-case/3.0.0: + resolution: {integrity: sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=} + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + dev: false + /camel-case/4.1.2: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} dependencies: @@ -3161,6 +3197,13 @@ packages: validator: 13.6.0 dev: false + /clean-css/4.2.3: + resolution: {integrity: sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==} + engines: {node: '>= 4.0'} + dependencies: + source-map: 0.6.1 + dev: false + /clean-stack/2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -4590,6 +4633,11 @@ packages: dependencies: function-bind: 1.1.1 + /he/1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: false + /hexoid/1.0.0: resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} engines: {node: '>=8'} @@ -4610,6 +4658,20 @@ packages: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true + /html-minifier/4.0.0: + resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==} + engines: {node: '>=6'} + hasBin: true + dependencies: + camel-case: 3.0.0 + clean-css: 4.2.3 + commander: 2.20.3 + he: 1.2.0 + param-case: 2.1.1 + relateurl: 0.2.7 + uglify-js: 3.14.2 + dev: false + /http-errors/1.7.3: resolution: {integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==} engines: {node: '>= 0.6'} @@ -5963,6 +6025,10 @@ packages: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} dev: false + /lower-case/1.1.4: + resolution: {integrity: sha1-miyr0bno4K6ZOkv31YdcOcQujqw=} + dev: false + /lower-case/2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: @@ -6307,6 +6373,12 @@ packages: reflect-metadata: 0.1.13 dev: false + /no-case/2.3.2: + resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} + dependencies: + lower-case: 1.1.4 + dev: false + /no-case/3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: @@ -6613,6 +6685,12 @@ packages: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} dev: false + /param-case/2.1.1: + resolution: {integrity: sha1-35T9jPZTHs915r75oIWPvHK+Ikc=} + dependencies: + no-case: 2.3.2 + dev: false + /parent-module/1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -7089,6 +7167,11 @@ packages: engines: {node: '>=8'} dev: true + /relateurl/0.2.7: + resolution: {integrity: sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=} + engines: {node: '>= 0.10'} + dev: false + /request/2.88.2: resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} engines: {node: '>= 6'} @@ -7479,7 +7562,6 @@ packages: /source-map/0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - dev: true /source-map/0.7.3: resolution: {integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==} @@ -8157,6 +8239,12 @@ packages: resolution: {integrity: sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==} dev: false + /uglify-js/3.14.2: + resolution: {integrity: sha512-rtPMlmcO4agTUfz10CbgJ1k6UAoXM2gWb3GoMPPZB/+/Ackf8lNWk11K4rYi2D0apgoFRLtQOZhb+/iGNJq26A==} + engines: {node: '>=0.8.0'} + hasBin: true + dev: false + /unbox-primitive/1.0.1: resolution: {integrity: sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==} dependencies: @@ -8174,6 +8262,10 @@ packages: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} + /upper-case/1.1.3: + resolution: {integrity: sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=} + dev: false + /uri-js/4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: diff --git a/src/modules/markdown/markdown.controller.ts b/src/modules/markdown/markdown.controller.ts index f6aab6d6..6d0a8662 100644 --- a/src/modules/markdown/markdown.controller.ts +++ b/src/modules/markdown/markdown.controller.ts @@ -11,15 +11,22 @@ import { import { ApiProperty, ApiQuery } from '@nestjs/swagger' import dayjs from 'dayjs' import { FastifyReply } from 'fastify' +import { minify } from 'html-minifier' import JSZip from 'jszip' import { join } from 'path' +import { performance } from 'perf_hooks' import { Readable } from 'stream' +import { URL } from 'url' import { Auth } from '~/common/decorator/auth.decorator' import { HTTPDecorators } from '~/common/decorator/http.decorator' import { ApiName } from '~/common/decorator/openapi.decorator' import { AssetService } from '~/processors/helper/hepler.asset.service' import { MongoIdDto } from '~/shared/dto/id.dto' import { CategoryModel } from '../category/category.model' +import { ConfigsService } from '../configs/configs.service' +import { NoteModel } from '../note/note.model' +import { PageModel } from '../page/page.model' +import { PostModel } from '../post/post.model' import { ArticleType, DataListDto } from './markdown.dto' import { MarkdownYAMLProperty } from './markdown.interface' import { MarkdownService } from './markdown.service' @@ -31,6 +38,7 @@ export class MarkdownController { private readonly service: MarkdownService, private readonly assetService: AssetService, + private readonly configs: ConfigsService, ) {} @Post('/import') @@ -163,14 +171,33 @@ export class MarkdownController { @CacheTTL(60 * 60 * 24) async renderArticle(@Param() params: MongoIdDto, @Res() reply: FastifyReply) { const { id } = params - const { html: markdown, document } = await this.service.renderArticle(id) + const now = performance.now() + const { + html: markdown, + document, + type, + } = await this.service.renderArticle(id) const style = await this.assetService.getAsset('markdown.css', { encoding: 'utf8', }) + const relativePath = (() => { + switch (type) { + case 'post': + return `/posts/${((document as PostModel).category as any).slug}/${ + (document as PostModel).slug + }` + case 'note': + return `/notes/${(document as NoteModel).nid}` + case 'page': + return `/${(document as PageModel).slug}` + } + })() + const url = new URL(relativePath, this.configs.get('url').webUrl) reply.type('text/html').send( - ` + minify( + `
@@ -186,26 +213,46 @@ export class MarkdownController {本文渲染于 ${dayjs().format('DD/MM/YYYY')}
${markdown}