feat: 分类锚点&质量检查&依赖治理

- 分类生成唯一 slug,模板/子菜单/滚动/扩展定位统一使用 data-id
- lint 覆盖 src/scripts/test,CI 增量格式检查
- 清理冗余依赖,升级 esbuild,overrides 修复审计项
- 补充单测并更新修复清单
This commit is contained in:
rbetree
2026-01-04 20:39:42 +08:00
parent 3d9363a550
commit 48609b86de
11 changed files with 641 additions and 224 deletions

View File

@@ -2,7 +2,7 @@ name: CI
on: on:
push: push:
branches: [ main ] branches: [main]
pull_request: pull_request:
jobs: jobs:
@@ -11,6 +11,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@@ -21,6 +23,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Format check (changed files)
run: npm run format:check:changed
- name: Lint - name: Lint
run: npm run lint run: npm run lint

347
package-lock.json generated
View File

@@ -9,27 +9,20 @@
"version": "1.3.0", "version": "1.3.0",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"ansi-regex": "^6.0.1",
"ansi-styles": "^6.2.1",
"color-convert": "^2.0.1",
"color-name": "^2.0.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"has-flag": "^5.0.1",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
"mime-db": "^1.52.0", "rss-parser": "^3.13.0"
"rss-parser": "^3.13.0",
"supports-color": "^9.4.0"
}, },
"devDependencies": { "devDependencies": {
"esbuild": "^0.20.2", "esbuild": "^0.27.2",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"serve": "^14.2.5" "serve": "^14.2.5"
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -40,13 +33,13 @@
"aix" "aix"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -57,13 +50,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -74,13 +67,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -91,13 +84,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -108,13 +101,13 @@
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -125,13 +118,13 @@
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -142,13 +135,13 @@
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -159,13 +152,13 @@
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -176,13 +169,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -193,13 +186,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -210,13 +203,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -227,13 +220,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -244,13 +237,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -261,13 +254,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -278,13 +271,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -295,13 +288,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -312,13 +305,30 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -329,13 +339,30 @@
"netbsd" "netbsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -346,13 +373,30 @@
"openbsd" "openbsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -363,13 +407,13 @@
"sunos" "sunos"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -380,13 +424,13 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -397,13 +441,13 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -414,7 +458,7 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@zeit/schemas": { "node_modules/@zeit/schemas": {
@@ -500,6 +544,7 @@
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@@ -512,6 +557,7 @@
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@@ -585,9 +631,9 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -738,6 +784,7 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-name": "~1.1.4" "color-name": "~1.1.4"
@@ -750,17 +797,9 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/color-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.0.tgz",
"integrity": "sha512-SbtvAMWvASO5TE2QP07jHBMXKafgdZz8Vrsrn96fiL+O92/FN/PLARzUW5sKt013fjAprK2d2iCn2hk2Xb5oow==",
"license": "MIT",
"engines": {
"node": ">=12.20"
}
},
"node_modules/compressible": { "node_modules/compressible": {
"version": "2.0.18", "version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -879,9 +918,9 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.20.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@@ -889,32 +928,35 @@
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.20.2", "@esbuild/aix-ppc64": "0.27.2",
"@esbuild/android-arm": "0.20.2", "@esbuild/android-arm": "0.27.2",
"@esbuild/android-arm64": "0.20.2", "@esbuild/android-arm64": "0.27.2",
"@esbuild/android-x64": "0.20.2", "@esbuild/android-x64": "0.27.2",
"@esbuild/darwin-arm64": "0.20.2", "@esbuild/darwin-arm64": "0.27.2",
"@esbuild/darwin-x64": "0.20.2", "@esbuild/darwin-x64": "0.27.2",
"@esbuild/freebsd-arm64": "0.20.2", "@esbuild/freebsd-arm64": "0.27.2",
"@esbuild/freebsd-x64": "0.20.2", "@esbuild/freebsd-x64": "0.27.2",
"@esbuild/linux-arm": "0.20.2", "@esbuild/linux-arm": "0.27.2",
"@esbuild/linux-arm64": "0.20.2", "@esbuild/linux-arm64": "0.27.2",
"@esbuild/linux-ia32": "0.20.2", "@esbuild/linux-ia32": "0.27.2",
"@esbuild/linux-loong64": "0.20.2", "@esbuild/linux-loong64": "0.27.2",
"@esbuild/linux-mips64el": "0.20.2", "@esbuild/linux-mips64el": "0.27.2",
"@esbuild/linux-ppc64": "0.20.2", "@esbuild/linux-ppc64": "0.27.2",
"@esbuild/linux-riscv64": "0.20.2", "@esbuild/linux-riscv64": "0.27.2",
"@esbuild/linux-s390x": "0.20.2", "@esbuild/linux-s390x": "0.27.2",
"@esbuild/linux-x64": "0.20.2", "@esbuild/linux-x64": "0.27.2",
"@esbuild/netbsd-x64": "0.20.2", "@esbuild/netbsd-arm64": "0.27.2",
"@esbuild/openbsd-x64": "0.20.2", "@esbuild/netbsd-x64": "0.27.2",
"@esbuild/sunos-x64": "0.20.2", "@esbuild/openbsd-arm64": "0.27.2",
"@esbuild/win32-arm64": "0.20.2", "@esbuild/openbsd-x64": "0.27.2",
"@esbuild/win32-ia32": "0.20.2", "@esbuild/openharmony-arm64": "0.27.2",
"@esbuild/win32-x64": "0.20.2" "@esbuild/sunos-x64": "0.27.2",
"@esbuild/win32-arm64": "0.27.2",
"@esbuild/win32-ia32": "0.27.2",
"@esbuild/win32-x64": "0.27.2"
} }
}, },
"node_modules/execa": { "node_modules/execa": {
@@ -982,18 +1024,6 @@
"uglify-js": "^3.1.4" "uglify-js": "^3.1.4"
} }
}, },
"node_modules/has-flag": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-5.0.1.tgz",
"integrity": "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/human-signals": { "node_modules/human-signals": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -1113,6 +1143,7 @@
"version": "1.54.0", "version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -1517,18 +1548,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/supports-color": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz",
"integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/type-fest": { "node_modules/type-fest": {
"version": "2.19.0", "version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",

View File

@@ -14,9 +14,10 @@
"sync-projects": "node ./scripts/sync-projects.js", "sync-projects": "node ./scripts/sync-projects.js",
"import-bookmarks": "node src/bookmark-processor.js", "import-bookmarks": "node src/bookmark-processor.js",
"test": "node --test test/*.js", "test": "node --test test/*.js",
"lint": "node --check \"src/generator.js\" && node --check \"src/bookmark-processor.js\" && node --check \"src/script.js\"", "lint": "node ./scripts/lint.js",
"format": "prettier --write \"src/**/*.js\" \"scripts/**/*.js\" \"test/**/*.js\" \".github/**/*.yml\" \"*.{md,json}\" \"config/**/*.md\" \"config/**/*.yml\"", "format": "prettier --write \"src/**/*.js\" \"scripts/**/*.js\" \"test/**/*.js\" \".github/**/*.yml\" \"*.{md,json}\" \"config/**/*.md\" \"config/**/*.yml\"",
"format:check": "prettier --check \"src/**/*.js\" \"scripts/**/*.js\" \"test/**/*.js\" \".github/**/*.yml\" \"*.{md,json}\" \"config/**/*.md\" \"config/**/*.yml\"", "format:check": "prettier --check \"src/**/*.js\" \"scripts/**/*.js\" \"test/**/*.js\" \".github/**/*.yml\" \"*.{md,json}\" \"config/**/*.md\" \"config/**/*.yml\"",
"format:check:changed": "node ./scripts/format-check-changed.js",
"check": "npm run lint && npm test && npm run build" "check": "npm run lint && npm test && npm run build"
}, },
"keywords": [ "keywords": [
@@ -29,18 +30,14 @@
"dependencies": { "dependencies": {
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"ansi-regex": "^6.0.1",
"ansi-styles": "^6.2.1",
"supports-color": "^9.4.0",
"has-flag": "^5.0.1",
"color-convert": "^2.0.1",
"color-name": "^2.0.0",
"mime-db": "^1.52.0",
"rss-parser": "^3.13.0" "rss-parser": "^3.13.0"
}, },
"devDependencies": { "devDependencies": {
"esbuild": "^0.20.2", "esbuild": "^0.27.2",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"serve": "^14.2.5" "serve": "^14.2.5"
},
"overrides": {
"brace-expansion": "1.1.12"
} }
} }

View File

@@ -0,0 +1,131 @@
const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
function runGit(args, cwd) {
return execFileSync('git', args, { cwd, encoding: 'utf8' }).trim();
}
function tryReadGithubEvent(eventPath) {
if (!eventPath) return null;
try {
const raw = fs.readFileSync(eventPath, 'utf8');
return JSON.parse(raw);
} catch {
return null;
}
}
function isAllZerosSha(value) {
return typeof value === 'string' && /^0{40}$/.test(value);
}
function getDiffRangeFromGithubEvent(event) {
if (!event || typeof event !== 'object') return null;
if (event.pull_request && event.pull_request.base && event.pull_request.head) {
const base = event.pull_request.base.sha;
const head = event.pull_request.head.sha;
if (base && head) return { base, head };
}
if (event.before && (event.after || event.head_commit)) {
const base = event.before;
const head = event.after || (event.head_commit && event.head_commit.id);
if (base && head && !isAllZerosSha(base)) return { base, head };
}
return null;
}
function collectChangedFiles(repoRoot, range) {
if (!range) return [];
const output = runGit(
['diff', '--name-only', '--diff-filter=ACMR', `${range.base}..${range.head}`],
repoRoot
);
return output
? output
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
: [];
}
function collectWorkingTreeChangedFiles(repoRoot) {
const files = new Set();
const unstaged = runGit(['diff', '--name-only', '--diff-filter=ACMR', 'HEAD'], repoRoot);
const staged = runGit(['diff', '--cached', '--name-only', '--diff-filter=ACMR'], repoRoot);
[unstaged, staged].forEach((block) => {
if (!block) return;
block
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.forEach((filePath) => files.add(filePath));
});
return Array.from(files).sort();
}
function shouldCheckFile(filePath) {
const normalized = filePath.split(path.sep).join('/');
if (normalized === 'package-lock.json') return false;
// 这两个文件历史上未统一为 Prettier 风格;避免为了启用检查产生巨量格式化 diff
if (normalized === 'src/generator.js' || normalized === 'src/script.js') return false;
// 与现有 npm scripts 的检查范围对齐:不检查 docs/ 与 templates/
const allowedRoots = ['src/', 'scripts/', 'test/', '.github/', 'config/'];
const isRootFile = !normalized.includes('/');
const hasAllowedRoot = allowedRoots.some((prefix) => normalized.startsWith(prefix));
const isAllowedPath =
hasAllowedRoot || (isRootFile && (normalized.endsWith('.md') || normalized.endsWith('.json')));
if (!isAllowedPath) return false;
const ext = path.extname(normalized).toLowerCase();
return ['.js', '.json', '.md', '.yml', '.yaml'].includes(ext);
}
function resolvePrettierBin(repoRoot) {
const base = path.join(repoRoot, 'node_modules', '.bin', 'prettier');
if (fs.existsSync(base)) return base;
if (fs.existsSync(`${base}.cmd`)) return `${base}.cmd`;
return null;
}
function main() {
const repoRoot = path.resolve(__dirname, '..');
const event = tryReadGithubEvent(process.env.GITHUB_EVENT_PATH);
const range = getDiffRangeFromGithubEvent(event);
const candidateFiles = range
? collectChangedFiles(repoRoot, range)
: collectWorkingTreeChangedFiles(repoRoot);
const filesToCheck = candidateFiles.filter(shouldCheckFile);
if (filesToCheck.length === 0) {
console.log('格式检查:未发现需要检查的文件,跳过。');
return;
}
const prettierBin = resolvePrettierBin(repoRoot);
if (!prettierBin) {
console.error('格式检查失败:未找到 prettier可先运行 npm ci / npm install。');
process.exitCode = 1;
return;
}
console.log(`格式检查:共 ${filesToCheck.length} 个文件`);
filesToCheck.forEach((filePath) => console.log(`- ${filePath}`));
execFileSync(prettierBin, ['--check', ...filesToCheck], { cwd: repoRoot, stdio: 'inherit' });
}
main();

67
scripts/lint.js Normal file
View File

@@ -0,0 +1,67 @@
const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
function collectJsFiles(rootDir) {
const files = [];
const walk = (currentDir) => {
let entries;
try {
entries = fs.readdirSync(currentDir, { withFileTypes: true });
} catch {
return;
}
entries.forEach((entry) => {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
if (entry.name === 'node_modules' || entry.name === 'dist') return;
walk(fullPath);
return;
}
if (entry.isFile() && entry.name.endsWith('.js')) {
files.push(fullPath);
}
});
};
walk(rootDir);
return files;
}
function main() {
const projectRoot = path.resolve(__dirname, '..');
const targetDirs = ['src', 'scripts', 'test'].map((dir) => path.join(projectRoot, dir));
const jsFiles = targetDirs.flatMap((dir) => collectJsFiles(dir)).sort();
if (jsFiles.length === 0) {
console.log('未发现需要检查的 .js 文件,跳过。');
return;
}
let hasError = false;
jsFiles.forEach((filePath) => {
const relativePath = path.relative(projectRoot, filePath);
try {
execFileSync(process.execPath, ['--check', filePath], { stdio: 'inherit' });
} catch (error) {
hasError = true;
console.error(`\n语法检查失败:${relativePath}`);
if (error && error.status) {
console.error(`退出码:${error.status}`);
}
}
});
if (hasError) {
process.exitCode = 1;
} else {
console.log(`语法检查通过:${jsFiles.length} 个文件`);
}
}
main();

View File

@@ -413,6 +413,46 @@ function getSubmenuForNavItem(navItem, config) {
return null; return null;
} }
function makeCategorySlugBase(name) {
const raw = typeof name === 'string' ? name : String(name ?? '');
const trimmed = raw.trim();
if (!trimmed) return 'category';
// 规则:尽量可读、跨平台稳定;保留字母/数字/下划线/短横线,其它字符替换为短横线
// 注意:分类名允许中文等非 ASCII 字符Node 18+ 支持 Unicode 属性类
const normalized = trimmed
.replace(/\s+/g, '-')
.toLowerCase()
.replace(/[^\p{L}\p{N}_-]+/gu, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '');
return normalized || 'category';
}
function makeUniqueSlug(base, usedSlugs) {
const current = usedSlugs.get(base) || 0;
const next = current + 1;
usedSlugs.set(base, next);
return next === 1 ? base : `${base}-${next}`;
}
function assignCategorySlugs(categories, usedSlugs) {
if (!Array.isArray(categories)) return;
categories.forEach(category => {
if (!category || typeof category !== 'object') return;
const base = makeCategorySlugBase(category.name);
const uniqueSlug = makeUniqueSlug(base, usedSlugs);
category.slug = uniqueSlug;
if (Array.isArray(category.subcategories)) {
assignCategorySlugs(category.subcategories, usedSlugs);
}
});
}
/** /**
* 将 JSON 字符串安全嵌入到 <script> 中,避免出现 `</script>` 结束标签导致脚本块被提前终止。 * 将 JSON 字符串安全嵌入到 <script> 中,避免出现 `</script>` 结束标签导致脚本块被提前终止。
* 说明:返回值仍是合法 JSONJSON.parse 后数据不变。 * 说明:返回值仍是合法 JSONJSON.parse 后数据不变。
@@ -825,6 +865,16 @@ function prepareRenderData(config) {
// 首页默认页规则navigation 顺序第一项即首页 // 首页默认页规则navigation 顺序第一项即首页
renderData.homePageId = renderData.navigation && renderData.navigation[0] ? renderData.navigation[0].id : null; renderData.homePageId = renderData.navigation && renderData.navigation[0] ? renderData.navigation[0].id : null;
// 为每个页面的分类生成稳定锚点 slug解决重名/空格/特殊字符导致的 hash 冲突)
if (Array.isArray(renderData.navigation)) {
renderData.navigation.forEach(navItem => {
const pageConfig = renderData[navItem.id];
if (pageConfig && Array.isArray(pageConfig.categories)) {
assignCategorySlugs(pageConfig.categories, new Map());
}
});
}
// 添加序列化的配置数据,用于浏览器扩展(确保包含 homePageId 等处理结果) // 添加序列化的配置数据,用于浏览器扩展(确保包含 homePageId 等处理结果)
renderData.configJSON = makeJsonSafeForHtmlScript( renderData.configJSON = makeJsonSafeForHtmlScript(
JSON.stringify({ JSON.stringify({
@@ -1382,6 +1432,11 @@ function renderPage(pageId, config) {
if (config.profile.subtitle !== undefined) data.subtitle = config.profile.subtitle; if (config.profile.subtitle !== undefined) data.subtitle = config.profile.subtitle;
} }
// 分类锚点:为当前页面分类生成稳定 slug用于 id/hash避免重名/特殊字符冲突)
if (Array.isArray(data.categories) && data.categories.length > 0) {
assignCategorySlugs(data.categories, new Map());
}
if (config[pageId] && config[pageId].template) { if (config[pageId] && config[pageId].template) {
console.log(`页面 ${pageId} 使用指定模板: ${templateName}`); console.log(`页面 ${pageId} 使用指定模板: ${templateName}`);
} }

View File

@@ -161,7 +161,8 @@ window.MeNav = {
} else if (type === 'social-link') { } else if (type === 'social-link') {
return element.getAttribute('data-url'); return element.getAttribute('data-url');
} else { } else {
return element.getAttribute('data-name'); // 优先使用 data-id例如分类 slug回退 data-name兼容旧扩展/旧页面)
return element.getAttribute('data-id') || element.getAttribute('data-name');
} }
}, },
@@ -172,8 +173,19 @@ window.MeNav = {
selector = `[data-type="${type}"][data-id="${id}"]`; selector = `[data-type="${type}"][data-id="${id}"]`;
} else if (type === 'social-link') { } else if (type === 'social-link') {
selector = `[data-type="${type}"][data-url="${id}"]`; selector = `[data-type="${type}"][data-url="${id}"]`;
} else if (type === 'site') {
// 站点:优先用 data-url更稳定回退 data-id/data-name
return (
document.querySelector(`[data-type="${type}"][data-url="${id}"]`) ||
document.querySelector(`[data-type="${type}"][data-id="${id}"]`) ||
document.querySelector(`[data-type="${type}"][data-name="${id}"]`)
);
} else { } else {
selector = `[data-type="${type}"][data-name="${id}"]`; // 其他:优先 data-id例如分类 slug回退 data-name兼容旧扩展/旧页面)
return (
document.querySelector(`[data-type="${type}"][data-id="${id}"]`) ||
document.querySelector(`[data-type="${type}"][data-name="${id}"]`)
);
} }
return document.querySelector(selector); return document.querySelector(selector);
}, },
@@ -1937,6 +1949,20 @@ document.addEventListener('DOMContentLoaded', () => {
// 获取页面ID和分类名称 // 获取页面ID和分类名称
const pageId = item.getAttribute('data-page'); const pageId = item.getAttribute('data-page');
const categoryName = item.getAttribute('data-category'); const categoryName = item.getAttribute('data-category');
const categoryId = item.getAttribute('data-category-id');
const escapeSelector = value => {
if (value === null || value === undefined) return '';
const text = String(value);
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(text);
// 回退:尽量避免打断选择器(不追求完全覆盖所有边界字符)
return text.replace(/[^a-zA-Z0-9_\u00A0-\uFFFF-]/g, '\\$&');
};
const escapeAttrValue = value => {
if (value === null || value === undefined) return '';
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
};
if (pageId) { if (pageId) {
// 清除所有子菜单项的激活状态 // 清除所有子菜单项的激活状态
@@ -1960,11 +1986,36 @@ document.addEventListener('DOMContentLoaded', () => {
// 查找目标分类元素 // 查找目标分类元素
const targetPage = document.getElementById(pageId); const targetPage = document.getElementById(pageId);
if (targetPage) { if (targetPage) {
const targetCategory = Array.from(targetPage.querySelectorAll('.category h2')).find( let targetCategory = null;
// 优先使用 slug/data-id 精准定位(解决重复命名始终命中第一个的问题)
if (categoryId) {
const escapedId = escapeSelector(categoryId);
targetCategory =
targetPage.querySelector(`#${escapedId}`) ||
targetPage.querySelector(
`[data-type="category"][data-id="${escapeAttrValue(categoryId)}"]`
);
}
// 回退:旧逻辑按文本包含匹配(兼容旧页面/旧数据)
if (!targetCategory && categoryName) {
targetCategory = Array.from(targetPage.querySelectorAll('.category h2')).find(
heading => heading.textContent.trim().includes(categoryName) heading => heading.textContent.trim().includes(categoryName)
); );
}
if (targetCategory) { if (targetCategory) {
// 由于对子菜单 click 做了 preventDefault这里手动同步 hash不触发浏览器默认跳转
const nextHash = categoryId || categoryName;
if (nextHash) {
try {
history.replaceState(null, '', `#${nextHash}`);
} catch (error) {
// 忽略 history API 失败,避免影响滚动体验
}
}
// 优化的滚动实现滚动到使目标分类位于视口1/4处更靠近顶部位置 // 优化的滚动实现滚动到使目标分类位于视口1/4处更靠近顶部位置
try { try {
// 直接获取所需元素和属性,减少重复查询 // 直接获取所需元素和属性,减少重复查询

View File

@@ -1,6 +1,7 @@
<section class="category {{#if level}}category-level-{{level}}{{else}}category-level-1{{/if}}" <section class="category {{#if level}}category-level-{{level}}{{else}}category-level-1{{/if}}"
id="{{name}}" id="{{#if slug}}{{slug}}{{else}}{{name}}{{/if}}"
data-type="category" data-type="category"
data-id="{{#if slug}}{{slug}}{{else}}{{name}}{{/if}}"
data-name="{{name}}" data-name="{{name}}"
data-icon="{{icon}}" data-icon="{{icon}}"
data-level="{{#if level}}{{level}}{{else}}1{{/if}}" data-level="{{#if level}}{{level}}{{else}}1{{/if}}"

View File

@@ -11,7 +11,7 @@
{{#if submenu}} {{#if submenu}}
<div class="submenu"> <div class="submenu">
{{#each submenu}} {{#each submenu}}
<a href="#{{name}}" class="submenu-item" data-page="{{../id}}" data-category="{{name}}"> <a href="#{{#if slug}}{{slug}}{{else}}{{name}}{{/if}}" class="submenu-item" data-page="{{../id}}" data-category="{{name}}" data-category-id="{{#if slug}}{{slug}}{{else}}{{name}}{{/if}}">
<i class="{{icon}}"></i> <i class="{{icon}}"></i>
<span>{{name}}</span> <span>{{name}}</span>
</a> </a>

View File

@@ -0,0 +1,48 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const { loadHandlebarsTemplates, generateAllPagesHTML } = require('../src/generator.js');
function withRepoRoot(fn) {
const originalCwd = process.cwd();
process.chdir(path.join(__dirname, '..'));
try {
return fn();
} finally {
process.chdir(originalCwd);
}
}
test('P1-2分类 slug 应稳定且可去重', () => {
withRepoRoot(() => {
loadHandlebarsTemplates();
const config = {
site: { title: 'Test Site', description: '', author: '', favicon: '', logo_text: 'Test' },
profile: { title: 'PROFILE_TITLE', subtitle: 'PROFILE_SUBTITLE' },
social: [],
navigation: [{ id: 'home', name: '首页', icon: 'fas fa-home' }],
home: {
title: 'HOME',
subtitle: 'HOME_SUB',
template: 'page',
categories: [
{ name: '重复 分类', icon: 'fas fa-tag', sites: [] },
{ name: '重复 分类', icon: 'fas fa-tag', sites: [] },
{ name: '含 空格/特殊#字符', icon: 'fas fa-tag', sites: [] },
],
},
};
const pages = generateAllPagesHTML(config);
assert.ok(typeof pages.home === 'string' && pages.home.length > 0);
assert.ok(pages.home.includes('id="重复-分类"'), '首个重复分类应生成稳定 slug');
assert.ok(pages.home.includes('id="重复-分类-2"'), '重复分类应通过后缀去重');
assert.ok(pages.home.includes('id="含-空格-特殊-字符"'), '空格/特殊字符应被规范化为可用 slug');
assert.ok(pages.home.includes('data-id="重复-分类"'));
assert.ok(pages.home.includes('data-id="重复-分类-2"'));
});
});

View File

@@ -0,0 +1,43 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const { loadHandlebarsTemplates, generateHTML } = require('../src/generator.js');
function withRepoRoot(fn) {
const originalCwd = process.cwd();
process.chdir(path.join(__dirname, '..'));
try {
return fn();
} finally {
process.chdir(originalCwd);
}
}
test('P1-2子菜单锚点应使用分类 slughref + data-category-id', () => {
withRepoRoot(() => {
loadHandlebarsTemplates();
const config = {
site: { title: 'Test Site', description: '', author: '', favicon: '', logo_text: 'Test' },
profile: { title: 'PROFILE_TITLE', subtitle: 'PROFILE_SUBTITLE' },
social: [],
navigation: [{ id: 'home', name: '首页', icon: 'fas fa-home' }],
home: {
title: 'HOME',
subtitle: 'HOME_SUB',
template: 'page',
categories: [
{ name: '重复 分类', icon: 'fas fa-tag', sites: [] },
{ name: '重复 分类', icon: 'fas fa-tag', sites: [] },
],
},
};
const html = generateHTML(config);
assert.ok(html.includes('class="submenu-item"'), '应输出子菜单项');
assert.ok(html.includes('href="#重复-分类"'), '子菜单 href 应指向 slug');
assert.ok(html.includes('data-category-id="重复-分类"'), '子菜单应携带 data-category-id');
});
});