feat: marked parse

This commit is contained in:
Innei
2021-09-13 13:39:13 +08:00
parent a1e1682852
commit b1785c2c8f
15 changed files with 747 additions and 936 deletions

View File

@@ -12,3 +12,15 @@ body {
width: 800px;
margin: auto;
}
.mermaid {
text-align: center;
}
.mermaid > svg {
margin: auto;
}
img {
max-width: 100%;
}

612
assets/newsprint.css Normal file
View File

@@ -0,0 +1,612 @@
/* meyer reset -- http://meyerweb.com/eric/tools/css/reset/ , v2.0 | 20110126 | License: none (public domain) */
@include-when-export url(https://fonts.loli.net/css?family=PT+Serif:400,400italic,700,700italic&subset=latin,cyrillic-ext,cyrillic,latin-ext);
/* =========== */
/* pt-serif-regular - latin */
@font-face {
font-family: 'PT Serif';
font-style: normal;
font-weight: normal;
src: local('PT Serif'), local('PTSerif-Regular'), url('./newsprint/pt-serif-v11-latin-regular.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* pt-serif-italic - latin */
@font-face {
font-family: 'PT Serif';
font-style: italic;
font-weight: normal;
src: local('PT Serif Italic'), local('PTSerif-Italic'), url('./newsprint/pt-serif-v11-latin-italic.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* pt-serif-700 - latin */
@font-face {
font-family: 'PT Serif';
font-style: normal;
font-weight: bold;
src: local('PT Serif Bold'), local('PTSerif-Bold'), url('./newsprint/pt-serif-v11-latin-700.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* pt-serif-700italic - latin */
@font-face {
font-family: 'PT Serif';
font-style: italic;
font-weight: bold;
src: local('PT Serif Bold Italic'), local('PTSerif-BoldItalic'), url('./newsprint/pt-serif-v11-latin-700italic.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root {
--active-file-bg-color: #dadada;
--active-file-bg-color: rgba(32, 43, 51, 0.63);
--active-file-text-color: white;
--bg-color: #f3f2ee;
--text-color: #1f0909;
--control-text-color: #444;
--rawblock-edit-panel-bd: #e5e5e5;
--select-text-bg-color: rgba(32, 43, 51, 0.63);
--select-text-font-color: white;
}
pre {
--select-text-bg-color: #36284e;
--select-text-font-color: #fff;
}
html {
font-size: 16px;
-webkit-font-smoothing: antialiased;
}
html, body {
background-color: #f3f2ee;
font-family: "PT Serif", 'Times New Roman', Times, serif;
color: #1f0909;
line-height: 1.5em;
}
/*#write {
overflow-x: auto;
max-width: initial;
padding-left: calc(50% - 17em);
padding-right: calc(50% - 17em);
}
@media (max-width: 36em) {
#write {
padding-left: 1em;
padding-right: 1em;
}
}*/
#write {
max-width: 40em;
}
@media only screen and (min-width: 1400px) {
#write {
max-width: 914px;
}
}
ol li {
list-style-type: decimal;
list-style-position: outside;
}
ul li {
list-style-type: disc;
list-style-position: outside;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/* styles */
/* ====== */
/* headings */
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: bold;
}
h1 {
font-size: 1.875em;
/*30 / 16*/
line-height: 1.6em;
/* 48 / 30*/
margin-top: 2em;
}
h2,
h3 {
font-size: 1.3125em;
/*21 / 16*/
line-height: 1.15;
/*24 / 21*/
margin-top: 2.285714em;
/*48 / 21*/
margin-bottom: 1.15em;
/*24 / 21*/
}
h3 {
font-weight: normal;
}
h4 {
font-size: 1.125em;
/*18 / 16*/
margin-top: 2.67em;
/*48 / 18*/
}
h5,
h6 {
font-size: 1em;
/*16*/
}
h1 {
border-bottom: 1px solid;
margin-bottom: 1.875em;
padding-bottom: 0.8125em;
}
/* links */
a {
text-decoration: none;
color: #065588;
}
a:hover,
a:active {
text-decoration: underline;
}
/* block spacing */
p,
blockquote,
.md-fences {
margin-bottom: 1.5em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-bottom: 1.5em;
}
/* blockquote */
blockquote {
font-style: italic;
border-left: 5px solid;
margin-left: 2em;
padding-left: 1em;
}
/* lists */
ul,
ol {
margin: 0 0 1.5em 1.5em;
}
/* tables */
.md-meta,.md-before, .md-after {
color:#999;
}
table {
margin-bottom: 1.5em;
/*24 / 16*/
font-size: 1em;
/* width: 100%; */
}
thead th,
tfoot th {
padding: .25em .25em .25em .4em;
text-transform: uppercase;
}
th {
text-align: left;
}
td {
vertical-align: top;
padding: .25em .25em .25em .4em;
}
code,
.md-fences {
background-color: #dadada;
}
code {
padding-left: 2px;
padding-right: 2px;
}
.md-fences {
margin-left: 2em;
margin-bottom: 3em;
padding-left: 1ch;
padding-right: 1ch;
}
pre,
code,
tt {
font-size: .875em;
line-height: 1.714285em;
}
/* some fixes */
h1 {
line-height: 1.3em;
font-weight: normal;
margin-bottom: 0.5em;
}
p + ul,
p + ol{
margin-top: .5em;
}
h3 + ul,
h4 + ul,
h5 + ul,
h6 + ul,
h3 + ol,
h4 + ol,
h5 + ol,
h6 + ol {
margin-top: .5em;
}
li > ul,
li > ol {
margin-top: inherit;
margin-bottom: 0;
}
li ol>li {
list-style-type: lower-alpha;
}
li li ol>li{
list-style-type: lower-roman;
}
h2,
h3 {
margin-bottom: .75em;
}
hr {
border-top: none;
border-right: none;
border-bottom: 1px solid;
border-left: none;
}
h1 {
border-color: #c5c5c5;
}
blockquote {
border-color: #bababa;
color: #656565;
}
blockquote ul,
blockquote ol {
margin-left:0;
}
.ty-table-edit {
background-color: transparent;
}
thead {
background-color: #dadada;
}
tr:nth-child(even) {
background: #e8e7e7;
}
hr {
border-color: #c5c5c5;
}
.task-list{
padding-left: 1rem;
}
.md-task-list-item {
padding-left: 1.5rem;
list-style-type: none;
}
.md-task-list-item > input:before {
content: '\221A';
display: inline-block;
width: 1.25rem;
height: 1.6rem;
vertical-align: middle;
text-align: center;
color: #ddd;
background-color: #F3F2EE;
}
.md-task-list-item > input:checked:before,
.md-task-list-item > input[checked]:before{
color: inherit;
}
#write pre.md-meta-block {
min-height: 1.875rem;
color: #555;
border: 0px;
background: transparent;
margin-top: -4px;
margin-left: 1em;
margin-top: 1em;
}
.md-image>.md-meta {
color: #9B5146;
}
.md-image>.md-meta{
font-family: Menlo, 'Ubuntu Mono', Consolas, 'Courier New', 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', serif;
}
#write>h3.md-focus:before{
left: -1.5rem;
color:#999;
border-color:#999;
}
#write>h4.md-focus:before{
left: -1.5rem;
top: .25rem;
color:#999;
border-color:#999;
}
#write>h5.md-focus:before{
left: -1.5rem;
top: .0.3125rem;
color:#999;
border-color:#999;
}
#write>h6.md-focus:before{
left: -1.5rem;
top: 0.3125rem;
color:#999;
border-color:#999;
}
.md-toc:focus .md-toc-content{
margin-top: 19px;
}
.md-toc-content:empty:before{
color: #065588;
}
.md-toc-item {
color: #065588;
}
#write div.md-toc-tooltip {
background-color: #f3f2ee;
}
#typora-sidebar {
background-color: #f3f2ee;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.375);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.375);
}
.pin-outline #typora-sidebar {
background: inherit;
box-shadow: none;
border-right: 1px dashed;
}
.pin-outline #typora-sidebar:hover .outline-title-wrapper {
border-left:1px dashed;
}
.outline-item:hover {
background-color: #dadada;
border-left: 28px solid #dadada;
border-right: 18px solid #dadada;
}
.typora-node .outline-item:hover {
border-right: 28px solid #dadada;
}
.outline-expander:before {
content: "\f0da";
font-family: FontAwesome;
font-size:14px;
top: 1px;
}
.outline-expander:hover:before,
.outline-item-open>.outline-item>.outline-expander:before {
content: "\f0d7";
}
.modal-content {
background-color: #f3f2ee;
}
.auto-suggest-container ul li {
list-style-type: none;
}
/** UI for electron */
.megamenu-menu,
#top-titlebar, #top-titlebar *,
.megamenu-content {
background: #f3f2ee;
color: #1f0909;
}
.megamenu-menu-header {
border-bottom: 1px dashed #202B33;
}
.megamenu-menu {
box-shadow: none;
border-right: 1px dashed;
}
header, .context-menu, .megamenu-content, footer {
font-family: "PT Serif", 'Times New Roman', Times, serif;
color: #1f0909;
}
#megamenu-back-btn {
color: #1f0909;
border-color: #1f0909;
}
.megamenu-menu-header #megamenu-menu-header-title:before {
color: #1f0909;
}
.megamenu-menu-list li a:hover, .megamenu-menu-list li a.active {
color: inherit;
background-color: #e8e7df;
}
.long-btn:hover {
background-color: #e8e7df;
}
#recent-file-panel tbody tr:nth-child(2n-1) {
background-color: transparent !important;
}
.megamenu-menu-panel tbody tr:hover td:nth-child(2) {
color: inherit;
}
.megamenu-menu-panel .btn {
background-color: #D2D1D1;
}
.btn-default {
background-color: transparent;
}
.typora-sourceview-on #toggle-sourceview-btn,
.ty-show-word-count #footer-word-count {
background: #c7c5c5;
}
#typora-quick-open {
background-color: inherit;
}
.md-diagram-panel {
margin-top: 8px;
}
.file-list-item-file-name {
font-weight: initial;
}
.file-list-item-summary {
opacity: 1;
}
.file-list-item {
color: #777;
}
.file-list-item.active {
background-color: inherit;
color: black;
}
.ty-side-sort-btn.active {
background-color: inherit;
}
.file-list-item.active .file-list-item-file-name {
font-weight: bold;
}
.file-list-item{
opacity:1 !important;
}
.file-library-node.active>.file-node-background{
background-color: rgba(32, 43, 51, 0.63);
background-color: var(--active-file-bg-color);
}
.file-tree-node.active>.file-node-content{
color: white;
color: var(--active-file-text-color);
}
.md-task-list-item>input {
margin-left: -1.7em;
margin-top: calc(1rem - 12px);
}
input {
border: 1px solid #aaa;
}
.megamenu-menu-header #megamenu-menu-header-title,
.megamenu-menu-header:hover,
.megamenu-menu-header:focus {
color: inherit;
}
.dropdown-menu .divider {
border-color: #e5e5e5;
opacity: 1;
}
/* https://github.com/typora/typora-issues/issues/2046 */
.os-windows-7 strong,
.os-windows-7 strong {
font-weight: 760;
}
.ty-preferences .btn-default {
background: transparent;
}
.ty-preferences .window-header {
border-bottom: 1px dashed #202B33;
box-shadow: none;
}
#sidebar-loading-template, #sidebar-loading-template.file-list-item {
color: #777;
}
.searchpanel-search-option-btn.active {
background: #777;
color: white;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -77,6 +77,7 @@
"js-yaml": "*",
"jszip": "3.7.1",
"lodash": "*",
"marked": "^3.0.3",
"mdurl": "*",
"mkdirp": "*",
"mongoose": "*",
@@ -92,17 +93,11 @@
"pluralize": "*",
"redis": "3.1.2",
"reflect-metadata": "0.1.13",
"rehype-stringify": "9.0.2",
"remark-gfm": "2.0.0",
"remark-html": "^13.0.1",
"remark-parse": "^5.0.0",
"remark-rehype": "9.0.0",
"rxjs": "7.3.0",
"snakecase-keys": "4.0.2",
"ts-morph": "*",
"ua-parser-js": "0.7.28",
"unified": "^9",
"unist-builder": "^2",
"xss": "^1.0.9",
"yargs": "*",
"zx": "4.2.0"
},
@@ -118,6 +113,7 @@
"@types/ioredis": "4.27.2",
"@types/jest": "27.0.1",
"@types/lodash": "4.14.172",
"@types/marked": "^3.0.0",
"@types/mongoose-paginate-v2": "1.3.11",
"@types/node": "16.9.1",
"@types/nodemailer": "6.4.4",

782
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

9
scripts/assets-push.sh Normal file
View File

@@ -0,0 +1,9 @@
cd assets
git init
git add .
git commit -m 'update assets'
git remote add origin git@github.com:mx-space/assets.git
git branch -M master
git push -u origin master -f
rm -rf .git

View File

@@ -7,7 +7,6 @@ import { Readable } from 'stream'
import { Auth } from '~/common/decorator/auth.decorator'
import { HTTPDecorators } from '~/common/decorator/http.decorator'
import { ApiName } from '~/common/decorator/openapi.decorator'
import { HttpService } from '~/processors/helper/helper.http.service'
import { AssetService } from '~/processors/helper/hepler.asset.service'
import { MongoIdDto } from '~/shared/dto/id.dto'
import { CategoryModel } from '../category/category.model'
@@ -20,7 +19,7 @@ import { MarkdownService } from './markdown.service'
export class MarkdownController {
constructor(
private readonly service: MarkdownService,
private readonly httpService: HttpService,
private readonly assetService: AssetService,
) {}
@@ -154,13 +153,7 @@ export class MarkdownController {
async renderArticle(@Param() params: MongoIdDto, @Res() reply: FastifyReply) {
const { id } = params
const { html: markdown, document } = await this.service.renderArticle(id)
const [theme] = await Promise.all([
this.httpService.axiosRef
.get(
'https://gitee.com/xun7788/mx-server-assets/raw/master/newsprint.css',
)
.then((r) => r.data),
])
const style = await this.assetService.getAsset('markdown.css', {
encoding: 'utf8',
})
@@ -176,14 +169,25 @@ export class MarkdownController {
<meta name="referrer" content="no-referrer">
<style>
${style}
${theme}
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/mx-space/assets@master/newsprint.css">
<title>${document.title}</title>
</head>
<body>
<h1>${document.title}</h1>
<article>
<h1>${document.title}</h1>
${markdown}
</article>
</body>
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script>
window.mermaid.initialize({
theme: 'default',
startOnLoad: false,
})
window.mermaid.init(undefined, '.mermaid')
</script>
</html>
`

View File

@@ -3,21 +3,16 @@ import { ReturnModelType } from '@typegoose/typegoose'
import { dump } from 'js-yaml'
import JSZip from 'jszip'
import { omit } from 'lodash'
import normalize from 'mdurl/encode.js'
import marked from 'marked'
import { Types } from 'mongoose'
import { InjectModel } from 'nestjs-typegoose'
import html from 'remark-html'
import markdown from 'remark-parse'
import unified from 'unified'
import u from 'unist-builder'
import xss from 'xss'
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'
import rules from './rules'
@Injectable()
export class MarkdownService {
private logger: Logger
@@ -226,57 +221,87 @@ ${text.trim()}
}
private render(text: string) {
const parser = unified()
.use(markdown)
.use(rules)
// @ts-ignore
.use(html, {
allowDangerousHtml: true,
handlers: {
image: (h, node: any) => {
// console.log(node)
const src = node.url as string
const _alt = node.alt as string | undefined
const alt = _alt?.match(/^[!¡]/) ? _alt!.replace(/^[¡!]/, '') : ''
if (!alt) {
return h(node, 'img', { src })
marked.use({
gfm: true,
sanitize: false,
extensions: [
{
level: 'inline',
name: 'spoiler',
start(src) {
return src.match(/\|/)?.index
},
renderer(token) {
// @ts-ignore
return `<span class="spoiler" style="filter: invert(25%)">${this.parser.parseInline(
token.text,
)}\n</span>`
},
tokenizer(src, tokens) {
const rule = /^\|\|([\s\S]+?)\|\|(?!\|)/ // Regex for the complete token
const match = rule.exec(src)
if (match) {
return {
// Token to generate
type: 'spoiler', // Should match "name" above
raw: match[0], // Text to consume from the source
// @ts-ignore
text: this.lexer.inlineTokens(match[1].trim()),
}
}
return h(node, 'figure', {}, [
h(node, 'img', { src: normalize(src) }),
h.augment(
node,
u(
'raw',
`<figcaption style="text-align: center; margin: 1em auto;">${this.escapeHTMLTag(
alt,
)}</figcaption>`,
),
),
])
},
spoiler: (h, node: any) => {
return h(node, 'del', {
class: 'spoiler',
style: 'filter: invert(25%);',
})
},
childTokens: ['text'],
},
} as any)
{
level: 'inline',
name: 'mention',
start(src) {
return src.match(/\(/)?.index
},
renderer(token) {
// @ts-ignore
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?(?!\[.*?\])/ // Regex for the complete token
const match = rule.exec(src)
if (match) {
return {
// Token to generate
type: 'mention', // Should match "name" above
raw: match[0], // Text to consume from the source
text: this.lexer.inlineTokens(match[1].trim(), []),
}
}
},
childTokens: ['text'],
},
],
return parser.processSync(text).toString()
}
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>`
},
private escapeHTMLTag(html: string) {
const lt = /</g,
gt = />/g,
ap = /'/g,
ic = /"/g
return html
.toString()
.replace(lt, '&lt;')
.replace(gt, '&gt;')
.replace(ap, '&#39;')
.replace(ic, '&#34;')
code(code, lang) {
if (lang == 'mermaid') {
return '<div class="mermaid">' + code + '</div>'
} else {
return '<pre><code>' + code + '</code></pre>'
}
},
},
})
return marked(text)
}
}

View File

@@ -1,6 +0,0 @@
import { mentions } from './mentions'
import { spoiler } from './spoiler'
export const rules = { mentions, spoiler }
export default Object.values(rules)

View File

@@ -1,46 +0,0 @@
/*
* @Author: Innei
* @Date: 2020-06-11 13:01:08
* @LastEditTime: 2020-06-12 20:19:16
* @LastEditors: Innei
* @FilePath: /mx-web/common/markdown/rules/mentions.ts
* @Coding with Love
*/
/**
* parse (@username) to github user profile
*/
function tokenizeMention(eat: any, value: string, silent?: boolean): any {
const match = /\((@(\w+\b))\)\s(?!\[.*?\])/.exec(value)
if (match) {
if (silent) {
return true
}
try {
return eat(match[0])({
type: 'link',
url: 'https://github.com/' + match[2],
children: [{ type: 'text', value: match[1] }],
})
// eslint-disable-next-line no-empty
} catch {}
}
}
tokenizeMention.notInLink = true
tokenizeMention.locator = locateMention
function locateMention(value, fromIndex) {
return value.indexOf('@', fromIndex)
}
function mentions(this: any) {
const Parser = this.Parser as any
const tokenizers = Parser.prototype.inlineTokenizers
const methods = Parser.prototype.inlineMethods
// Add an inline tokenizer (defined in the following example).
tokenizers.mention = tokenizeMention
// Run it just before `text`.
methods.splice(methods.indexOf('text'), 0, 'mention')
}
export { mentions }

View File

@@ -1,42 +0,0 @@
/*
* @Author: Innei
* @Date: 2020-06-11 13:31:05
* @LastEditTime: 2020-09-02 20:03:18
* @LastEditors: Innei
* @FilePath: /mx-web/common/markdown/rules/spoiler.ts
* @Coding with Love
*/
function tokenizeSpoiler(eat: any, value: string, silent?: boolean): any {
const match = /^\|\|([\s\S]+?)\|\|(?!\|)/.exec(value)
if (match) {
if (silent) {
return true
}
try {
return eat(match[0])({
type: 'spoiler',
value: match[1],
})
// eslint-disable-next-line no-empty
} catch {}
}
}
tokenizeSpoiler.notInLink = true
tokenizeSpoiler.locator = function (value, fromIndex) {
return value.indexOf('||', fromIndex)
}
function spoiler(this: any) {
const Parser = this.Parser as any
const tokenizers = Parser.prototype.inlineTokenizers
const methods = Parser.prototype.inlineMethods
// Add an inline tokenizer (defined in the following example).
tokenizers.spoiler = tokenizeSpoiler
// Run it just before `text`.
methods.splice(methods.indexOf('text'), 0, 'spoiler')
}
export { spoiler }

View File

@@ -21,6 +21,9 @@ export class AssetService {
}
public assetPath = path.resolve(process.cwd(), 'assets')
// 在线资源的地址 `/` 结尾
private onlineAssetPath =
'https://cdn.jsdelivr.net/gh/mx-space/assets@master/'
private checkRoot() {
if (!fs.existsSync(this.assetPath)) {
@@ -48,7 +51,7 @@ export class AssetService {
try {
// 去线上拉取
const { data } = await this.httpService.axiosRef.get(
`https://gitee.com/xun7788/mx-server-assets/raw/master/` + path,
this.onlineAssetPath + path,
)
fs.mkdirSync(