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) => ``).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 = `
+
+ `;
+ } 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}${tag}>`;
+}
+
+/**
+ * 构建脚注数组 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 `
${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 ``;
+ },
+
+ 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 = `
+
+
+
+ `;
+
+ // 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