From 63206e71b8c5c2d6c96bc0695846c5b23c2d41b7 Mon Sep 17 00:00:00 2001
From: Miao_Spring <539943419@qq.com>
Date: Mon, 20 Jan 2025 01:15:38 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96markdown=E6=B8=B2?=
=?UTF-8?q?=E6=9F=93?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.node-version | 2 +-
components/codeMirror.vue | 69 +++++++++++++
components/markdownRender.vue | 57 +++++++++--
package.json | 3 +
pnpm-lock.yaml | 149 +++++++++++++++++++++++++++
utils/html2VNode.ts | 188 ++++++++++++++++++++++++++++++++++
6 files changed, 460 insertions(+), 8 deletions(-)
create mode 100644 components/codeMirror.vue
create mode 100644 utils/html2VNode.ts
diff --git a/.node-version b/.node-version
index e8aa644..c12134b 100644
--- a/.node-version
+++ b/.node-version
@@ -1 +1 @@
-v20.18.1
+v20.15.0
diff --git a/components/codeMirror.vue b/components/codeMirror.vue
new file mode 100644
index 0000000..6b3f583
--- /dev/null
+++ b/components/codeMirror.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/markdownRender.vue b/components/markdownRender.vue
index d0a18b7..00c2759 100644
--- a/components/markdownRender.vue
+++ b/components/markdownRender.vue
@@ -2,12 +2,15 @@
-
\ No newline at end of file
diff --git a/package.json b/package.json
index 52bf516..f132eac 100644
--- a/package.json
+++ b/package.json
@@ -10,10 +10,13 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
+ "@codemirror/lang-javascript": "^6.2.2",
+ "@codemirror/state": "^6.5.1",
"@pinia/nuxt": "^0.9.0",
"@types/node": "^22.10.5",
"bcryptjs": "^2.4.3",
"better-sqlite3": "11.3.0",
+ "codemirror": "^6.0.1",
"dayjs": "^1.11.13",
"jsonwebtoken": "^9.0.2",
"markdown-it": "^14.1.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a197e0b..9701c85 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,12 @@ importers:
.:
dependencies:
+ '@codemirror/lang-javascript':
+ specifier: ^6.2.2
+ version: 6.2.2
+ '@codemirror/state':
+ specifier: ^6.5.1
+ version: 6.5.1
'@pinia/nuxt':
specifier: ^0.9.0
version: 0.9.0(magicast@0.3.5)(pinia@2.3.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))(rollup@4.30.1)
@@ -20,6 +26,9 @@ importers:
better-sqlite3:
specifier: 11.3.0
version: 11.3.0
+ codemirror:
+ specifier: ^6.0.1
+ version: 6.0.1
dayjs:
specifier: ^1.11.13
version: 1.11.13
@@ -226,6 +235,30 @@ packages:
resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==}
engines: {node: '>=16.13'}
+ '@codemirror/autocomplete@6.18.4':
+ resolution: {integrity: sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==}
+
+ '@codemirror/commands@6.8.0':
+ resolution: {integrity: sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==}
+
+ '@codemirror/lang-javascript@6.2.2':
+ resolution: {integrity: sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==}
+
+ '@codemirror/language@6.10.8':
+ resolution: {integrity: sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==}
+
+ '@codemirror/lint@6.8.4':
+ resolution: {integrity: sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==}
+
+ '@codemirror/search@6.5.8':
+ resolution: {integrity: sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==}
+
+ '@codemirror/state@6.5.1':
+ resolution: {integrity: sha512-3rA9lcwciEB47ZevqvD8qgbzhM9qMb8vCcQCNmDfVRPQG4JT9mSb0Jg8H7YjKGGQcFnLN323fj9jdnG59Kx6bg==}
+
+ '@codemirror/view@6.36.2':
+ resolution: {integrity: sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==}
+
'@ctrl/tinycolor@3.6.1':
resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
engines: {node: '>=10'}
@@ -576,11 +609,26 @@ packages:
'@kwsites/promise-deferred@1.1.1':
resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==}
+ '@lezer/common@1.2.3':
+ resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
+
+ '@lezer/highlight@1.2.1':
+ resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==}
+
+ '@lezer/javascript@1.4.21':
+ resolution: {integrity: sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==}
+
+ '@lezer/lr@1.4.2':
+ resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==}
+
'@mapbox/node-pre-gyp@2.0.0-rc.0':
resolution: {integrity: sha512-nhSMNprz3WmeRvd8iUs5JqkKr0Ncx46JtPxM3AhXes84XpSJfmIwKeWXRpsr53S7kqPkQfPhzrMFUxSNb23qSA==}
engines: {node: '>=18'}
hasBin: true
+ '@marijn/find-cluster-break@1.0.2':
+ resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
+
'@netlify/functions@2.8.2':
resolution: {integrity: sha512-DeoAQh8LuNPvBE4qsKlezjKj0PyXDryOFJfJKo3Z1qZLKzQ21sT314KQKPVjfvw6knqijj+IO+0kHXy/TJiqNA==}
engines: {node: '>=14.0.0'}
@@ -1314,6 +1362,9 @@ packages:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
+ codemirror@6.0.1:
+ resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -1380,6 +1431,9 @@ packages:
resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==}
engines: {node: '>= 14'}
+ crelt@1.0.6:
+ resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
+
croner@9.0.0:
resolution: {integrity: sha512-onMB0OkDjkXunhdW9htFjEhqrD54+M94i6ackoUkjHKbRnXdyEyKRelp4nJ1kAz32+s27jP1FsebpJCVl0BsvA==}
engines: {node: '>=18.0'}
@@ -2954,6 +3008,9 @@ packages:
strip-literal@2.1.1:
resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
+ style-mod@4.1.2:
+ resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==}
+
stylehacks@7.0.4:
resolution: {integrity: sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==}
engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
@@ -3411,6 +3468,9 @@ packages:
typescript:
optional: true
+ w3c-keyname@2.2.8:
+ resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
+
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
@@ -3698,6 +3758,61 @@ snapshots:
dependencies:
mime: 3.0.0
+ '@codemirror/autocomplete@6.18.4':
+ dependencies:
+ '@codemirror/language': 6.10.8
+ '@codemirror/state': 6.5.1
+ '@codemirror/view': 6.36.2
+ '@lezer/common': 1.2.3
+
+ '@codemirror/commands@6.8.0':
+ dependencies:
+ '@codemirror/language': 6.10.8
+ '@codemirror/state': 6.5.1
+ '@codemirror/view': 6.36.2
+ '@lezer/common': 1.2.3
+
+ '@codemirror/lang-javascript@6.2.2':
+ dependencies:
+ '@codemirror/autocomplete': 6.18.4
+ '@codemirror/language': 6.10.8
+ '@codemirror/lint': 6.8.4
+ '@codemirror/state': 6.5.1
+ '@codemirror/view': 6.36.2
+ '@lezer/common': 1.2.3
+ '@lezer/javascript': 1.4.21
+
+ '@codemirror/language@6.10.8':
+ dependencies:
+ '@codemirror/state': 6.5.1
+ '@codemirror/view': 6.36.2
+ '@lezer/common': 1.2.3
+ '@lezer/highlight': 1.2.1
+ '@lezer/lr': 1.4.2
+ style-mod: 4.1.2
+
+ '@codemirror/lint@6.8.4':
+ dependencies:
+ '@codemirror/state': 6.5.1
+ '@codemirror/view': 6.36.2
+ crelt: 1.0.6
+
+ '@codemirror/search@6.5.8':
+ dependencies:
+ '@codemirror/state': 6.5.1
+ '@codemirror/view': 6.36.2
+ crelt: 1.0.6
+
+ '@codemirror/state@6.5.1':
+ dependencies:
+ '@marijn/find-cluster-break': 1.0.2
+
+ '@codemirror/view@6.36.2':
+ dependencies:
+ '@codemirror/state': 6.5.1
+ style-mod: 4.1.2
+ w3c-keyname: 2.2.8
+
'@ctrl/tinycolor@3.6.1': {}
'@element-plus/icons-vue@2.3.1(vue@3.5.13(typescript@5.7.3))':
@@ -3916,6 +4031,22 @@ snapshots:
'@kwsites/promise-deferred@1.1.1': {}
+ '@lezer/common@1.2.3': {}
+
+ '@lezer/highlight@1.2.1':
+ dependencies:
+ '@lezer/common': 1.2.3
+
+ '@lezer/javascript@1.4.21':
+ dependencies:
+ '@lezer/common': 1.2.3
+ '@lezer/highlight': 1.2.1
+ '@lezer/lr': 1.4.2
+
+ '@lezer/lr@1.4.2':
+ dependencies:
+ '@lezer/common': 1.2.3
+
'@mapbox/node-pre-gyp@2.0.0-rc.0':
dependencies:
consola: 3.3.3
@@ -3929,6 +4060,8 @@ snapshots:
- encoding
- supports-color
+ '@marijn/find-cluster-break@1.0.2': {}
+
'@netlify/functions@2.8.2':
dependencies:
'@netlify/serverless-functions-api': 1.26.1
@@ -4845,6 +4978,16 @@ snapshots:
cluster-key-slot@1.1.2: {}
+ codemirror@6.0.1:
+ dependencies:
+ '@codemirror/autocomplete': 6.18.4
+ '@codemirror/commands': 6.8.0
+ '@codemirror/language': 6.10.8
+ '@codemirror/lint': 6.8.4
+ '@codemirror/search': 6.5.8
+ '@codemirror/state': 6.5.1
+ '@codemirror/view': 6.36.2
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -4896,6 +5039,8 @@ snapshots:
crc-32: 1.2.2
readable-stream: 4.7.0
+ crelt@1.0.6: {}
+
croner@9.0.0: {}
cronstrue@2.52.0: {}
@@ -6676,6 +6821,8 @@ snapshots:
dependencies:
js-tokens: 9.0.1
+ style-mod@4.1.2: {}
+
stylehacks@7.0.4(postcss@8.4.49):
dependencies:
browserslist: 4.24.4
@@ -7119,6 +7266,8 @@ snapshots:
optionalDependencies:
typescript: 5.7.3
+ w3c-keyname@2.2.8: {}
+
web-streams-polyfill@3.3.3: {}
webidl-conversions@3.0.1: {}
diff --git a/utils/html2VNode.ts b/utils/html2VNode.ts
new file mode 100644
index 0000000..b73752a
--- /dev/null
+++ b/utils/html2VNode.ts
@@ -0,0 +1,188 @@
+import type { VNode } from "vue";
+
+// 未完成
+export default class Html2VNode {
+ private middlewareList: middlewareType[] = []
+
+ constructor() {
+ this.use(this.defaultMiddleware)
+ }
+
+ public use(middleware: middlewareType) {
+ this.middlewareList.push(middleware)
+ }
+ public rander(htmlString: string) {
+ const html2ObjResult = this.html2Obj(htmlString);
+ const middlewareMap = this.buildMiddlewareMap(html2ObjResult);
+ const wrinklesResult = this.wrinkles(html2ObjResult)
+ const vnodes = this.rander2VNode(wrinklesResult, middlewareMap)
+ return vnodes;
+ }
+
+ private buildMiddlewareMap(objs: (TagType | miaoVNodeType)[]) {
+ const tagNameSet = new Set()
+ const middlewareMap = new Map()
+ for (const item of objs) {
+ const { tagName = '' } = this.parseTag(item.tag);
+ tagNameSet.add(tagName)
+ }
+ for (const tagName of tagNameSet) {
+ for (const middleware of this.middlewareList) {
+ if (middleware.filter(tagName)) {
+ middlewareMap.set(tagName, middleware)
+ }
+ }
+ }
+ return middlewareMap;
+ }
+
+ private rander2VNode = (
+ wrinklesResult: (miaoVNodeType | string)[],
+ middlewareMap: Map
+ ): (VNode | string)[] => {
+ return wrinklesResult.map((item: miaoVNodeType | string): VNode | string => {
+ if (typeof item === 'string') {
+ return item;
+ }
+ const { tagName = '', tagAttrs } = this.parseTag(item.tag);
+ return middlewareMap.get(tagName)!.rander({
+ item, tagName, tagAttrs, middlewareMap
+ })
+ })
+ }
+
+ private defaultMiddleware: middlewareType = {
+ filter: (_: string) => {
+ return true;
+ },
+ rander: ({
+ item, tagName, tagAttrs, middlewareMap
+ }) => {
+ if (!tagName) {
+ return item.children as string;
+ }
+ let child: string | (string | VNode)[] = item.children as string
+ if (typeof item.children !== 'string') {
+ child = this.rander2VNode(item.children, middlewareMap)
+ }
+ return h(tagName ?? 'span', tagAttrs, child)
+ }
+ }
+
+ private html2Obj = (htmlString: string) => {
+ const html2ObjResult: (TagType | miaoVNodeType)[] = []
+ let currhtml = htmlString;
+ while (true) {
+ const nextTag = this.getNextHtmlTag(currhtml);
+ if (!nextTag) {
+ break;
+ }
+ const middleChild = currhtml.slice(0, nextTag.Index);
+ if (middleChild) {
+ html2ObjResult.push({
+ tag: '',
+ children: middleChild,
+ // html: middleContent
+ })
+ }
+ currhtml = currhtml.slice(nextTag.Index + nextTag.tag.length);
+ html2ObjResult.push(nextTag)
+ }
+ return html2ObjResult;
+ }
+
+ private check = (html2ObjResult: (TagType | miaoVNodeType)[]) => {
+ const stack = []
+ for (const item of html2ObjResult) {
+ if (!item.tag) {
+ continue;
+ }
+ // @ts-ignore
+ if (item?.isClose && item.isClose == true) {
+ const last = stack.pop();
+ if (last !== this.parseTag(item.tag).tagName) {
+ return false;
+ }
+ } else {
+ stack.push(this.parseTag(item.tag).tagName)
+ }
+ }
+ return true;
+ }
+
+ // 想必是flat的反义词吧
+ private wrinkles = (target: (TagType | miaoVNodeType)[]) => {
+ const res: miaoVNodeType[] = []
+ const resStack = [res]
+ for (const item of target) {
+ if (!item.tag) {
+ // @ts-ignore
+ resStack[resStack.length - 1].push(item)
+ // @ts-ignore
+ } else if (item.isClose) {
+ resStack.pop()
+ } else {
+ const _childRes: any = []
+ resStack[resStack.length - 1].push({
+ tag: item.tag,
+ // tag: this.getTagName(item.tag),
+ children: _childRes,
+ })
+ resStack.push(_childRes)
+ }
+ }
+ return res;
+ }
+
+ private getNextHtmlTag = (htmlString: string): {
+ tag: string,
+ Index: number,
+ isClose: boolean
+ } | null => {
+ let startIndex = 0;
+ let tagRegex = /<("[^"]*"|'[^']*'|[^'">])*>/;
+ let tagMatch = tagRegex.exec(htmlString.slice(startIndex));
+ if (tagMatch) {
+ const isClose = tagMatch[0].startsWith('');
+ return {
+ tag: tagMatch[0],
+ Index: tagMatch.index,
+ isClose
+ };
+ }
+ return null;
+ }
+
+ private parseTag = (tag: string) => {
+ const tagStr = tag.replace(/<\s*\/*\s*|\s*>/g, '');
+ const tagArr = tagStr.split(' ').filter(item => item.trim());
+ const tagName = tagArr.shift();
+ const tagAttrs = tagArr.map(v => v.trim().split('=')).reduce((pre, cur) => {
+ pre[cur[0]] = cur[1];
+ return pre;
+ }, {} as { [key: string]: string })
+ return { tagName, tagAttrs };
+ }
+}
+
+type miaoVNodeType = {
+ tag: string,
+ children: string | miaoVNodeType[],
+}
+
+type TagType = {
+ tag: string;
+ Index: number;
+ isClose: boolean;
+}
+
+export type middlewareType = {
+ filter: (tagName: string) => boolean,
+ rander: (option: {
+ item: miaoVNodeType,
+ tagName: string,
+ tagAttrs: { [key: string]: string },
+ middlewareMap: Map
+ }
+ ) => VNode | string
+}
\ No newline at end of file