diff --git a/.gitignore b/.gitignore index ec65c4fc6..daaf916ed 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ packages/**/yarn.lock # config examples/cherry-markdown-publish/src/common/config/*.yaml + +# CodeBuddy local data +.codebuddy/ diff --git a/examples/ai_chat.html b/examples/ai_chat.html index e42318c7b..427b8521f 100644 --- a/examples/ai_chat.html +++ b/examples/ai_chat.html @@ -5,7 +5,7 @@ 流式输出md内容 - + - + + + + + + Cherry Editor - Markdown Editor + + + + + + - -
- - - - + +
+ + + + diff --git a/examples/chart_toolbar_demo.html b/examples/chart_toolbar_demo.html index 125652d21..c4527aec6 100644 --- a/examples/chart_toolbar_demo.html +++ b/examples/chart_toolbar_demo.html @@ -1,20 +1,23 @@ - - - - - + + + + + Cherry Markdown - 图表工具栏测试 - - - - - -
+ + + + + - + + - + diff --git a/examples/chatgpt.html b/examples/chatgpt.html index 1d86571b2..793ec00f6 100644 --- a/examples/chatgpt.html +++ b/examples/chatgpt.html @@ -44,7 +44,7 @@
- + diff --git a/examples/custom_codeblock_wrapper.html b/examples/custom_codeblock_wrapper.html index 28747cc96..1df04edff 100644 --- a/examples/custom_codeblock_wrapper.html +++ b/examples/custom_codeblock_wrapper.html @@ -1,215 +1,216 @@ - - - - - - 自定义代码块外层容器 - Cherry Editor - - - - - - -
- - - - + + + } + }, + toolbars: { + toolbar: [ + 'bold', + 'italic', + 'size', + '|', + 'color', + 'header', + '|', + 'drawIo', + '|', + 'ol', + 'ul', + 'checklist', + 'panel', + 'align', + 'detail', + '|', + 'formula', + 'graph', + 'proTable', + 'codeTheme', + 'search', + 'shortcutKey', + ], + }, + previewer: { + enablePreviewerBubble: false, + } + }; + var sourceCode = document.documentElement.outerHTML; + var cherry = new Cherry(config); + cherry.setValue('\n## 自定义json代码块的特殊渲染\n\n```json\n{\n"--oc-white": "#ffffff",\n"--oc-black": "#000000",\n"--oc-gray-0": "#f8f9fa",\n"--oc-gray-1": "#f1f3f5",\n"--oc-gray-2": "#e9ecef",\n"--oc-gray-3": "#dee2e6",\n"--oc-gray-4": "#ced4da",\n"--oc-gray-5": "#adb5bd",\n"--oc-gray-6": "#868e96",\n"--oc-gray-7": "#495057",\n"--oc-gray-8": "#343a40",\n"--oc-gray-9": "#212529"\n}\n```\n\n'); + // 绑定 body的点击事件,判断是否为 .j-switch 元素 + document.body.addEventListener('click', function(e) { + const target = e.target.closest('.j-switch'); + if (target) { + const switcher = target.querySelector('.my-code-wrapper-switcher'); + const preview = target.parentElement.parentElement.querySelector('.my-code-wrapper-preview-content'); + const code = target.parentElement.parentElement.querySelector('.my-code-wrapper-content'); + if (switcher.classList.contains('active')) { + switcher.classList.remove('active'); + preview.classList.add('hidden'); + code.classList.remove('hidden'); + } else { + switcher.classList.add('active'); + preview.classList.remove('hidden'); + code.classList.add('hidden'); + const codeStr = code.innerText.replace(/\n/g, ''); + const codeJson = JSON.parse(codeStr); + const thead = '变量名颜色值'; + let tbody = ''; + for (let key in codeJson) { + tbody += `${key}`; + } + preview.innerHTML = `${thead}${tbody}
`; + } + } + }); + + diff --git a/examples/h5.html b/examples/h5.html index b1d572cb1..59cb68ee9 100644 --- a/examples/h5.html +++ b/examples/h5.html @@ -28,7 +28,7 @@
- + + @@ -86,26 +88,26 @@ Sit down: 3: Me ``` - - - - - + + + + + diff --git a/examples/multiple.html b/examples/multiple.html index aaa9321c1..7f8f7795f 100644 --- a/examples/multiple.html +++ b/examples/multiple.html @@ -37,7 +37,7 @@

Cherry Markdown Editor

Cherry Markdown Editor

- + + + diff --git a/examples/suggester.html b/examples/suggester.html index 153dd1847..516b37d2a 100644 --- a/examples/suggester.html +++ b/examples/suggester.html @@ -37,7 +37,7 @@
- + diff --git a/examples/vim.html b/examples/vim.html index f0820046b..098ab495c 100644 --- a/examples/vim.html +++ b/examples/vim.html @@ -1,48 +1,50 @@ - - - - - - VIM demo - Cherry Editor - - - - + + + + + + VIM demo - Cherry Editor + + + + + + - -
- - - + +
+ + + diff --git a/examples/xss.html b/examples/xss.html index 34c809acf..3064dd2b2 100644 --- a/examples/xss.html +++ b/examples/xss.html @@ -40,7 +40,7 @@
- + - - - - diff --git a/packages/cherry-markdown/package.json b/packages/cherry-markdown/package.json index d2a4aa503..2e9a58108 100644 --- a/packages/cherry-markdown/package.json +++ b/packages/cherry-markdown/package.json @@ -25,6 +25,7 @@ ], "scripts": { "iconfont": "gulp", + "dev": "vite", "dev:vite": "vite", "build": "run-s clean build:all", "build:all": "run-p iconfont build:styles build:types build:addons build:full build:core build:engine build:stream", diff --git a/packages/cherry-markdown/vite.config.ts b/packages/cherry-markdown/vite.config.ts index 5166b6b27..6540cb7eb 100644 --- a/packages/cherry-markdown/vite.config.ts +++ b/packages/cherry-markdown/vite.config.ts @@ -1,95 +1,104 @@ +/** + * Vite 开发服务器配置 + * + * 该配置用于开发环境,支持实时预览 examples 中的所有 HTML 页面。 + * + * 架构特点: + * - MPA(多页面应用):以 examples/ 作为根目录 + * - 虚拟模块:dist 请求重定向到源码,支持热更新 + * - 中间件拦截:处理字体文件和资源路由 + * - HTML 转换:自动处理 link 和 script 标签 + * + * 注意:此配置仅用于开发,生产构建使用 Rollup(build/*.config.js) + */ + import { defineConfig } from 'vite'; import path from 'path'; +import { cherryDevPlugin, printLinksPlugin } from './vite.plugins'; + +// Cherry Markdown 源码目录 +const cherryMarkdownDir = path.resolve(__dirname); +const examplesDir = path.resolve(__dirname, '../../examples'); +const srcDir = path.resolve(cherryMarkdownDir, 'src'); -const paths = [ +// examples 目录下所有可访问的 HTML 页面 +// 用于启动时打印可访问链接,以及插件中的文件存在性检查 +const htmlPages = [ '/index.html', - // '/basic.html', + '/basic.html', '/h5.html', '/multiple.html', '/notoolbar.html', '/preview_only.html', '/xss.html', - // '/api.html', + '/api.html', '/img.html', '/table.html', '/head_num.html', '/ai_chat.html', - // '/vim.html', - // '/mermaid.html', + '/ai_chat_stream.html', + '/mermaid.html', + '/vim.html', + '/drawio_demo.html', + '/chart_toolbar_demo.html', + '/chatgpt.html', + '/suggester.html', + '/custom_codeblock_wrapper.html', ]; -function printLinks() { - return { - name: 'print-links', - configureServer(server: any) { - server.httpServer?.once('listening', () => { - const address = server.httpServer?.address(); - let port = 5173; - // 始终使用 localhost - const host = 'localhost'; - if (typeof address === 'object' && address) { - port = address.port || port; - } - console.log('\n可访问页面链接:'); - paths.forEach((p) => { - console.log(` http://${host}:${port}${p}`); - }); - console.log(''); - }); - }, - }; -} - -function spaFallback() { - return { - name: 'spa-fallback', - transformIndexHtml: (html: string, ctx: { path: string; filename?: string; server?: any }) => { - const path = ctx.path; - // 如果是配置的路径之一,注入脚本设置原始路径 - // 优化的路径匹配逻辑 - const isExactMatch = paths.includes(path); - const isPathWithoutHtmlMatch = path.endsWith('.html') && paths.includes(path.slice(0, -5)); - const matchedPath = isExactMatch || isPathWithoutHtmlMatch ? (isExactMatch ? path : path.slice(0, -5)) : null; - - if (matchedPath && matchedPath !== '/index.html') { - // 转义路径中的特殊字符以防止XSS注入 - const escapedPath = matchedPath.replace(/['"\\]/g, '\\$&'); - const script = ``; - return html.replace('', `${script}\n `); - } - return html; - }, - }; -} - export default defineConfig({ - root: process.cwd(), + // 以 examples 目录作为根目录,实现真正的多页面应用 + root: examplesDir, base: '/', - publicDir: 'dist', + resolve: { alias: [ - { find: '@', replacement: path.resolve(__dirname, 'src') }, - { find: '@examples', replacement: path.resolve(__dirname, '../../examples') }, + // 源码别名 + { find: '@', replacement: srcDir }, + // examples 别名 + { find: '@examples', replacement: examplesDir }, ], }, + server: { host: '0.0.0.0', port: 5173, - open: false, + open: '/index.html', + // 允许所有主机 allowedHosts: true, fs: { - allow: [path.resolve(__dirname), path.resolve(__dirname, 'dist'), path.resolve(__dirname, '..', '..')], + // 允许访问的目录 + allow: [ + examplesDir, + cherryMarkdownDir, + // 允许访问整个 monorepo + path.resolve(__dirname, '../..'), + ], }, }, - build: { - outDir: 'dist', - emptyOutDir: false, - }, + + // 定义全局常量 define: { 'process.env.BUILD_VERSION': JSON.stringify(process.env.BUILD_VERSION || ''), 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), BUILD_ENV: JSON.stringify(process.env.NODE_ENV || 'development'), - __EXAMPLES_PATH__: JSON.stringify(path.resolve(__dirname, '../../examples').replace(/\\/g, '/')), }, - plugins: [spaFallback(), printLinks()], + + // CSS 配置 + css: { + preprocessorOptions: { + scss: { + // SCSS 配置 + charset: false, + }, + }, + }, + + // 优化依赖预构建 + optimizeDeps: { + include: ['codemirror'], + exclude: [], + }, + + plugins: [cherryDevPlugin(srcDir, cherryMarkdownDir), printLinksPlugin(examplesDir, htmlPages)], }); diff --git a/packages/cherry-markdown/vite.plugins.ts b/packages/cherry-markdown/vite.plugins.ts new file mode 100644 index 000000000..6da887b3d --- /dev/null +++ b/packages/cherry-markdown/vite.plugins.ts @@ -0,0 +1,177 @@ +/** + * Vite 开发模式插件集合 + * + * 本文件包含为开发环境设计的 Vite 插件,用于: + * 1. 虚拟模块处理:将对 dist 的请求重定向到源码 + * 2. 资源中间件:处理字体文件和资源路由 + * 3. HTML 转换:适配开发模式的 script 和 link 标签 + * 4. 链接打印:启动时显示所有可访问的页面链接 + * + * 注意:这些插件仅在开发模式使用,生产环境不加载此文件 + */ + +import { Plugin } from 'vite'; +import path from 'path'; +import fs from 'fs'; + +/** + * 打印可访问链接的插件 + * 在开发服务器启动后,打印所有可访问的 HTML 页面链接 + */ +export function printLinksPlugin(examplesDir: string, htmlPages: string[]): Plugin { + return { + name: 'print-links', + configureServer(server) { + server.httpServer?.once('listening', () => { + const address = server.httpServer?.address(); + let port = 5173; + const host = 'localhost'; + if (typeof address === 'object' && address) { + port = address.port || port; + } + console.log('\n🍒 Cherry Markdown 开发服务器已启动'); + console.log('\n可访问页面链接:'); + htmlPages.forEach((p) => { + const filePath = path.join(examplesDir, p); + if (fs.existsSync(filePath)) { + console.log(` ✅ http://${host}:${port}${p}`); + } + }); + console.log(''); + }); + }, + }; +} + +/** + * Cherry Markdown 开发模式插件 + * + * 功能: + * 1. 拦截对 dist/cherry-markdown.js 的请求,重定向到虚拟模块(源码) + * 2. 拦截对 dist/cherry-markdown.css 的请求,重定向到虚拟模块(SCSS 源文件) + * 3. 拦截字体文件请求,代理到 dist/fonts/ 目录 + * 4. 转换 HTML,将 link 标签转换为 JS 模块导入,将引用 dist 的 script 转换为 module 类型 + */ +export function cherryDevPlugin(srcDir: string, cherryMarkdownDir: string): Plugin { + // 虚拟模块 ID + const virtualCherryJsId = 'virtual:cherry-markdown-js'; + const virtualCherryCssId = 'virtual:cherry-markdown-css'; + const resolvedVirtualCherryJsId = `\0${virtualCherryJsId}`; + const resolvedVirtualCherryCssId = `\0${virtualCherryCssId}`; + + return { + name: 'cherry-dev-redirect', + enforce: 'pre', + + configureServer(server) { + // 中间件:拦截请求并进行转发或代理 + server.middlewares.use((req, res, next) => { + const url = req.url || ''; + + // 匹配模式:/packages/cherry-markdown/dist/cherry-markdown*.js + const jsPattern = /\/?\.{0,2}\/?packages\/cherry-markdown\/dist\/cherry-markdown[^/]*\.js/; + const cssPattern = /\/?\.{0,2}\/?packages\/cherry-markdown\/dist\/cherry-markdown[^/]*\.css/; + + // 拦截 cherry-markdown.js 请求 → 虚拟模块 + if (jsPattern.test(url)) { + req.url = `/@id/${virtualCherryJsId}`; + return next(); + } + + // 拦截 cherry-markdown.css 请求 → 虚拟模块 + if (cssPattern.test(url)) { + req.url = `/@id/${virtualCherryCssId}`; + return next(); + } + + // 拦截字体文件请求,代理到 dist/fonts/ + // 情况1: src/sass/fonts/ 路径(Vite 处理 SCSS 时生成的绝对路径) + // 情况2: /fonts/ 根路径(CSS 通过 JS 模块注入时,浏览器用页面 URL 解析相对路径 ./fonts/) + const fontPatterns = [/\/packages\/cherry-markdown\/src\/sass\/fonts\/(.+)/, /^\/fonts\/(ch-icon\.[^?]+)/]; + const mimeTypes: Record = { + '.woff2': 'font/woff2', + '.woff': 'font/woff', + '.ttf': 'font/ttf', + '.eot': 'application/vnd.ms-fontobject', + '.svg': 'image/svg+xml', + }; + + for (const pattern of fontPatterns) { + const fontMatch = url.match(pattern); + if (fontMatch) { + const fontFile = fontMatch[1].split('?')[0]; + const fontPath = path.join(cherryMarkdownDir, 'dist', 'fonts', fontFile); + if (fs.existsSync(fontPath)) { + const ext = path.extname(fontFile).toLowerCase(); + res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream'); + res.setHeader('Cache-Control', 'max-age=31536000'); + fs.createReadStream(fontPath).pipe(res); + return; + } + } + } + + next(); + }); + }, + + resolveId(id) { + if (id === virtualCherryJsId) { + return resolvedVirtualCherryJsId; + } + if (id === virtualCherryCssId) { + return resolvedVirtualCherryCssId; + } + }, + + load(id) { + // 加载虚拟 JS 模块 - 从源码导入并暴露到全局 + if (id === resolvedVirtualCherryJsId) { + return ` +import Cherry from '${srcDir.replace(/\\/g, '/')}/index.js'; + +// 暴露到全局,兼容 examples 中的用法 +window.Cherry = Cherry; + +export default Cherry; +export { Cherry }; +`; + } + + // 加载虚拟 CSS 模块 - 导入 SCSS 源文件 + if (id === resolvedVirtualCherryCssId) { + return `import '${srcDir.replace(/\\/g, '/')}/sass/index.scss';`; + } + }, + + // 转换 HTML,处理脚本类型和样式引入 + transformIndexHtml(html) { + let result = html; + + // 1. 将引用 dist/cherry-markdown.css 的 link 标签转换为 JS 模块导入 + // 因为 Vite 处理 SCSS 需要通过 JS 模块 + result = result.replace( + /]*href=["']([^"']*\/packages\/cherry-markdown\/dist\/cherry-markdown[^"']*\.css)["'][^>]*\/?>/gi, + (_match, href) => { + return ``; + }, + ); + + // 2. 将引用 dist/cherry-markdown.js 的普通 script 标签转换为 module 类型 + // 因为源码是 ES Module + result = result.replace( + /]*)\s+src=["']([^"']*\/packages\/cherry-markdown\/dist\/cherry-markdown[^"']*)["']([^>]*)><\/script>/gi, + (match, before, src, after) => { + // 如果已经是 module 类型,不做处理 + if (/type\s*=\s*["']module["']/i.test(before + after)) { + return match; + } + // 添加 type="module" + return ``; + }, + ); + + return result; + }, + }; +}