import { BadRequestException , Injectable , Logger } from '@nestjs/common' ; import { ReturnModelType } from '@typegoose/typegoose' ; import { dump } from 'js-yaml' ; import JSZip from 'jszip' ; import { omit } from 'lodash' ; import { marked } from 'marked' ; import { Types } from 'mongoose' ; import { InjectModel } from 'nestjs-typegoose' ; import xss from 'xss' ; import { DatabaseService } from '~/processors/database/database.service' ; import { AssetService } from '~/processors/helper/helper.asset.service' ; import { CategoryModel } from '../category/category.model' ; import { NoteModel } from '../note/note.model' ; import { PageModel } from '../page/page.model' ; import { PostModel } from '../post/post.model' ; import { DatatypeDto } from './markdown.dto' ; import { MarkdownYAMLProperty } from './markdown.interface' ; @ Injectable ( ) export class MarkdownService { constructor ( private readonly assetService , @ InjectModel ( CategoryModel ) private readonly categoryModel , @ InjectModel ( PostModel ) private readonly postModel , @ InjectModel ( NoteModel ) private readonly noteModel , @ InjectModel ( PageModel ) private readonly pageModel , private readonly databaseService ) { } async insertPostsToDb ( data ) { let count = 1 ; const categoryNameAndId = ( await this . categoryModel . find ( ) . lean ( ) ) . map ( ( c ) => { return { name : c . name , _id : c . _id , slug : c . slug } ; } ) ; const insertOrCreateCategory = async ( name ) => { if ( ! name ) { return ; } const hasCategory = categoryNameAndId . find ( ( c ) => name === c . name || name === c . slug ) ; if ( ! hasCategory ) { const newCategoryDoc = await this . categoryModel . create ( { name , slug : name , type : 0 , } ) ; categoryNameAndId . push ( { name : newCategoryDoc . name , _id : newCategoryDoc . _id , slug : newCategoryDoc . slug , } ) ; await newCategoryDoc . save ( ) ; return newCategoryDoc ; } else { return hasCategory ; } } ; const genDate = this . genDate ; const models = [ ] as PostModel [ ] ; const defaultCategory = await this . categoryModel . findOne ( ) ; for await ( const item of data ) { if ( ! item . meta ) { models . push ( { title : '未命名-' + count ++ , slug : new Date ( ) . getTime ( ) , text : item . text , ... genDate ( item ) , categoryId : new Types . ObjectId ( defaultCategory . _id ) , } as any as PostModel ) ; } else { const category = await insertOrCreateCategory ( item . meta . categories ?. shift ( ) ) ; models . push ( { title : item . meta . title , slug : item . meta . slug || item . meta . title , text : item . text , ... genDate ( item ) , categoryId : category ?. _id || defaultCategory . _id , } as PostModel ) ; } } return await this . postModel . insertMany ( models , { ordered : false } ) . catch ( ( err ) => { Logger . log ( '一篇文章导入失败' , MarkdownService . name ) ; } ) ; } async insertNotesToDb ( data ) { const models = [ ] as NoteModel [ ] ; for await ( const item of data ) { if ( ! item . meta ) { models . push ( { title : '未命名记录' , text : item . text , ... this . genDate ( item ) , } as NoteModel ) ; } else { models . push ( { title : item . meta . title , text : item . text , ... this . genDate ( item ) , } as NoteModel ) ; } } return await this . noteModel . create ( models ) ; } private readonly genDate = ( item ) => { const { meta } = item ; if ( ! meta ) { return { created : new Date ( ) , modified : new Date ( ) , } ; } const { date , updated } = meta ; return { created : date ? new Date ( date ) : new Date ( ) , modified : updated ? new Date ( updated ) : date ? new Date ( date ) : new Date ( ) , } ; } ; async extractAllArticle ( ) { return { posts : await this . postModel . find ( ) . populate ( 'category' ) , notes : await this . noteModel . find ( ) . lean ( ) , pages : await this . pageModel . find ( ) . lean ( ) , } ; } async generateArchive ( { documents , options = { } , } ) { const zip = new JSZip ( ) ; for ( const document of documents ) { zip . file ( ( options . slug ? document . meta . slug : document . meta . title ) . concat ( '.md' ) . replace ( /\//g , '-' ) , document . text ) ; } return zip ; } markdownBuilder ( property , includeYAMLHeader ? , showHeader ? ) { const { meta : { created , modified , title } , text , } = property ; if ( ! includeYAMLHeader ) { return `${ showHeader ? `# ${ title }\n\n` : '' }${ text . trim ( ) }` ; } const header = { date : created , updated : modified , title , ... omit ( property . meta , [ 'created' , 'modified' , 'title' ] ) , } ; const toYaml = dump ( header , { skipInvalid : true } ) ; const res = `
---
${ toYaml . trim ( ) }
---

${ showHeader ? `# ${ title }\n\n` : '' }
${ text . trim ( ) }
` . trim ( ) ; return res ; } param id returns async renderArticle ( id ) { const doc = await this . databaseService . findGlobalById ( id ) ; if ( ! doc . document ) { throw new BadRequestException ( '文档不存在' ) ; } return { html : this . renderMarkdownContent ( doc . document . text ) , ... doc , } ; } param text returns public renderMarkdownContent ( text ) { marked . use ( { gfm : true , sanitize : false , extensions : [ { level : 'inline' , name : 'spoiler' , start ( src ) { return src . match ( /\|/ ) ?. index ; } , renderer ( token ) { return `<span class="spoiler" style="filter: invert(25%)">${ this . parser . parseInline ( token . text ) }\n</span>` ; } , tokenizer ( src , tokens ) { const rule = /^\|\|([\s\S]+?)\|\|(?!\|)/ ; const match = rule . exec ( src ) ; if ( match ) { return { type : 'spoiler' , raw : match [ 0 ] , text : this . lexer . inlineTokens ( match [ 1 ] . trim ( ) ) , } ; } } , childTokens : [ 'text' ] , } , { level : 'inline' , name : 'mention' , start ( src ) { return src . match ( /\(/ ) ?. index ; } , renderer ( token ) { const username = this . parser . parseInline ( token . text ) . slice ( 1 ) ; return `<a class="mention" rel="noreferrer nofollow" href="https://github.com/${ username }" target="_blank">@${ username }\n</a>` ; } , tokenizer ( src , tokens ) { const rule = /^\((@(\w+\b))\)\s?(?!\[.*?\])/ ; const match = rule . exec ( src ) ; if ( match ) { return { type : 'mention' , raw : match [ 0 ] , text : this . lexer . inlineTokens ( match [ 1 ] . trim ( ) , [ ] ) , } ; } } , childTokens : [ 'text' ] , } , ] , renderer : { image ( src , title , _alt ) { const alt = _alt ?. match ( /^[!¡]/ ) ? _alt . replace ( /^[¡!]/ , '' ) : '' ; if ( ! alt ) { return `<img src="${ xss ( src ) }"/>` ; } return `<figure>
          <img src="${ xss ( src ) }"/>
          <figcaption style="text-align: center; margin: 1em auto;">${ xss ( alt ) }</figcaption></figure>` ; } , code ( code , lang ) { if ( lang == 'mermaid' ) { return '<pre class="mermaid">' + code + '</pre>' ; } else { return ( `<pre><code class="language-${ lang }">` + xss ( code ) + '</code></pre>' ) ; } } , } , } ) ; return marked ( text ) ; } async getRenderedMarkdownHtmlStructure ( html , title , theme = 'newsprint' ) { const style = await this . assetService . getAsset ( '/markdown/markdown.css' , { encoding : 'utf8' , } ) ; const themeStyleSheet = await this . assetService . getAsset ( '/markdown/theme/' + theme + '.css' , { encoding : 'utf-8' } ) ; return { body : [ `<article><h1>${ title }</h1>${ html }</article>` ] , extraScripts : [ '<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/mermaid/8.9.0/mermaid.min.js"></script>' , '<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.23.0/components/prism-core.min.js"></script>' , '<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.23.0/plugins/autoloader/prism-autoloader.min.js"></script>' , '<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.23.0/plugins/line-numbers/prism-line-numbers.min.js" />' , ] , script : [ `window.mermaid.initialize({theme: 'default',startOnLoad: false})` , `window.mermaid.init(undefined, '.mermaid')` , ] , link : [ '<link href="https://cdn.jsdelivr.net/gh/PrismJS/prism-themes@master/themes/prism-one-light.css" rel="stylesheet" />' , '<link href="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.23.0/plugins/line-numbers/prism-line-numbers.min.css" rel="stylesheet" />' , ] , style : [ style , themeStyleSheet ] , } ; } getMarkdownEjsRenderTemplate ( ) { return this . assetService . getAsset ( '/render/markdown.ejs' , { encoding : 'utf8' , } ) as Promise < string > ; } }