diff --git a/.gitignore b/.gitignore index c2757e7d9..aae139f1a 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,4 @@ apps/utools/release apps/web/public/static/libs/mathjax apps/web/public/static/libs/mermaid apps/web/public/static/libs/article-syncjs +CLAUDE.md diff --git a/.npmrc b/.npmrc index 7f082f23a..7549542d7 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -registry=https://registry.npmmirror.com \ No newline at end of file +registry=https://registry.npmmirror.com diff --git a/apps/web/src/App.vue b/apps/web/src/App.vue index e8ad244dd..e6204f561 100644 --- a/apps/web/src/App.vue +++ b/apps/web/src/App.vue @@ -104,7 +104,9 @@ body { color: #333333; background-color: #ffffff; - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.08); + box-shadow: + 0 4px 8px 0 rgba(0, 0, 0, 0.12), + 0 2px 4px 0 rgba(0, 0, 0, 0.08); } .CodeMirror-hint { diff --git a/md-to-html-api-doc-new.md b/md-to-html-api-doc-new.md new file mode 100644 index 000000000..8db0c09e9 --- /dev/null +++ b/md-to-html-api-doc-new.md @@ -0,0 +1,268 @@ +# MD-CLI 渲染接口文档 + +本文档描述了将 Markdown 文本转换为微信公众号兼容 HTML 的 API 接口。 + +## 接口概览 + +| 接口 | 说明 | 返回格式 | +| ------------------ | ------------------------------------ | -------- | +| `/api/render` | 渲染 Markdown 为 HTML 片段 | JSON | +| `/api/render/html` | 渲染 Markdown 为完整 HTML 页面并下载 | 文件流 | + +**服务地址**: `http://localhost:8800` + +--- + +## 1. 渲染 API (`/api/render`) + +将 Markdown 内容转换为 HTML 片段,返回 JSON 格式数据。 + +- **接口地址**: `/api/render` +- **请求方式**: `POST` +- **Content-Type**: `application/json` + +### 请求参数 + +#### 基础参数 + +| 参数名 | 类型 | 必填 | 默认值 | 可选值/说明 | +| :--------------- | :------- | :----- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `markdown` | `string` | **是** | - | 需要渲染的 Markdown 源码内容 | +| `path` | `string` | 否 | - | Markdown 文件路径(与 `markdown` 二选一) | +| `theme` | `string` | 否 | `'default'` | - `'default'`: 经典主题
- `'grace'`: 优雅主题
- `'simple'`: 简洁主题 | +| `primaryColor` | `string` | 否 | `'#0F4C81'` | 主题色,支持任意 CSS 颜色值。预设主题色参考:
- 经典蓝: `'#0F4C81'`
- 翡翠绿: `'#009874'`
- 活力橘: `'#FA5151'`
- 柠檬黄: `'#FECE00'`
- 薰衣紫: `'#92617E'`
- 天空蓝: `'#55C9EA'`
- 玫瑰金: `'#B76E79'`
- 橄榄绿: `'#556B2F'`
- 石墨黑: `'#333333'`
- 雾烟灰: `'#A9A9A9'`
- 樱花粉: `'#FFB7C5'` | +| `fontFamily` | `string` | 否 | (系统字体栈)\* | 任何有效的 CSS font-family 字符串 | +| `fontSize` | `string` | 否 | `'16px'` | 任何有效的 CSS 字体大小单位,例如:
- `'14px'`, `'15px'`, `'17px'`
- `'1em'`, `'1.2rem'` | +| `highlightTheme` | `string` | 否 | `'github'` | [Highlight.js](https://highlightjs.org/) 支持的所有主题名称,例如:
- `'github'`
- `'monokai'`
- `'dracula'`
- `'vs2015'`
- `'atom-one-dark'` | + +> \*默认字体栈: `"-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif"` + +#### 样式配置参数 + +| 参数名 | 类型 | 必填 | 默认值 | 说明 | +| :----------------- | :-------- | :--- | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `isMacCodeBlock` | `boolean` | 否 | `true` | 是否显示 Mac 风格代码块(带红黄绿三个圆点) | +| `isShowLineNumber` | `boolean` | 否 | `false` | 是否显示代码块行号 | +| `legend` | `string` | 否 | `'alt'` | 图注显示格式:
- `'alt'`: 只显示 alt
- `'title'`: 只显示 title
- `'alt-title'`: alt 优先
- `'title-alt'`: title 优先
- `'none'`: 不显示 | +| `isCiteStatus` | `boolean` | 否 | `false` | 是否开启微信外链接引用(在文末显示引用链接列表) | +| `isCountStatus` | `boolean` | 否 | `false` | 是否显示字数统计和阅读时间 | +| `isUseIndent` | `boolean` | 否 | `false` | 是否开启段落首行缩进(2em) | +| `isUseJustify` | `boolean` | 否 | `false` | 是否开启段落两端对齐 | +| `themeMode` | `string` | 否 | `'light'` | 主题模式:
- `'light'`: 浅色模式
- `'dark'`: 深色模式 | + +### 响应结构 + +**成功响应 (200 OK)** + +```json +{ + "html": "
...
" +} +``` + +**错误响应** + +- **400 Bad Request**: 缺少必填参数或文件未找到 + ```json + { "error": "Markdown content is required" } + ``` + 或 + ```json + { "error": "File not found" } + ``` +- **500 Internal Server Error**: 服务器内部错误 + ```json + { "error": "Error message details..." } + ``` + +--- + +## 2. 渲染并下载 API (`/api/render/html`) + +将 Markdown 渲染为完整的 HTML 页面文件,保存到本地 `Downloads` 目录并触发下载。 + +- **接口地址**: `/api/render/html` +- **请求方式**: `POST` +- **Content-Type**: `application/json` + +### 请求参数 + +与 `/api/render` 接口相同,另外支持以下参数: + +| 参数名 | 类型 | 必填 | 默认值 | 说明 | +| :------ | :------- | :--- | :-------------------------- | :---------------------------------------- | +| `title` | `string` | 否 | `'Markdown Render Preview'` | HTML 页面标题,如未指定则自动从文件名提取 | + +### 行为说明 + +1. 接收 Markdown 内容或文件路径 +2. 调用 `/api/render` 的渲染逻辑生成 HTML 片段 +3. 封装为完整的 HTML 文档(包含 ``, ``, `` 和基础样式) +4. **自动保存**: 将生成的文件保存到用户的 `~/Downloads` 目录 + - 如果提供了 `path`,文件名与源文件名一致(后缀改为 `.html`) + - 否则文件名为 `output.html` +5. **响应下载**: 返回文件下载流 + +### 响应结构 + +- **成功 (200 OK)**: 返回文件流,触发浏览器下载 +- **失败 (400/500)**: 返回纯文本错误信息 + +--- + +## 调用示例 + +### 基础调用 - `/api/render` (curl) + +```bash +curl -X POST http://localhost:8800/api/render \ + -H "Content-Type: application/json" \ + -d '{ + "markdown": "# Hello World\n\nThis is a **markdown** text.", + "theme": "default", + "primaryColor": "#0F4C81" + }' +``` + +### 完整参数调用 - `/api/render` (curl) + +```bash +curl -X POST http://localhost:8800/api/render \ + -H "Content-Type: application/json" \ + -d '{ + "markdown": "# 标题\n\n**加粗**文本和`代码`\n\n> 引用块\n\n- 列表项\n\n[链接](https://example.com)", + "theme": "grace", + "primaryColor": "#009874", + "fontSize": "16px", + "highlightTheme": "github", + "isMacCodeBlock": true, + "isShowLineNumber": true, + "legend": "alt", + "isCiteStatus": true, + "isCountStatus": true, + "isUseIndent": true, + "isUseJustify": false, + "themeMode": "light" + }' +``` + +### 下载 HTML 文件 - `/api/render/html` (curl) + +```bash +curl -X POST http://localhost:8800/api/render/html \ + -H "Content-Type: application/json" \ + -d '{ + "markdown": "# 我的文档\n\n这是正文内容...", + "title": "我的文档", + "theme": "grace", + "primaryColor": "#009874", + "isCiteStatus": true + }' --output my-document.html +``` + +### 使用 JavaScript (Fetch) 调用 + +```javascript +// 调用 /api/render 获取 HTML 片段 +async function renderMarkdown() { + const response = await fetch('http://localhost:8800/api/render', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + markdown: '## 测试标题\n\n这是一段测试文本。\n\n[链接](https://example.com)', + theme: 'grace', + primaryColor: '#009874', + fontSize: '16px', + isCiteStatus: true, + isCountStatus: true, + isUseIndent: true + }) + }) + + const data = await response.json() + if (response.ok) { + console.log('Rendered HTML:', data.html) + } + else { + console.error('Error:', data.error) + } +} + +// 调用 /api/render/html 下载文件 +async function downloadHtml() { + const response = await fetch('http://localhost:8800/api/render/html', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + markdown: '# 标题\n\n正文内容...', + title: '我的文档', + theme: 'grace' + }) + }) + + if (response.ok) { + const blob = await response.blob() + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'output.html' + a.click() + } +} +``` + +### 使用 Python 调用 + +```python +import requests + +# 调用 /api/render 获取 HTML 片段 +def render_markdown(): + url = "http://localhost:8800/api/render" + payload = { + "markdown": "# 标题\n\n**加粗**文本\n\n> 引用\n\n[链接](https://example.com)", + "theme": "grace", + "primaryColor": "#009874", + "isCiteStatus": True, + "isCountStatus": True, + "isUseIndent": True + } + + response = requests.post(url, json=payload) + + if response.status_code == 200: + html = response.json()["html"] + print("渲染成功,HTML 长度:", len(html)) + else: + print("错误:", response.json()["error"]) + +# 调用 /api/render/html 下载文件 +def download_html(): + url = "http://localhost:8800/api/render/html" + payload = { + "markdown": "# 标题\n\n正文内容...", + "title": "我的文档", + "theme": "grace" + } + + response = requests.post(url, json=payload) + + if response.status_code == 200: + with open("output.html", "wb") as f: + f.write(response.content) + print("文件已保存") + else: + print("错误:", response.text) +``` + +--- + +## 启动服务 + +```bash +cd packages/md-cli +node index.js +``` + +或者 pnpm cli dev +服务默认运行在 `http://localhost:8800`。 diff --git a/md-to-html-api-doc.md b/md-to-html-api-doc.md new file mode 100644 index 000000000..c5f030dcc --- /dev/null +++ b/md-to-html-api-doc.md @@ -0,0 +1,171 @@ +# MD-CLI 渲染接口文档 + +本文档描述了将 Markdown 文本转换为微信公众号兼容 HTML 的 API 接口。 + +## 接口概览 + +- **接口地址**: `http://localhost:8800/api/render` +- **请求方式**: `POST` +- **Content-Type**: `application/json` + +## 请求参数 + +请求体为一个 JSON 对象,支持以下参数: + +### 基础参数 + +| 参数名 | 类型 | 必填 | 默认值 | 可选值/说明 | +| :--------------- | :------- | :----- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `markdown` | `string` | **是** | - | 需要渲染的 Markdown 源码内容 | +| `path` | `string` | 否 | - | Markdown 文件路径(与 `markdown` 二选一) | +| `theme` | `string` | 否 | `'default'` | - `'default'`: 经典主题
- `'grace'`: 优雅主题
- `'simple'`: 简洁主题 | +| `primaryColor` | `string` | 否 | `'#0F4C81'` | 主题色,支持任意 CSS 颜色值。预设主题色参考:
- 经典蓝: `'#0F4C81'`
- 翡翠绿: `'#009874'`
- 活力橘: `'#FA5151'`
- 柠檬黄: `'#FECE00'`
- 薰衣紫: `'#92617E'`
- 天空蓝: `'#55C9EA'`
- 玫瑰金: `'#B76E79'`
- 橄榄绿: `'#556B2F'`
- 石墨黑: `'#333333'`
- 雾烟灰: `'#A9A9A9'`
- 樱花粉: `'#FFB7C5'` | +| `fontFamily` | `string` | 否 | (系统字体栈)\* | 任何有效的 CSS font-family 字符串 | +| `fontSize` | `string` | 否 | `'16px'` | 任何有效的 CSS 字体大小单位,例如:
- `'14px'`, `'15px'`, `'17px'`
- `'1em'`, `'1.2rem'` | +| `highlightTheme` | `string` | 否 | `'github'` | [Highlight.js](https://highlightjs.org/) 支持的所有主题名称,例如:
- `'github'`
- `'monokai'`
- `'dracula'`
- `'vs2015'`
- `'atom-one-dark'` | + +> \*默认字体栈: `"-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif"` + +### 样式配置参数 + +| 参数名 | 类型 | 必填 | 默认值 | 说明 | +| :----------------- | :-------- | :--- | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `isMacCodeBlock` | `boolean` | 否 | `true` | 是否显示 Mac 风格代码块(带红黄绿三个圆点) | +| `isShowLineNumber` | `boolean` | 否 | `false` | 是否显示代码块行号 | +| `legend` | `string` | 否 | `'alt'` | 图注显示格式:
- `'alt'`: 只显示 alt
- `'title'`: 只显示 title
- `'alt-title'`: alt 优先
- `'title-alt'`: title 优先
- `'none'`: 不显示 | +| `isCiteStatus` | `boolean` | 否 | `false` | 是否开启微信外链接引用(在文末显示引用链接列表) | +| `isCountStatus` | `boolean` | 否 | `false` | 是否显示字数统计和阅读时间 | +| `isUseIndent` | `boolean` | 否 | `false` | 是否开启段落首行缩进(2em) | +| `isUseJustify` | `boolean` | 否 | `false` | 是否开启段落两端对齐 | +| `themeMode` | `string` | 否 | `'light'` | 主题模式:
- `'light'`: 浅色模式
- `'dark'`: 深色模式 | + +## 响应结构 + +### 成功响应 (200 OK) + +返回 JSON 对象,包含渲染后的 HTML 字符串。 + +```json +{ + "html": "
引用块\n\n- 列表项\n\n[链接](https://example.com)", + "theme": "grace", + "primaryColor": "#009874", + "fontSize": "16px", + "highlightTheme": "github", + "isMacCodeBlock": true, + "isShowLineNumber": true, + "legend": "alt", + "isCiteStatus": true, + "isCountStatus": true, + "isUseIndent": true, + "isUseJustify": false, + "themeMode": "light" + }' +``` + +### 使用 JavaScript (Fetch) 调用 + +```javascript +async function renderMarkdown() { + const response = await fetch('http://localhost:8800/api/render', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + markdown: '## 测试标题\n\n这是一段测试文本。\n\n[链接](https://example.com)', + theme: 'grace', + primaryColor: '#009874', + fontSize: '16px', + isCiteStatus: true, + isCountStatus: true, + isUseIndent: true + }) + }) + + const data = await response.json() + if (response.ok) { + console.log('Rendered HTML:', data.html) + } + else { + console.error('Error:', data.error) + } +} +``` + +### 使用 Python 调用 + +```python +import requests +import json + +url = "http://localhost:8800/api/render" + +payload = { + "markdown": "# 标题\n\n**加粗**文本\n\n> 引用\n\n[链接](https://example.com)", + "theme": "grace", + "primaryColor": "#009874", + "isCiteStatus": True, + "isCountStatus": True, + "isUseIndent": True +} + +response = requests.post(url, json=payload) + +if response.status_code == 200: + html = response.json()["html"] + print("渲染成功,HTML 长度:", len(html)) +else: + print("错误:", response.json()["error"]) +``` + +## 启动服务 + +```bash +cd packages/md-cli +node index.js +``` + +或者 pnpm cli dev +服务默认运行在 `http://localhost:8800`。 diff --git a/packages/md-cli/index.js b/packages/md-cli/index.js index 2846c198f..b65b577b7 100644 --- a/packages/md-cli/index.js +++ b/packages/md-cli/index.js @@ -22,6 +22,12 @@ async function startServer() { return getPort() }) + // 固定使用指定端口,不自动切换 + // port = await getPort({ port }).catch(_ => { + // console.log(`端口 ${port} 被占用,正在寻找可用端口...`) + // return getPort() + // }) + console.log(`doocs/md-cli v${packageJson.version}`) console.log(`服务启动中...`) diff --git a/packages/md-cli/package.json b/packages/md-cli/package.json index cffb74311..cb0bd5689 100644 --- a/packages/md-cli/package.json +++ b/packages/md-cli/package.json @@ -16,7 +16,8 @@ "public", "index.js", "server.js", - "util.js" + "util.js", + "renderer.js" ], "keywords": [], "author": "yanglbme", @@ -31,9 +32,18 @@ "dependencies": { "express": "^5.2.1", "form-data": "4.0.5", + "front-matter": "^4.0.2", "get-port": "7.1.0", + "highlight.js": "^11.9.0", "http-proxy-middleware": "^3.0.5", - "multer": "^2.0.2" + "juice": "^11.0.3", + "marked": "^17.0.1", + "multer": "^2.0.2", + "node-fetch": "^3.3.2", + "postcss": "^8.5.3", + "postcss-calc": "^10.1.1", + "postcss-custom-properties": "^14.0.4", + "reading-time": "^1.5.0" }, "devDependencies": { "nodemon": "^3.1.11" diff --git a/packages/md-cli/renderer.js b/packages/md-cli/renderer.js new file mode 100644 index 000000000..ffd39934f --- /dev/null +++ b/packages/md-cli/renderer.js @@ -0,0 +1,540 @@ +import { marked } from 'marked'; +import hljs from 'highlight.js'; +import frontMatter from 'front-matter'; +import readingTime from 'reading-time'; +import juice from 'juice'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; +import postcss from 'postcss'; +import postcssCalc from 'postcss-calc'; +import postcssCustomProperties from 'postcss-custom-properties'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const require = createRequire(import.meta.url); + +// ============================================================================ +// CSS 加载和处理工具 +// ============================================================================ + +/** + * 加载主题 CSS + * @param {string} themeName - 主题名称 + * @returns {string} CSS 内容 + */ +function loadThemeCss(themeName) { + try { + const themeCssDir = path.resolve(__dirname, '../shared/src/configs/theme-css'); + + // 1. 加载基础样式 + const basePath = path.join(themeCssDir, 'base.css'); + let cssContent = fs.existsSync(basePath) ? fs.readFileSync(basePath, 'utf-8') : ''; + + // 2. 加载 default 主题作为基础 + const defaultPath = path.join(themeCssDir, 'default.css'); + if (fs.existsSync(defaultPath)) { + cssContent += '\n' + fs.readFileSync(defaultPath, 'utf-8'); + } + + // 3. 如果请求的不是 default 主题,叠加主题特定样式 + if (themeName && themeName !== 'default') { + const themePath = path.join(themeCssDir, `${themeName}.css`); + if (fs.existsSync(themePath)) { + cssContent += '\n' + fs.readFileSync(themePath, 'utf-8'); + } + } + + return cssContent; + } catch (e) { + console.error(`Failed to load theme CSS for ${themeName}:`, e); + return ''; + } +} + +/** + * 加载代码高亮主题 CSS + * @param {string} themeName - 高亮主题名称 + * @returns {string} CSS 内容 + */ +function loadHighlightCss(themeName) { + try { + let highlightPath; + try { + const highlightBase = path.dirname(require.resolve('highlight.js/package.json')); + highlightPath = path.join(highlightBase, 'styles', `${themeName}.css`); + } catch { + highlightPath = path.resolve(__dirname, '../../node_modules/highlight.js/styles', `${themeName}.css`); + if (!fs.existsSync(highlightPath)) { + highlightPath = path.resolve(__dirname, 'node_modules/highlight.js/styles', `${themeName}.css`); + } + } + + if (fs.existsSync(highlightPath)) { + return fs.readFileSync(highlightPath, 'utf-8'); + } + return ''; + } catch (e) { + console.warn(`Failed to load highlight CSS for ${themeName}`, e); + return ''; + } +} + +/** + * 生成 CSS 变量(与页面端 generateCSSVariables 一致) + * @param {object} config - 配置对象 + * @returns {string} CSS 变量字符串 + */ +function generateCSSVariables(config) { + return ` +:root { + /* 动态配置变量 */ + --md-primary-color: ${config.primaryColor}; + --md-font-family: ${config.fontFamily}; + --md-font-size: ${config.fontSize}; +} + +/* 段落缩进和对齐 */ +.preview-wrapper p { + ${config.isUseIndent ? 'text-indent: 2em;' : ''} + ${config.isUseJustify ? 'text-align: justify;' : ''} +} + `.trim(); +} + +/** + * 使用 PostCSS 处理 CSS(与页面端 processCSS 一致) + * @param {string} css - CSS 字符串 + * @returns {Promise} 处理后的 CSS + */ +async function processCSS(css) { + try { + const result = await postcss([ + postcssCustomProperties({ + preserve: false, + }), + postcssCalc({ + preserve: false, + mediaQueries: false, + selectors: false, + }), + ]).process(css, { + from: undefined, + }); + + return result.css; + } catch (error) { + console.warn(`[processCSS] CSS 处理失败,使用原始 CSS:`, error); + return css; + } +} + +/** + * 后处理 CSS - 替换未被 PostCSS 处理的变量 + * @param {string} css - CSS 字符串 + * @param {object} options - 配置选项 + * @returns {string} 处理后的 CSS + */ +function postProcessCss(css, options) { + if (!css) return ''; + let processed = css; + + // 替换可能残留的 CSS 变量 + if (options.primaryColor) { + processed = processed.replace(/var\(--md-primary-color\)/g, options.primaryColor); + } + if (options.fontSize) { + processed = processed.replace(/var\(--md-font-size\)/g, options.fontSize); + } + if (options.fontFamily) { + processed = processed.replace(/var\(--font-family\)/g, options.fontFamily); + processed = processed.replace(/var\(--md-font-family\)/g, options.fontFamily); + } + + // 处理 hsl 变量和其他默认值 + processed = processed.replace(/hsl\(var\(--foreground\)\)/g, '#333'); + processed = processed.replace(/var\(--foreground\)/g, '#333'); + processed = processed.replace(/var\(--blockquote-background\)/g, '#f8f8f8'); + + return processed; +} + +// ============================================================================ +// HTML 渲染工具 +// ============================================================================ + +// Mac 代码块 SVG +const macCodeSvg = ` + + + + + +`.trim(); + +/** + * 转义 HTML 特殊字符 + * @param {string} text - 原始文本 + * @returns {string} 转义后的文本 + */ +function escapeHtml(text) { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/`/g, '`'); +} + +/** + * 格式化高亮代码 + * @param {string} html - 高亮后的 HTML + * @param {boolean} preserveNewlines - 是否保留换行 + * @returns {string} 格式化后的 HTML + */ +function formatHighlightedCode(html, preserveNewlines = false) { + let formatted = html; + formatted = formatted.replace(/(]*>[^<]*<\/span>)(\s+)(]*>[^<]*<\/span>)/g, (_, span1, spaces, span2) => span1 + span2.replace(/^(]*>)/, `$1${spaces}`)); + formatted = formatted.replace(/(\s+)(]*>)/g, (_, spaces, span) => span.replace(/^(]*>)/, `$1${spaces}`)); + formatted = formatted.replace(/\t/g, ' '); + + if (preserveNewlines) { + formatted = formatted.replace(/\r\n/g, '
').replace(/\n/g, '
').replace(/(>[^<]+)|(^[^<]+)/g, (str) => str.replace(/\s/g, ' ')); + } else { + formatted = formatted.replace(/(>[^<]+)|(^[^<]+)/g, (str) => str.replace(/\s/g, ' ')); + } + return formatted; +} + +/** + * 高亮并格式化代码(与页面端 highlightAndFormatCode 一致) + * @param {string} text - 代码文本 + * @param {string} language - 语言 + * @param {boolean} showLineNumber - 是否显示行号 + * @returns {string} 高亮后的 HTML + */ +function highlightAndFormatCode(text, language, showLineNumber) { + let highlighted = ''; + const validLanguage = hljs.getLanguage(language) ? language : 'plaintext'; + + if (showLineNumber) { + const rawLines = text.replace(/\r\n/g, '\n').split('\n'); + const highlightedLines = rawLines.map((lineRaw) => { + const lineHtml = hljs.highlight(lineRaw, { language: validLanguage }).value; + const formatted = formatHighlightedCode(lineHtml, false); + return formatted === '' ? ' ' : formatted; + }); + + const lineNumbersHtml = highlightedLines.map((_, idx) => `
${idx + 1}
`).join(''); + const codeInnerHtml = highlightedLines.join('
'); + const codeLinesHtml = `
${codeInnerHtml}
`; + const lineNumberColumnStyles = 'text-align:right;padding:8px 0;border-right:1px solid rgba(0,0,0,0.04);user-select:none;background:var(--code-bg,transparent);'; + + highlighted = ` +
+
${lineNumbersHtml}
+
${codeLinesHtml}
+
+ `; + } else { + const rawHighlighted = hljs.highlight(text, { language: validLanguage }).value; + highlighted = formatHighlightedCode(rawHighlighted, true); + } + + return highlighted; +} + +/** + * 生成带 CSS 类的内容(与页面端 styledContent 一致) + * @param {string} styleLabel - CSS 类名标识 + * @param {string} content - 内容 + * @param {string} [tagName] - HTML 标签名 + * @returns {string} HTML 字符串 + */ +function styledContent(styleLabel, content, tagName) { + const tag = tagName ?? styleLabel; + const className = `${styleLabel.replace(/_/g, '-')}`; + const headingAttr = /^h\d$/.test(tag) ? ' data-heading="true"' : ''; + return `<${tag} class="${className}"${headingAttr}>${content}`; +} + +/** + * 构建脚注数组 HTML + * @param {Array} footnotes - 脚注数组 + * @returns {string} HTML 字符串 + */ +function buildFootnoteArray(footnotes) { + return footnotes + .map(([index, title, link]) => + link === title + ? `[${index}]: ${title}
` + : `[${index}] ${title}: ${link}
` + ) + .join('\n'); +} + +/** + * 转换图注格式 + * @param {string} legend - 图注格式配置 + * @param {string|null} text - alt 文本 + * @param {string|null} title - title 文本 + * @returns {string} 图注文本 + */ +function transformLegend(legend, text, title) { + if (!legend || legend === 'none') return ''; + const options = legend.split('-'); + for (const option of options) { + if (option === 'alt' && text) return text; + if (option === 'title' && title) return title; + } + return ''; +} + +// ============================================================================ +// 主渲染函数 +// ============================================================================ + +/** + * 渲染 Markdown 为 HTML(与页面端逻辑对齐) + * @param {string} markdown - Markdown 内容 + * @param {object} options - 渲染选项 + * @returns {Promise<{html: string, readingTime: object}>} 渲染结果 + */ +export async function renderMarkdown(markdown, options = {}) { + const { + theme = 'default', + primaryColor = '#0F4C81', + fontFamily = "-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif", + fontSize = '16px', + isMacCodeBlock = true, + isShowLineNumber = false, + legend = 'alt', + isCiteStatus = false, + isCountStatus = false, + isUseIndent = false, + isUseJustify = false, + highlightTheme = 'github', + themeMode = 'light' + } = options; + + // 1. Front-matter 解析 + const { attributes, body } = frontMatter(markdown); + const readingTimeResult = readingTime(body); + + // 2. 配置渲染器状态 + const footnotes = []; + let footnoteIndex = 0; + const listOrderedStack = []; + const listCounters = []; + + // 辅助函数:添加脚注 + function addFootnote(title, link) { + const existing = footnotes.find(([, , l]) => l === link); + if (existing) return existing[0]; + footnotes.push([++footnoteIndex, title, link]); + return footnoteIndex; + } + + // 3. 配置 marked 渲染器(与页面端 renderer-impl.ts 对齐) + const renderer = { + heading({ tokens, depth }) { + const text = this.parser.parseInline(tokens); + return styledContent(`h${depth}`, text); + }, + + paragraph({ tokens }) { + const text = this.parser.parseInline(tokens); + const isFigureImage = text.includes('${macCodeSvg}` + : ''; + + const code = `${highlighted}`; + return `
${span}${code}
`; + }, + + codespan({ text }) { + const escaped = escapeHtml(text); + return styledContent('codespan', escaped, 'code'); + }, + + list({ ordered, items, start = 1 }) { + listOrderedStack.push(ordered); + listCounters.push(Number(start)); + const html = items.map(item => this.listitem(item)).join(''); + listOrderedStack.pop(); + listCounters.pop(); + return styledContent(ordered ? 'ol' : 'ul', html); + }, + + listitem(token) { + const ordered = listOrderedStack[listOrderedStack.length - 1]; + const idx = listCounters[listCounters.length - 1]++; + const prefix = ordered ? `${idx}. ` : '• '; + + let content; + try { + content = this.parser.parseInline(token.tokens); + } catch { + content = this.parser.parse(token.tokens).replace(/^]*)?>([\\s\\S]*?)<\/p>/, '$1'); + } + return styledContent('listitem', `${prefix}${content}`, 'li'); + }, + + image({ href, title, text }) { + const subText = transformLegend(legend, text, title); + const subHtml = subText ? styledContent('figcaption', subText) : ''; + const titleAttr = title ? ` title="${title}"` : ''; + return `
${text}${subHtml}
`; + }, + + link({ href, title, text, tokens }) { + const parsedText = this.parser.parseInline(tokens); + if (/^https?:\/\/mp\.weixin\.qq\.com/.test(href)) { + return `${parsedText}`; + } + if (href === text) return parsedText; + + if (isCiteStatus) { + const ref = addFootnote(title || text, href); + return `${parsedText}[${ref}]`; + } + return `${parsedText}`; + }, + + strong({ tokens }) { + return styledContent('strong', this.parser.parseInline(tokens)); + }, + + em({ tokens }) { + return styledContent('em', this.parser.parseInline(tokens)); + }, + + table({ header, rows }) { + const headerRow = header.map(cell => styledContent('th', this.parser.parseInline(cell.tokens))).join(''); + const body = rows.map(row => styledContent('tr', row.map(cell => styledContent('td', this.parser.parseInline(cell.tokens))).join(''))).join(''); + return `
${headerRow}${body}
`; + }, + + hr() { + return styledContent('hr', ''); + } + }; + + marked.use({ renderer, breaks: true }); + + // 4. 解析 Markdown + const htmlContent = marked.parse(body); + + // 5. 后处理:阅读时间统计 + let readingTimeHtml = ''; + if (isCountStatus && readingTimeResult.words) { + readingTimeHtml = ` +
+

字数 ${readingTimeResult.words},阅读大约需 ${Math.ceil(readingTimeResult.minutes)} 分钟

+
+ `; + } + + // 6. 后处理:脚注 + let footnotesHtml = ''; + if (footnotes.length > 0) { + footnotesHtml = styledContent('h4', '引用链接') + styledContent('footnotes', buildFootnoteArray(footnotes), 'p'); + } + + // 7. 后处理:附加样式 + const additionStyle = ` + + `; + + const macSignStyle = ` + + `; + + const h2StrongStyle = ` + + `; + + // 8. 组装 HTML + const innerHtml = readingTimeHtml + htmlContent + footnotesHtml + additionStyle + macSignStyle + h2StrongStyle; + const wrappedHtml = styledContent('container', innerHtml, 'section'); + + // 9. 加载并处理 CSS + const themeCss = loadThemeCss(theme); + const highlightCss = loadHighlightCss(highlightTheme); + + // 生成 CSS 变量 + const variablesCss = generateCSSVariables({ + primaryColor, + fontFamily, + fontSize, + isUseIndent, + isUseJustify + }); + + // 合并 CSS + let combinedCss = [variablesCss, themeCss, highlightCss].filter(Boolean).join('\n'); + + // 使用 PostCSS 处理 CSS 变量 + combinedCss = await processCSS(combinedCss); + + // 后处理残留的 CSS 变量 + combinedCss = postProcessCss(combinedCss, { primaryColor, fontSize, fontFamily }); + + // 10. 构建最终 HTML + const baseHtml = ` +
+
+ ${wrappedHtml} +
+
+ `; + + // 11. 使用 juice 内联样式 + const inlinedHtml = juice.inlineContent(baseHtml, combinedCss, { + inlinePseudoElements: true, + preserveImportant: true, + resolveCSSVariables: false + }); + + return { + html: inlinedHtml, + readingTime: readingTimeResult + }; +} diff --git a/packages/md-cli/server.js b/packages/md-cli/server.js index 388561c70..d817ca6c6 100644 --- a/packages/md-cli/server.js +++ b/packages/md-cli/server.js @@ -2,6 +2,7 @@ import express from 'express' import multer from 'multer' import path from 'node:path' import fs from 'node:fs' +import os from 'node:os' import { fileURLToPath } from 'node:url' import { dirname } from 'node:path' import { createProxyMiddleware } from 'http-proxy-middleware' @@ -10,6 +11,7 @@ import { parseArgv, colors } from './util.js' +import { renderMarkdown } from './renderer.js' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) @@ -53,6 +55,126 @@ export function createServer(port = 8800) { app.use('/public', express.static(path.join(__dirname, 'public'))) + // 公共参数解析和渲染函数 + async function parseAndRender(req) { + let { + markdown, + path: filePath, + title, + theme = 'default', + primaryColor = '#0F4C81', + fontFamily = "-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif", + fontSize = '16px', + highlightTheme = 'github', + isMacCodeBlock = true, + isShowLineNumber = false, + legend = 'alt', + isCiteStatus = false, + isCountStatus = false, + isUseIndent = false, + isUseJustify = false, + themeMode = 'light' + } = req.body; + + if (!markdown && filePath) { + if (fs.existsSync(filePath)) { + markdown = fs.readFileSync(filePath, 'utf-8'); + } else { + throw new Error('File not found'); + } + } + + if (!markdown) { + throw new Error('Markdown content is required'); + } + + const result = await renderMarkdown(markdown, { + theme, + primaryColor, + fontFamily, + fontSize, + highlightTheme, + isMacCodeBlock, + isShowLineNumber, + legend, + isCiteStatus, + isCountStatus, + isUseIndent, + isUseJustify, + themeMode + }); + + return { ...result, filePath, title }; + } + + // API Route for Rendering Markdown + app.post('/api/render', async (req, res) => { + try { + const { html } = await parseAndRender(req); + res.json({ html }); + } catch (error) { + console.error('Render error:', error); + if (error.message === 'File not found' || error.message === 'Markdown content is required') { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: error.message }); + } + }); + + // API Route for Rendering Markdown to HTML file (Preview) + app.post('/api/render/html', async (req, res) => { + try { + const { html, filePath, title } = await parseAndRender(req); + + // 确定页面标题 + let pageTitle = title || 'Markdown Render Preview'; + if (!title && filePath) { + pageTitle = path.basename(filePath, path.extname(filePath)); + } + + // 封装成完整 HTML 页面 + const fullHtml = ` + + + + + ${pageTitle} + + + + ${html} + +`; + + // 确定输出文件名 + let filename = 'output.html'; + if (filePath) { + const base = path.basename(filePath, path.extname(filePath)); + filename = `${base}.html`; + } + + const downloadDir = path.join(os.homedir(), 'Downloads'); + if (!fs.existsSync(downloadDir)) { + fs.mkdirSync(downloadDir, { recursive: true }); + } + + const savePath = path.join(downloadDir, filename); + fs.writeFileSync(savePath, fullHtml, 'utf-8'); + console.log(`Rendered HTML saved to: ${savePath}`); + + res.download(savePath, filename); + } catch (error) { + console.error('Render error:', error); + if (error.message === 'File not found' || error.message === 'Markdown content is required') { + return res.status(400).send(error.message); + } + res.status(500).send(error.message); + } + }); + // 文件上传 API app.post('/upload', upload.single('file'), async (req, res) => { try { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 385b347c2..dc9613cd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -384,15 +384,42 @@ importers: form-data: specifier: 4.0.5 version: 4.0.5 + front-matter: + specifier: ^4.0.2 + version: 4.0.2 get-port: specifier: 7.1.0 version: 7.1.0 + highlight.js: + specifier: ^11.9.0 + version: 11.11.1 http-proxy-middleware: specifier: ^3.0.5 version: 3.0.5 + juice: + specifier: 11.0.3 + version: 11.0.3 + marked: + specifier: ^17.0.1 + version: 17.0.1 multer: specifier: ^2.0.2 version: 2.0.2 + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 + postcss: + specifier: ^8.5.3 + version: 8.5.6 + postcss-calc: + specifier: ^10.1.1 + version: 10.1.1(postcss@8.5.6) + postcss-custom-properties: + specifier: ^14.0.4 + version: 14.0.6(postcss@8.5.6) + reading-time: + specifier: ^1.5.0 + version: 1.5.0 devDependencies: nodemon: specifier: ^3.1.11 @@ -1124,6 +1151,13 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@csstools/cascade-layer-name-parser@2.0.5': + resolution: {integrity: sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + '@csstools/cascade-layer-name-parser@3.0.0': resolution: {integrity: sha512-/3iksyevwRfSJx5yH0RkcrcYXwuhMQx3Juqf40t97PeEy2/Mz2TItZ/z/216qpe4GgOyFBP8MKIwVvytzHmfIQ==} engines: {node: '>=20.19.0'} @@ -1149,6 +1183,12 @@ packages: '@csstools/css-parser-algorithms': ^4.0.0 '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms@4.0.0': resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} engines: {node: '>=20.19.0'} @@ -1158,10 +1198,20 @@ packages: '@csstools/css-syntax-patches-for-csstree@1.0.26': resolution: {integrity: sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==} + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@csstools/css-tokenizer@4.0.0': resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} + '@csstools/utilities@2.0.0': + resolution: {integrity: sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + '@csstools/utilities@3.0.0': resolution: {integrity: sha512-etDqA/4jYvOGBM6yfKCOsEXfH96BKztZdgGmGqKi2xHnDe0ILIBraRspwgYatJH9JsCZ5HCGoCst8w18EKOAdg==} engines: {node: '>=20.19.0'} @@ -3897,6 +3947,10 @@ packages: dagre@0.8.5: resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-urls@6.0.1: resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} engines: {node: '>=20'} @@ -4548,6 +4602,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -4632,6 +4690,10 @@ packages: resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==} engines: {node: '>= 18'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -5982,9 +6044,18 @@ packages: node-addon-api@4.3.0: resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-forge@1.3.3: resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} engines: {node: '>= 6.13.0'} @@ -6346,6 +6417,12 @@ packages: peerDependencies: postcss: ^8.4.38 + postcss-custom-properties@14.0.6: + resolution: {integrity: sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + postcss-custom-properties@15.0.0: resolution: {integrity: sha512-FsD3VNtFr3qmspvIobDRszK9onKPHp8iHG4Aox2Nnm9SL93uw5GDw4z+NM7zWKiw6U+DSNm24JUm4coyIyanzQ==} engines: {node: '>=20.19.0'} @@ -7675,6 +7752,10 @@ packages: resolution: {integrity: sha512-NlfnGF8MY9ZUwFjyq3vOUBx7KwF8bmE+ywR781SB0nWB6MoMxN4BA8gtgP1KGTZo/O/AyWJz7HZpR704eaj4mg==} engines: {node: '>=10.0.0'} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webidl-conversions@8.0.1: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} @@ -9184,6 +9265,11 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@csstools/cascade-layer-name-parser@2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/cascade-layer-name-parser@3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) @@ -9203,14 +9289,24 @@ snapshots: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-tokenizer': 4.0.0 '@csstools/css-syntax-patches-for-csstree@1.0.26': {} + '@csstools/css-tokenizer@3.0.4': {} + '@csstools/css-tokenizer@4.0.0': {} + '@csstools/utilities@2.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + '@csstools/utilities@3.0.0(postcss@8.5.6)': dependencies: postcss: 8.5.6 @@ -12183,6 +12279,8 @@ snapshots: graphlib: 2.1.8 lodash: 4.17.23 + data-uri-to-buffer@4.0.1: {} + data-urls@6.0.1: dependencies: whatwg-mimetype: 5.0.0 @@ -13016,6 +13114,11 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fflate@0.8.2: {} file-entry-cache@8.0.0: @@ -13097,6 +13200,10 @@ snapshots: formdata-node@6.0.3: {} + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -14622,8 +14729,16 @@ snapshots: node-addon-api@4.3.0: optional: true + node-domexception@1.0.0: {} + node-fetch-native@1.6.7: {} + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-forge@1.3.3: {} node-notifier@10.0.1: @@ -15047,6 +15162,15 @@ snapshots: postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 + postcss-custom-properties@14.0.6(postcss@8.5.6): + dependencies: + '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + postcss-custom-properties@15.0.0(postcss@8.5.6): dependencies: '@csstools/cascade-layer-name-parser': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) @@ -16619,6 +16743,8 @@ snapshots: mime: 2.6.0 valid-data-url: 3.0.1 + web-streams-polyfill@3.3.3: {} + webidl-conversions@8.0.1: {} webpack-cli@6.0.1(webpack@5.105.0): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 82e0edaf7..d49a84fc8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,5 @@ +catalogMode: prefer + shellEmulator: true trustPolicy: no-downgrade