Commit b22cb9e4 authored by xuchentao's avatar xuchentao

feat: import Word articles with embedded images

parent 17a35237
Pipeline #433 passed with stage
in 20 seconds
...@@ -12,7 +12,7 @@ variables: ...@@ -12,7 +12,7 @@ variables:
SHARED_ENV: "/root/yinzhuang/shared/.env" SHARED_ENV: "/root/yinzhuang/shared/.env"
DEPLOY_LOCK: "/root/yinzhuang/shared/deploy.lock" DEPLOY_LOCK: "/root/yinzhuang/shared/deploy.lock"
APP_NAME: "yinzhuang" APP_NAME: "yinzhuang"
SITE_URL: "http://127.0.0.1:8790/" SITE_URL: "http://127.0.0.1:8791/"
PM2_BIN: "/usr/bin/pm2" PM2_BIN: "/usr/bin/pm2"
PM2_HOME_DIR: "/root/.pm2" PM2_HOME_DIR: "/root/.pm2"
PM2_USE_SUDO: "1" PM2_USE_SUDO: "1"
......
...@@ -28,13 +28,13 @@ npm run build ...@@ -28,13 +28,13 @@ npm run build
npm start npm start
``` ```
- 官网:`http://localhost:8790/` - 官网:`http://localhost:8791/`
- 文章后台:`http://localhost:8790/admin` - 文章后台:`http://localhost:8791/admin`
- 银饰资讯:`http://localhost:8790/articles/` - 银饰资讯:`http://localhost:8791/articles/`
启动命令会自动读取项目根目录的 `.env`,其中包含: 启动命令会自动读取项目根目录的 `.env`,其中包含:
- `CMS_PORT`:官网与后台共用的服务端口,默认 `8790`(斯嘉丽项目端口 `8788` + 1) - `CMS_PORT`:官网与后台共用的服务端口,默认 `8791`
- `CMS_PASSWORD`:后台登录密码 - `CMS_PASSWORD`:后台登录密码
- `CMS_SECRET`:后台会话签名密钥 - `CMS_SECRET`:后台会话签名密钥
- `CMS_API_KEY`:外部程序调用管理 API 时使用的 Bearer Token - `CMS_API_KEY`:外部程序调用管理 API 时使用的 Bearer Token
...@@ -64,10 +64,34 @@ npm start ...@@ -64,10 +64,34 @@ npm start
| 发布 | `POST /api/cms/articles/:slug/unpublish` | 下架并生成静态页面 | | 发布 | `POST /api/cms/articles/:slug/unpublish` | 下架并生成静态页面 |
| 工具 | `POST /api/cms/preview` | Markdown 预览 | | 工具 | `POST /api/cms/preview` | Markdown 预览 |
| 工具 | `POST /api/cms/uploads` | 上传文章图片 | | 工具 | `POST /api/cms/uploads` | 上传文章图片 |
| 导入 | `POST /api/cms/imports/word` | 导入 `.docx` 为 Markdown 草稿并提取内嵌图片 |
| 构建 | `GET /api/cms/build` | 查询静态构建状态 | | 构建 | `GET /api/cms/build` | 查询静态构建状态 |
除会话登录接口外,外部程序可通过 `Authorization: Bearer <CMS_API_KEY>` 调用这些地址。 除会话登录接口外,外部程序可通过 `Authorization: Bearer <CMS_API_KEY>` 调用这些地址。
后台文章列表的“导入文件”支持 `.md``.docx`:Markdown 的第一个一级标题会作为标题并从正文移除;Word 文件由下述接口转换并直接创建草稿。导入默认使用分类列表中的第一个分类,保存后可在编辑页调整。
### 通过 API 导入 Word
Word 导入接口接收不超过 20MB 的 `.docx` Data URL,不支持旧版 `.doc`。文档第一个一级标题会作为默认文章标题并从正文中移除;也可以显式传入 `title` 覆盖。内嵌的 PNG、JPG、WEBP、GIF 图片会自动保存到上传目录并转成 Markdown 图片链接。导入只创建草稿,不会自动发布;转换或建稿失败时会删除本次已经提取的图片。
```http
POST /api/cms/imports/word
Authorization: Bearer <CMS_API_KEY>
Content-Type: application/json
```
```json
{
"fileName": "银饰文章.docx",
"dataUrl": "data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,...",
"category": "佩戴养护",
"title": "可选的自定义标题"
}
```
成功后返回自动生成的 `slug`、最终 `title``imageCount`、转换警告和 `new-draft` 草稿状态。确认内容后再调用 `POST /api/cms/articles/:slug/publish` 发布。
## 内容目录 ## 内容目录
- `src/content/articles/`:后台工作稿,包括草稿、待发布和已发布文章 - `src/content/articles/`:后台工作稿,包括草稿、待发布和已发布文章
...@@ -127,7 +151,7 @@ CMS_BUILD_LOCK=/root/yinzhuang/shared/site-build.lock ...@@ -127,7 +151,7 @@ CMS_BUILD_LOCK=/root/yinzhuang/shared/site-build.lock
- 部署根目录:`/root/yinzhuang` - 部署根目录:`/root/yinzhuang`
- 持久化配置:`/root/yinzhuang/shared/.env` - 持久化配置:`/root/yinzhuang/shared/.env`
- PM2 应用名:`yinzhuang` - PM2 应用名:`yinzhuang`
- 服务端口:`8790` - 服务端口:`8791`
- Nginx 配置:`/etc/nginx/conf.d/yinzhuang.conf` - Nginx 配置:`/etc/nginx/conf.d/yinzhuang.conf`
- 公网入口:`101.126.10.129:80` - 公网入口:`101.126.10.129:80`
...@@ -138,7 +162,7 @@ CMS_BUILD_LOCK=/root/yinzhuang/shared/site-build.lock ...@@ -138,7 +162,7 @@ CMS_BUILD_LOCK=/root/yinzhuang/shared/site-build.lock
```env ```env
CMS_DATA_DIR=/root/yinzhuang/shared/cms-data CMS_DATA_DIR=/root/yinzhuang/shared/cms-data
CMS_BUILD_LOCK=/root/yinzhuang/shared/site-build.lock CMS_BUILD_LOCK=/root/yinzhuang/shared/site-build.lock
CMS_PORT=8790 CMS_PORT=8791
``` ```
`CMS_DATA_DIR` 中包含工作稿、已发布文章、分类和上传文件。数据库文件、其他业务数据及所有运行期生成的持久化内容也必须放在 `shared/` 中,再通过环境变量或软链接提供给应用。`current` 的全部内容会在每次部署时删除,不能用于持久化数据。 `CMS_DATA_DIR` 中包含工作稿、已发布文章、分类和上传文件。数据库文件、其他业务数据及所有运行期生成的持久化内容也必须放在 `shared/` 中,再通过环境变量或软链接提供给应用。`current` 的全部内容会在每次部署时删除,不能用于持久化数据。
......
...@@ -19,7 +19,7 @@ npm run build ...@@ -19,7 +19,7 @@ npm run build
npm start npm start
``` ```
生产模式默认监听 `http://127.0.0.1:8790/` 生产模式默认监听 `http://127.0.0.1:8791/`
## 常用命令 ## 常用命令
...@@ -40,7 +40,7 @@ npm start # 启动 standalone 服务 ...@@ -40,7 +40,7 @@ npm start # 启动 standalone 服务
- Astro standalone 输出:`dist/server/``dist/client/` - Astro standalone 输出:`dist/server/``dist/client/`
- GitLab CI:`.gitlab-ci.yml` - GitLab CI:`.gitlab-ci.yml`
- Nginx 配置:`deploy/nginx/yinzhuang.conf` - Nginx 配置:`deploy/nginx/yinzhuang.conf`
- 生产端口:`8790` - 生产端口:`8791`
- 站点域名:`www.inzung.cn` - 站点域名:`www.inzung.cn`
- 服务器目录:`/root/yinzhuang/current`(唯一运行版本,实体目录)与 `/root/yinzhuang/shared`(持久化数据) - 服务器目录:`/root/yinzhuang/current`(唯一运行版本,实体目录)与 `/root/yinzhuang/shared`(持久化数据)
......
# 银妆官网 Nginx 配置 # 银妆官网 Nginx 配置
# 通过服务器 IP 访问,反向代理到本机 8790 端口。 # 通过服务器 IP 访问,反向代理到本机 8791 端口。
server { server {
listen 80; listen 80;
server_name inzung.cn www.inzung.cn 101.126.10.129; server_name inzung.cn www.inzung.cn 101.126.10.129;
...@@ -8,7 +8,7 @@ server { ...@@ -8,7 +8,7 @@ server {
client_max_body_size 28m; client_max_body_size 28m;
location / { location / {
proxy_pass http://127.0.0.1:8790; proxy_pass http://127.0.0.1:8791;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
......
...@@ -12,10 +12,16 @@ ...@@ -12,10 +12,16 @@
"@astrojs/sitemap": "^3.7.3", "@astrojs/sitemap": "^3.7.3",
"astro": "5.18.2", "astro": "5.18.2",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"marked": "^14.1.4" "mammoth": "^1.12.0",
"marked": "^14.1.4",
"turndown": "^7.2.4",
"turndown-plugin-gfm": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/check": "^0.9.6", "@astrojs/check": "^0.9.6",
"@types/turndown": "^5.0.6",
"docx": "^9.7.1",
"tsx": "^4.23.1",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"engines": { "engines": {
...@@ -1207,6 +1213,12 @@ ...@@ -1207,6 +1213,12 @@
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@mixmark-io/domino": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
"license": "BSD-2-Clause"
},
"node_modules/@oslojs/encoding": { "node_modules/@oslojs/encoding": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz",
...@@ -1699,6 +1711,13 @@ ...@@ -1699,6 +1711,13 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/turndown": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.6.tgz",
"integrity": "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/unist": { "node_modules/@types/unist": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
...@@ -1809,6 +1828,15 @@ ...@@ -1809,6 +1828,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@xmldom/xmldom": {
"version": "0.8.13",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.18.0", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
...@@ -2122,6 +2150,32 @@ ...@@ -2122,6 +2150,32 @@
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
"license": "MIT"
},
"node_modules/boolbase": { "node_modules/boolbase": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
...@@ -2325,6 +2379,12 @@ ...@@ -2325,6 +2379,12 @@
"integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/crossws": { "node_modules/crossws": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz",
...@@ -2530,12 +2590,72 @@ ...@@ -2530,12 +2590,72 @@
"node": ">=0.3.1" "node": ">=0.3.1"
} }
}, },
"node_modules/dingbat-to-unicode": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
"license": "BSD-2-Clause"
},
"node_modules/dlv": { "node_modules/dlv": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/docx": {
"version": "9.7.1",
"resolved": "https://registry.npmjs.org/docx/-/docx-9.7.1.tgz",
"integrity": "sha512-ilXFf9Moz47ABjFpDiA5s1w9lpb4EFSp7+5iiJSbfyYDM+bpZdAgLlSr7fW4aXhVe/E+F6QCv0EvRVFEd5CsWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "^25.2.3",
"hash.js": "^1.1.7",
"jszip": "^3.10.1",
"nanoid": "^5.1.3",
"xml": "^1.0.1",
"xml-js": "^1.6.8"
},
"engines": {
"node": ">=10"
}
},
"node_modules/docx/node_modules/@types/node": {
"version": "25.9.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz",
"integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/docx/node_modules/nanoid": {
"version": "5.1.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz",
"integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/docx/node_modules/undici-types": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"dev": true,
"license": "MIT"
},
"node_modules/dom-serializer": { "node_modules/dom-serializer": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
...@@ -2612,6 +2732,15 @@ ...@@ -2612,6 +2732,15 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/duck": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
"license": "BSD",
"dependencies": {
"underscore": "^1.13.1"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
...@@ -2968,6 +3097,17 @@ ...@@ -2968,6 +3097,17 @@
"uncrypto": "^0.1.3" "uncrypto": "^0.1.3"
} }
}, },
"node_modules/hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/hast-util-from-html": { "node_modules/hast-util-from-html": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
...@@ -3187,6 +3327,12 @@ ...@@ -3187,6 +3327,12 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-meta-resolve": { "node_modules/import-meta-resolve": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
...@@ -3290,6 +3436,12 @@ ...@@ -3290,6 +3436,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
...@@ -3326,6 +3478,18 @@ ...@@ -3326,6 +3478,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/kind-of": { "node_modules/kind-of": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
...@@ -3345,6 +3509,15 @@ ...@@ -3345,6 +3509,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/longest-streak": { "node_modules/longest-streak": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
...@@ -3355,6 +3528,17 @@ ...@@ -3355,6 +3528,17 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/lop": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
"license": "BSD-2-Clause",
"dependencies": {
"duck": "^0.1.12",
"option": "~0.2.1",
"underscore": "^1.13.1"
}
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "11.5.2", "version": "11.5.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
...@@ -3384,6 +3568,39 @@ ...@@ -3384,6 +3568,39 @@
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
} }
}, },
"node_modules/mammoth": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz",
"integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==",
"license": "BSD-2-Clause",
"dependencies": {
"@xmldom/xmldom": "^0.8.6",
"argparse": "~1.0.3",
"base64-js": "^1.5.1",
"bluebird": "~3.4.0",
"dingbat-to-unicode": "^1.0.1",
"jszip": "^3.7.1",
"lop": "^0.4.2",
"path-is-absolute": "^1.0.0",
"underscore": "^1.13.1",
"xmlbuilder": "^10.0.0"
},
"bin": {
"mammoth": "bin/mammoth"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/mammoth/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/markdown-table": { "node_modules/markdown-table": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
...@@ -4225,6 +4442,13 @@ ...@@ -4225,6 +4442,13 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"dev": true,
"license": "ISC"
},
"node_modules/mrmime": { "node_modules/mrmime": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
...@@ -4366,6 +4590,12 @@ ...@@ -4366,6 +4590,12 @@
"regex-recursion": "^6.0.2" "regex-recursion": "^6.0.2"
} }
}, },
"node_modules/option": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
"license": "BSD-2-Clause"
},
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz",
...@@ -4415,6 +4645,12 @@ ...@@ -4415,6 +4645,12 @@
"integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parse-latin": { "node_modules/parse-latin": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz",
...@@ -4452,6 +4688,15 @@ ...@@ -4452,6 +4688,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/piccolore": { "node_modules/piccolore": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",
...@@ -4529,6 +4774,12 @@ ...@@ -4529,6 +4774,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/prompts": { "node_modules/prompts": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
...@@ -4580,6 +4831,21 @@ ...@@ -4580,6 +4831,21 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
...@@ -4882,6 +5148,12 @@ ...@@ -4882,6 +5148,12 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/sax": { "node_modules/sax": {
"version": "1.6.1", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
...@@ -4948,6 +5220,12 @@ ...@@ -4948,6 +5220,12 @@
"integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
...@@ -5092,6 +5370,15 @@ ...@@ -5092,6 +5370,15 @@
"integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
...@@ -5260,106 +5547,634 @@ ...@@ -5260,106 +5547,634 @@
"license": "0BSD", "license": "0BSD",
"optional": true "optional": true
}, },
"node_modules/type-fest": { "node_modules/tsx": {
"version": "4.41.0", "version": "4.23.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==",
"license": "(MIT OR CC0-1.0)", "devOptional": true,
"engines": { "license": "MIT",
"node": ">=16" "dependencies": {
"esbuild": "~0.28.0"
}, },
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typesafe-path": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/typesafe-path/-/typesafe-path-0.2.2.tgz",
"integrity": "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==",
"dev": true,
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsx": "dist/cli.mjs"
"tsserver": "bin/tsserver"
}, },
"engines": { "engines": {
"node": ">=14.17" "node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
} }
}, },
"node_modules/typescript-auto-import-cache": { "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
"version": "0.3.6", "version": "0.28.1",
"resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
"integrity": "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==", "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"cpu": [
"ppc64"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "optional": true,
"semver": "^7.3.8" "os": [
"aix"
],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/ufo": { "node_modules/tsx/node_modules/@esbuild/android-arm": {
"version": "1.6.4", "version": "0.28.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
"integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"license": "MIT" "cpu": [
}, "arm"
"node_modules/ultrahtml": { ],
"version": "1.7.0", "dev": true,
"resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz",
"integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==",
"license": "MIT"
},
"node_modules/uncrypto": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
},
"node_modules/unified": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
"integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
"license": "MIT", "license": "MIT",
"dependencies": { "optional": true,
"@types/unist": "^3.0.0", "os": [
"bail": "^2.0.0", "android"
"devlop": "^1.0.0", ],
"extend": "^3.0.0", "engines": {
"is-plain-obj": "^4.0.0", "node": ">=18"
"trough": "^2.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
} }
}, },
"node_modules/unifont": { "node_modules/tsx/node_modules/@esbuild/android-arm64": {
"version": "0.7.4", "version": "0.28.1",
"resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
"integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==", "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "optional": true,
"css-tree": "^3.1.0", "os": [
"ofetch": "^1.5.1", "android"
"ohash": "^2.0.11" ],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/unist-util-find-after": { "node_modules/tsx/node_modules/@esbuild/android-x64": {
"version": "5.0.0", "version": "0.28.1",
"resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
"integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/esbuild": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"devOptional": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.1",
"@esbuild/android-arm": "0.28.1",
"@esbuild/android-arm64": "0.28.1",
"@esbuild/android-x64": "0.28.1",
"@esbuild/darwin-arm64": "0.28.1",
"@esbuild/darwin-x64": "0.28.1",
"@esbuild/freebsd-arm64": "0.28.1",
"@esbuild/freebsd-x64": "0.28.1",
"@esbuild/linux-arm": "0.28.1",
"@esbuild/linux-arm64": "0.28.1",
"@esbuild/linux-ia32": "0.28.1",
"@esbuild/linux-loong64": "0.28.1",
"@esbuild/linux-mips64el": "0.28.1",
"@esbuild/linux-ppc64": "0.28.1",
"@esbuild/linux-riscv64": "0.28.1",
"@esbuild/linux-s390x": "0.28.1",
"@esbuild/linux-x64": "0.28.1",
"@esbuild/netbsd-arm64": "0.28.1",
"@esbuild/netbsd-x64": "0.28.1",
"@esbuild/openbsd-arm64": "0.28.1",
"@esbuild/openbsd-x64": "0.28.1",
"@esbuild/openharmony-arm64": "0.28.1",
"@esbuild/sunos-x64": "0.28.1",
"@esbuild/win32-arm64": "0.28.1",
"@esbuild/win32-ia32": "0.28.1",
"@esbuild/win32-x64": "0.28.1"
}
},
"node_modules/turndown": {
"version": "7.2.4",
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz",
"integrity": "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==",
"license": "MIT",
"dependencies": {
"@mixmark-io/domino": "^2.2.0"
},
"engines": {
"node": ">=18",
"npm": ">=9"
}
},
"node_modules/turndown-plugin-gfm": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz",
"integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==",
"license": "MIT"
},
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typesafe-path": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/typesafe-path/-/typesafe-path-0.2.2.tgz",
"integrity": "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==",
"dev": true,
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/typescript-auto-import-cache": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.6.tgz",
"integrity": "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.3.8"
}
},
"node_modules/ufo": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
"integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
"license": "MIT"
},
"node_modules/ultrahtml": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz",
"integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==",
"license": "MIT"
},
"node_modules/uncrypto": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
"license": "MIT"
},
"node_modules/underscore": {
"version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
},
"node_modules/unified": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
"integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"bail": "^2.0.0",
"devlop": "^1.0.0",
"extend": "^3.0.0",
"is-plain-obj": "^4.0.0",
"trough": "^2.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unifont": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz",
"integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==",
"license": "MIT",
"dependencies": {
"css-tree": "^3.1.0",
"ofetch": "^1.5.1",
"ohash": "^2.0.11"
}
},
"node_modules/unist-util-find-after": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
"integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/unist": "^3.0.0", "@types/unist": "^3.0.0",
...@@ -5603,6 +6418,12 @@ ...@@ -5603,6 +6418,12 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/vfile": { "node_modules/vfile": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
...@@ -6536,6 +7357,35 @@ ...@@ -6536,6 +7357,35 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
"dev": true,
"license": "MIT"
},
"node_modules/xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"sax": "^1.2.4"
},
"bin": {
"xml-js": "bin/cli.js"
}
},
"node_modules/xmlbuilder": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/xxhash-wasm": { "node_modules/xxhash-wasm": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz",
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
"build:astro": "astro check && astro build", "build:astro": "astro check && astro build",
"preview": "astro preview", "preview": "astro preview",
"check": "astro check", "check": "astro check",
"test": "node --import tsx/esm --test tests/*.test.ts",
"start": "node --env-file=.env server.mjs", "start": "node --env-file=.env server.mjs",
"admin": "npm start", "admin": "npm start",
"serve": "npm run build && npm start" "serve": "npm run build && npm start"
...@@ -22,10 +23,16 @@ ...@@ -22,10 +23,16 @@
"@astrojs/sitemap": "^3.7.3", "@astrojs/sitemap": "^3.7.3",
"astro": "5.18.2", "astro": "5.18.2",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"marked": "^14.1.4" "mammoth": "^1.12.0",
"marked": "^14.1.4",
"turndown": "^7.2.4",
"turndown-plugin-gfm": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/check": "^0.9.6", "@astrojs/check": "^0.9.6",
"@types/turndown": "^5.0.6",
"docx": "^9.7.1",
"tsx": "^4.23.1",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
} }
const $ = (selector) => document.querySelector(selector); const $ = (selector) => document.querySelector(selector);
const API_BASE = "/api/cms"; const API_BASE = "/api/cms";
const MAX_UPLOAD_SIZE = 20 * 1024 * 1024; const MAX_UPLOAD_SIZE = 20 * 1024 * 1024;
const DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
let categories = []; let categories = [];
let editingSlug = null; let editingSlug = null;
let editingStatus = "new-unsaved"; let editingStatus = "new-unsaved";
...@@ -456,6 +457,58 @@ async function openEditor(slug) { ...@@ -456,6 +457,58 @@ async function openEditor(slug) {
} }
$("#new-article").addEventListener("click", () => openEditor(null)); $("#new-article").addEventListener("click", () => openEditor(null));
$("#import-article").addEventListener("click", () => $("#article-file").click());
$("#article-file").addEventListener("change", async (event) => {
const file = event.target.files?.[0];
event.target.value = "";
if (!file) return;
if (file.size > MAX_UPLOAD_SIZE) {
$("#import-status").textContent = "文件不能超过 20MB";
return;
}
const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0];
if (![".md", ".docx"].includes(extension)) {
$("#import-status").textContent = "仅支持 Markdown(.md)或 Word(.docx)文件";
return;
}
$("#import-status").textContent = `正在导入 ${file.name}…`;
try {
let result;
if (extension === ".md") {
let body = await file.text();
const heading = body.match(/^#\s+(.+)$/m);
const title = heading?.[1].trim() || file.name.replace(/\.md$/i, "");
if (heading?.index !== undefined) {
body = `${body.slice(0, heading.index)}${body.slice(heading.index + heading[0].length)}`
.trim()
.replace(/\n{3,}/g, "\n\n");
}
if (!body) throw new Error("Markdown 文件中没有可导入的正文内容");
result = await api("/articles", { method: "POST", body: { title, category: categories[0], body } });
} else {
const dataUrl = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
result = await api("/imports/word", {
method: "POST",
body: { fileName: file.name, category: categories[0], dataUrl: String(dataUrl).replace(/^data:[^;,]*;/, `data:${DOCX_MIME};`) },
});
}
const successMessage = extension === ".docx"
? `导入成功:已创建草稿并提取 ${result.imageCount} 张图片`
: "导入成功:已创建 Markdown 草稿";
$("#import-status").textContent = successMessage;
await openEditor(result.slug);
$("#message").textContent = successMessage;
} catch (error) {
$("#import-status").textContent = error.message;
}
});
$("#back").addEventListener("click", () => { $("#back").addEventListener("click", () => {
if (dirty && !confirm("还有未保存的内容,确定返回文章列表吗?")) return; if (dirty && !confirm("还有未保存的内容,确定返回文章列表吗?")) return;
setView(false); setView(false);
......
...@@ -45,6 +45,7 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(209,37,26,.1 ...@@ -45,6 +45,7 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(209,37,26,.1
.view-head p { margin: 0 0 5px; color: var(--primary-dark); font-size: 11px; font-weight: 700; letter-spacing: .16em; } .view-head p { margin: 0 0 5px; color: var(--primary-dark); font-size: 11px; font-weight: 700; letter-spacing: .16em; }
.view-head h1 { margin: 0; font-size: 30px; } .view-head h1 { margin: 0; font-size: 30px; }
.view-actions { display: flex; gap: 9px; } .view-actions { display: flex; gap: 9px; }
.import-status { min-height: 20px; margin: -16px 0 12px; color: var(--muted); font-size: 13px; text-align: right; }
.article-list { display: grid; gap: 10px; } .article-list { display: grid; gap: 10px; }
.article-row { padding: 18px 20px; display: flex; align-items: center; justify-content: space-between; gap: 20px; background: white; border: 1px solid var(--border); border-radius: 14px; } .article-row { padding: 18px 20px; display: flex; align-items: center; justify-content: space-between; gap: 20px; background: white; border: 1px solid var(--border); border-radius: 14px; }
.article-summary { min-width: 0; flex: 1; } .article-summary { min-width: 0; flex: 1; }
......
import fs from "node:fs/promises";
import path from "node:path";
import { Document, HeadingLevel, ImageRun, Packer, Paragraph, TextRun } from "docx";
const root = process.cwd();
const outputDirectory = path.join(root, "tests", "fixtures");
const imageFile = "/Users/mac/Desktop/test.png";
const marker = "YINZHUANG-PRODUCTION-IMPORT-20260731";
await fs.mkdir(outputDirectory, { recursive: true });
await fs.writeFile(path.join(outputDirectory, "import-test.md"), [
"# 旧银来料加工前,建议先确认这五件事",
"",
"家里闲置的旧银,可能是一只不再合圈口的手镯,也可能是一件承载着家庭记忆的老首饰。把旧银重新设计、熔炼打制,可以让熟悉的材质延续新的故事。为了让沟通和制作更顺利,送到门店前建议先确认下面五件事。",
"",
"![旧银来料加工沟通示意图]({{TEST_IMAGE_URL}})",
"",
"## 一、先明确想做成什么",
"",
"可以先说明佩戴者、使用场景、喜欢的风格和预算。例如,是把旧银改成日常佩戴的实心手镯,还是制作一对带纪念刻字的戒指。参考图能帮助表达方向,但最终还要结合银料重量、尺寸和工艺可行性判断。",
"",
"## 二、现场确认来料状态与重量",
"",
"门店会先检查旧银的状态、材质标识和重量,再说明哪些部分可以继续使用。熔炼、修整和抛光会产生合理损耗,因此加工前应把称重结果、预计损耗和成品目标重量沟通清楚。",
"",
"## 三、把尺寸测准确",
"",
"手镯需要确认圈口和佩戴松紧,戒指需要测量指围,项链则要确认链长。足银质地较软,结构、克重与尺寸会共同影响稳定性和佩戴体验,不能只按外观决定。",
"",
"## 四、分开了解银料与工费",
"",
"光面、古法、磨砂、浮雕、錾刻、镂空和花丝等工艺所需时间与难度不同。建议在制作前确认来料处理、设计、工艺和附加定制分别如何计费,并核对预计交付时间。",
"",
"## 五、保留好确认信息",
"",
"款式、尺寸、刻字内容、重量、损耗、工费和交付时间都确认后,再进入制作环节。成品交付时,可以再次核对结构、表面状态和后续养护方式。",
"",
"银妆深耕银饰行业 14 年,提供来料私人加工、专属雕刻和现场打制等服务。复杂图案或特殊结构建议提前沟通,让匠人根据银料和用途给出更稳妥的方案。",
"",
`测试标记:${marker}-MARKDOWN`,
"",
].join("\n"));
const image = await fs.readFile(imageFile);
const heading = (text) => new Paragraph({ text, heading: HeadingLevel.HEADING_2 });
const body = (text) => new Paragraph({ children: [new TextRun(text)] });
const document = new Document({
sections: [{
children: [
new Paragraph({ text: "足银手镯日常佩戴与养护指南", heading: HeadingLevel.HEADING_1 }),
body("足银手镯质地温润,也比许多合金首饰更柔软。日常佩戴时兼顾清洁、收纳与受力保护,能让手镯更长久地保持光泽和形态。"),
new Paragraph({ children: [new ImageRun({ data: image, transformation: { width: 320, height: 210 }, type: "png", altText: { title: "银饰养护测试图片", description: "用于验证 Word 导入图片提取", name: "test.png" } })] }),
heading("佩戴前确认圈口与结构"),
body("手掌最宽处、腕围和个人松紧偏好都会影响圈口选择。第一次购买建议到店试戴或在顾问指导下测量。足银手镯的实心、空心、开口、闭口和推拉结构受力方式不同,日常佩戴前应先了解正确的开合与调整方法。"),
heading("减少化学接触和重压"),
body("洗澡、游泳、泡温泉、运动或做家务时,可以暂时取下手镯。香水、发胶和护肤品使用后,待皮肤表面干燥再佩戴。足银质地较软,应避免用力掰折、碰撞和重压;如出现变形,交给专业门店检查调整更稳妥。"),
heading("不佩戴时单独密封"),
body("收纳前先用柔软布料擦去表面汗液,再放入密封袋或首饰盒。不同硬度、不同表面工艺的首饰尽量分开,避免互相摩擦。潮湿环境和长期裸露收纳都可能加快银饰氧化。"),
heading("根据工艺选择清洁方式"),
body("光面素银可用专业擦银布轻拭;做旧、鎏金、珐琅、珍珠或镶嵌款,不建议自行使用强效清洁液,以免破坏原有效果。银饰氧化发黑属于常见现象,并不等于材质有问题。"),
heading("定期使用专业养护"),
body("银妆采用足银 999/9999 国标银料,产品配套质检证书,并为所售银饰提供终身免费清洗、抛光、去氧化及基础整形服务。日常轻柔佩戴、正确收纳,再配合专业养护,能让银饰持续陪伴重要时刻。"),
body(`测试标记:${marker}-WORD`),
],
}],
});
await fs.writeFile(path.join(outputDirectory, "import-test.docx"), await Packer.toBuffer(document));
console.log(`Created fixtures in ${outputDirectory}`);
process.env.PORT ||= process.env.CMS_PORT || "8790"; process.env.PORT ||= process.env.CMS_PORT || "8791";
process.env.HOST ||= "127.0.0.1"; process.env.HOST ||= "127.0.0.1";
await import("./dist/server/entry.mjs"); await import("./dist/server/entry.mjs");
...@@ -489,3 +489,11 @@ export async function saveUpload(dataUrl: unknown): Promise<string> { ...@@ -489,3 +489,11 @@ export async function saveUpload(dataUrl: unknown): Promise<string> {
await writeAtomic(path.join(UPLOADS_DIR, name), buffer); await writeAtomic(path.join(UPLOADS_DIR, name), buffer);
return `/uploads/${name}`; return `/uploads/${name}`;
} }
export async function removeUpload(url: unknown): Promise<void> {
const match = String(url || "").match(/^\/uploads\/([a-zA-Z0-9._-]+)$/);
if (!match) throw new StoreError("图片地址不合法");
await fs.unlink(path.join(UPLOADS_DIR, match[1])).catch((error: NodeJS.ErrnoException) => {
if (error.code !== "ENOENT") throw error;
});
}
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
listWorkingArticles, listWorkingArticles,
publishArticle, publishArticle,
readWorkingArticle, readWorkingArticle,
removeUpload,
removeCategory, removeCategory,
renameCategory, renameCategory,
safeSlug, safeSlug,
...@@ -20,6 +21,7 @@ import { ...@@ -20,6 +21,7 @@ import {
unpublishArticle, unpublishArticle,
writeArticle, writeArticle,
} from "./article-store"; } from "./article-store";
import { convertWordToMarkdown, decodeWordDataUrl } from "./word-import";
import { buildSiteAtomic, tryAcquireSiteBuildLock, withSiteBuildLock } from "../../scripts/site-build.mjs"; import { buildSiteAtomic, tryAcquireSiteBuildLock, withSiteBuildLock } from "../../scripts/site-build.mjs";
const PASSWORD = process.env.CMS_PASSWORD || "admin"; const PASSWORD = process.env.CMS_PASSWORD || "admin";
...@@ -262,6 +264,42 @@ export async function handleCmsApi(request: Request, routeValue: string, clientA ...@@ -262,6 +264,42 @@ export async function handleCmsApi(request: Request, routeValue: string, clientA
} }
} }
if (route === "imports/word" && method === "POST") {
const body = await bodyOf(request);
const fileName = String(body.fileName || "").trim();
const buffer = decodeWordDataUrl(body.dataUrl, fileName);
const uploadedUrls: string[] = [];
let createdSlug: string | null = null;
try {
const converted = await convertWordToMarkdown(buffer, fileName, async (dataUrl) => {
const url = await saveUpload(dataUrl);
uploadedUrls.push(url);
return url;
});
const title = String(body.title || "").trim() || converted.suggestedTitle;
const imported = await withSiteBuildLock(async () => {
const slug = await generateSlug(body.category);
createdSlug = slug;
const article = await writeArticle(slug, { title, category: body.category, body: converted.body }, true);
return { slug, publishStatus: await getPublishStatus(article) };
});
return json({
ok: true,
slug: imported.slug,
title,
publishStatus: imported.publishStatus,
imageCount: converted.imageCount,
warnings: converted.warnings,
}, 201);
} catch (error) {
if (createdSlug) {
await withSiteBuildLock(() => discardDraft(createdSlug as string)).catch(() => {});
}
await Promise.allSettled(uploadedUrls.map((url) => removeUpload(url)));
throw error;
}
}
if (parts[0] === "articles" && parts[1]) { if (parts[0] === "articles" && parts[1]) {
const slug = requireSlug(parts[1]); const slug = requireSlug(parts[1]);
if (parts.length === 3 && parts[2] === "draft" && method === "DELETE") { if (parts.length === 3 && parts[2] === "draft" && method === "DELETE") {
......
import mammoth from "mammoth";
import TurndownService from "turndown";
import { gfm } from "turndown-plugin-gfm";
import { StoreError } from "./article-store";
export const MAX_WORD_FILE_SIZE = 20 * 1024 * 1024;
const DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
const ACCEPTED_DOCX_MIMES = new Set([DOCX_MIME, "application/octet-stream", "application/zip"]);
const IMAGE_MIME_ALIASES: Record<string, string> = {
"image/jpg": "image/jpeg",
"image/x-png": "image/png",
};
const SUPPORTED_IMAGE_MIMES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
export interface WordImportResult {
body: string;
suggestedTitle: string;
imageCount: number;
warnings: string[];
}
export function decodeWordDataUrl(dataUrl: unknown, fileName: unknown): Buffer {
const name = String(fileName || "").trim();
if (/\.doc$/i.test(name)) throw new StoreError("仅支持 .docx 文件,不支持旧版 .doc 格式");
if (!/\.docx$/i.test(name)) throw new StoreError("请选择 .docx 文件");
const match = String(dataUrl || "").match(/^data:([^;,]+);base64,([a-zA-Z0-9+/=\s]+)$/s);
if (!match || !ACCEPTED_DOCX_MIMES.has(match[1].toLowerCase())) {
throw new StoreError("Word 文件格式不正确,请上传 .docx 文件");
}
const buffer = Buffer.from(match[2].replace(/\s/g, ""), "base64");
if (!buffer.length || buffer.subarray(0, 2).toString("ascii") !== "PK") {
throw new StoreError("Word 文件无效或已经损坏");
}
if (buffer.length > MAX_WORD_FILE_SIZE) throw new StoreError("Word 文件不能超过 20MB", 413);
return buffer;
}
function titleFromFileName(fileName: string): string {
return fileName
.replace(/^.*[\\/]/, "")
.replace(/\.docx$/i, "")
.replace(/[_-]+/g, " ")
.trim() || "Word 导入文章";
}
function plainHeading(value: string): string {
return value
.replace(/!\[[^\]]*\]\([^)]*\)/g, "")
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
.replace(/[`*_~]/g, "")
.trim();
}
export async function convertWordToMarkdown(
buffer: Buffer,
fileName: string,
saveImage: (dataUrl: string) => Promise<string>,
): Promise<WordImportResult> {
let imageCount = 0;
let result: Awaited<ReturnType<typeof mammoth.convertToHtml>>;
try {
result = await mammoth.convertToHtml({ buffer }, {
styleMap: ["p[style-name='Title'] => h1:fresh"],
convertImage: mammoth.images.imgElement(async (image) => {
const mime = IMAGE_MIME_ALIASES[image.contentType] || image.contentType;
if (!SUPPORTED_IMAGE_MIMES.has(mime)) {
throw new StoreError(`Word 中包含不支持的图片格式:${image.contentType || "未知格式"}`);
}
const base64 = await image.readAsBase64String();
const src = await saveImage(`data:${mime};base64,${base64}`);
imageCount += 1;
return { src };
}),
});
} catch (error) {
if (error instanceof StoreError) throw error;
throw new StoreError(`Word 文件解析失败:${error instanceof Error ? error.message : "文件无效或已经损坏"}`);
}
const turndown = new TurndownService({
bulletListMarker: "-",
codeBlockStyle: "fenced",
headingStyle: "atx",
});
turndown.use(gfm);
let body = turndown.turndown(result.value).trim();
let suggestedTitle = titleFromFileName(fileName);
const firstHeading = body.match(/^#\s+(.+)$/m);
if (firstHeading && firstHeading.index !== undefined) {
suggestedTitle = plainHeading(firstHeading[1]) || suggestedTitle;
body = `${body.slice(0, firstHeading.index)}${body.slice(firstHeading.index + firstHeading[0].length)}`
.replace(/^\s+|\s+$/g, "")
.replace(/\n{3,}/g, "\n\n");
}
if (!body) throw new StoreError("Word 文件中没有可导入的正文内容");
return {
body,
suggestedTitle,
imageCount,
warnings: result.messages.map((message) => message.message),
};
}
...@@ -35,7 +35,9 @@ ...@@ -35,7 +35,9 @@
</header> </header>
<main id="list-view" class="view"> <main id="list-view" class="view">
<div class="view-head"><div><p>CONTENT</p><h1>银饰资讯</h1></div><div class="view-actions"><button id="manage-categories" class="ghost">管理分类</button><button id="new-article" class="primary">+ 新建文章</button></div></div> <div class="view-head"><div><p>CONTENT</p><h1>银饰资讯</h1></div><div class="view-actions"><button id="manage-categories" class="ghost">管理分类</button><button id="import-article" class="ghost">导入文件</button><button id="new-article" class="primary">+ 新建文章</button></div></div>
<input id="article-file" class="hidden" type="file" accept=".md,.docx,text/markdown,application/vnd.openxmlformats-officedocument.wordprocessingml.document" />
<p id="import-status" class="import-status" aria-live="polite"></p>
<div id="article-list" class="article-list"></div> <div id="article-list" class="article-list"></div>
</main> </main>
......
declare module "turndown-plugin-gfm" {
import type TurndownService from "turndown";
export function gfm(service: TurndownService): void;
}
# 旧银来料加工前,建议先确认这五件事
家里闲置的旧银,可能是一只不再合圈口的手镯,也可能是一件承载着家庭记忆的老首饰。把旧银重新设计、熔炼打制,可以让熟悉的材质延续新的故事。为了让沟通和制作更顺利,送到门店前建议先确认下面五件事。
![旧银来料加工沟通示意图]({{TEST_IMAGE_URL}})
## 一、先明确想做成什么
可以先说明佩戴者、使用场景、喜欢的风格和预算。例如,是把旧银改成日常佩戴的实心手镯,还是制作一对带纪念刻字的戒指。参考图能帮助表达方向,但最终还要结合银料重量、尺寸和工艺可行性判断。
## 二、现场确认来料状态与重量
门店会先检查旧银的状态、材质标识和重量,再说明哪些部分可以继续使用。熔炼、修整和抛光会产生合理损耗,因此加工前应把称重结果、预计损耗和成品目标重量沟通清楚。
## 三、把尺寸测准确
手镯需要确认圈口和佩戴松紧,戒指需要测量指围,项链则要确认链长。足银质地较软,结构、克重与尺寸会共同影响稳定性和佩戴体验,不能只按外观决定。
## 四、分开了解银料与工费
光面、古法、磨砂、浮雕、錾刻、镂空和花丝等工艺所需时间与难度不同。建议在制作前确认来料处理、设计、工艺和附加定制分别如何计费,并核对预计交付时间。
## 五、保留好确认信息
款式、尺寸、刻字内容、重量、损耗、工费和交付时间都确认后,再进入制作环节。成品交付时,可以再次核对结构、表面状态和后续养护方式。
银妆深耕银饰行业 14 年,提供来料私人加工、专属雕刻和现场打制等服务。复杂图案或特殊结构建议提前沟通,让匠人根据银料和用途给出更稳妥的方案。
测试标记:YINZHUANG-PRODUCTION-IMPORT-20260731-MARKDOWN
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test, { after } from "node:test";
import { Document, HeadingLevel, ImageRun, Packer, Paragraph, TextRun } from "docx";
const TEST_IMAGE = "/Users/mac/Desktop/test.png";
const UNIQUE_MARKER = "YINZHUANG-IMPORT-TEST-20260731";
const dataDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "yinzhuang-import-"));
process.env.CMS_DATA_DIR = dataDirectory;
process.env.CMS_BUILD_LOCK = path.join(dataDirectory, "site-build.lock");
process.env.CMS_API_KEY = "word-import-test-key";
const [{ handleCmsApi }, { GET: getUpload }] = await Promise.all([
import("../src/lib/cms-api"),
import("../src/pages/uploads/[file]"),
]);
after(async () => {
await fs.rm(dataDirectory, { recursive: true, force: true });
});
function cmsRequest(route: string, method = "GET", body?: Record<string, unknown>, key = "word-import-test-key"): Request {
return new Request(`http://localhost/api/cms/${route}`, {
method,
headers: {
Authorization: `Bearer ${key}`,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
}
async function wordDocument(): Promise<Buffer> {
const png = await fs.readFile(TEST_IMAGE);
const document = new Document({
sections: [{
children: [
new Paragraph({ text: "Yinzhuang Word Import Test", heading: HeadingLevel.HEADING_1 }),
new Paragraph({ children: [new TextRun(`Imported Word body marker: ${UNIQUE_MARKER}`)] }),
new Paragraph({ children: [new ImageRun({ data: png, transformation: { width: 160, height: 105 }, type: "png" })] }),
],
}],
});
return Packer.toBuffer(document);
}
test("imports a Markdown file payload as a draft", async () => {
const markdown = `# 银妆 Markdown 导入测试\n\n这是 Markdown 上传测试:${UNIQUE_MARKER}`;
const heading = markdown.match(/^#\s+(.+)$/m);
assert.ok(heading?.index !== undefined);
const body = `${markdown.slice(0, heading.index)}${markdown.slice(heading.index + heading[0].length)}`.trim();
const response = await handleCmsApi(cmsRequest("articles", "POST", {
title: heading[1],
category: "佩戴养护",
body,
}), "articles");
assert.equal(response.status, 201);
const imported = await response.json();
assert.equal(imported.publishStatus, "new-draft");
const articleResponse = await handleCmsApi(cmsRequest(`articles/${imported.slug}`), `articles/${imported.slug}`);
const article = await articleResponse.json();
assert.equal(article.title, "银妆 Markdown 导入测试");
assert.match(article.body, new RegExp(UNIQUE_MARKER));
assert.doesNotMatch(article.body, /^#\s+/);
});
test("imports a docx with the requested embedded PNG as a Markdown draft", async () => {
const buffer = await wordDocument();
const response = await handleCmsApi(cmsRequest("imports/word", "POST", {
fileName: "银妆导入测试.docx",
category: "佩戴养护",
dataUrl: `data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,${buffer.toString("base64")}`,
}), "imports/word");
assert.equal(response.status, 201);
const imported = await response.json();
assert.equal(imported.title, "Yinzhuang Word Import Test");
assert.equal(imported.publishStatus, "new-draft");
assert.equal(imported.imageCount, 1);
const articleResponse = await handleCmsApi(cmsRequest(`articles/${imported.slug}`), `articles/${imported.slug}`);
assert.equal(articleResponse.status, 200);
const article = await articleResponse.json();
assert.equal(article.category, "佩戴养护");
assert.match(article.body, new RegExp(UNIQUE_MARKER));
assert.doesNotMatch(article.body, /^#\s+Yinzhuang Word Import Test/m);
const imageUrl = article.body.match(/!\[[^\]]*\]\((\/uploads\/[^)]+)\)/)?.[1];
assert.ok(imageUrl);
const filename = imageUrl.replace(/^\/uploads\//, "");
const imageResponse = await getUpload({ params: { file: filename } } as never);
assert.equal(imageResponse.status, 200);
assert.equal(imageResponse.headers.get("content-type"), "image/png");
assert.deepEqual(Buffer.from(await imageResponse.arrayBuffer()), await fs.readFile(TEST_IMAGE));
});
test("requires valid authentication for Word import", async () => {
const response = await handleCmsApi(cmsRequest("imports/word", "POST", {}, "wrong-key"), "imports/word");
assert.equal(response.status, 401);
});
test("rejects legacy .doc files", async () => {
const response = await handleCmsApi(cmsRequest("imports/word", "POST", {
fileName: "旧文章.doc",
dataUrl: "data:application/octet-stream;base64,AA==",
}), "imports/word");
assert.equal(response.status, 400);
assert.match((await response.json()).error, /不支持旧版 \.doc/);
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment