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(' { + 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