From 48609b86de00949d191f11ff22bb71f9d52006ec Mon Sep 17 00:00:00 2001 From: rbetree Date: Sun, 4 Jan 2026 20:39:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=86=E7=B1=BB=E9=94=9A=E7=82=B9&?= =?UTF-8?q?=E8=B4=A8=E9=87=8F=E6=A3=80=E6=9F=A5&=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E6=B2=BB=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 分类生成唯一 slug,模板/子菜单/滚动/扩展定位统一使用 data-id - lint 覆盖 src/scripts/test,CI 增量格式检查 - 清理冗余依赖,升级 esbuild,overrides 修复审计项 - 补充单测并更新修复清单 --- .github/workflows/ci.yml | 7 +- package-lock.json | 347 +++++++++--------- package.json | 15 +- scripts/format-check-changed.js | 131 +++++++ scripts/lint.js | 67 ++++ src/generator.js | 55 +++ src/script.js | 145 +++++--- templates/components/category.hbs | 3 +- templates/components/navigation.hbs | 4 +- test/category-slug-dedup.node-test.js | 48 +++ ...on-submenu-uses-category-slug.node-test.js | 43 +++ 11 files changed, 641 insertions(+), 224 deletions(-) create mode 100644 scripts/format-check-changed.js create mode 100644 scripts/lint.js create mode 100644 test/category-slug-dedup.node-test.js create mode 100644 test/navigation-submenu-uses-category-slug.node-test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d34698..0b3a4b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [ main ] + branches: [main] pull_request: jobs: @@ -11,6 +11,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 @@ -21,6 +23,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Format check (changed files) + run: npm run format:check:changed + - name: Lint run: npm run lint diff --git a/package-lock.json b/package-lock.json index 02b2f1d..0013dd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,27 +9,20 @@ "version": "1.3.0", "license": "AGPL-3.0-only", "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", - "has-flag": "^5.0.1", "js-yaml": "^4.1.1", - "mime-db": "^1.52.0", - "rss-parser": "^3.13.0", - "supports-color": "^9.4.0" + "rss-parser": "^3.13.0" }, "devDependencies": { - "esbuild": "^0.20.2", + "esbuild": "^0.27.2", "prettier": "^3.4.2", "serve": "^14.2.5" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -40,13 +33,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -57,13 +50,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -74,13 +67,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -91,13 +84,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -108,13 +101,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -125,13 +118,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -142,13 +135,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -159,13 +152,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -176,13 +169,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -193,13 +186,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -210,13 +203,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -227,13 +220,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -244,13 +237,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -261,13 +254,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -278,13 +271,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -295,13 +288,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -312,13 +305,30 @@ "linux" ], "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": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -329,13 +339,30 @@ "netbsd" ], "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": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -346,13 +373,30 @@ "openbsd" ], "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": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -363,13 +407,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -380,13 +424,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -397,13 +441,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -414,7 +458,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@zeit/schemas": { @@ -500,6 +544,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -512,6 +557,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -585,9 +631,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -738,6 +784,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -750,17 +797,9 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "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": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -879,9 +918,9 @@ } }, "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -889,32 +928,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.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": { @@ -982,18 +1024,6 @@ "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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -1113,6 +1143,7 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -1517,18 +1548,6 @@ "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": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", diff --git a/package.json b/package.json index 133443e..243f5e3 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,10 @@ "sync-projects": "node ./scripts/sync-projects.js", "import-bookmarks": "node src/bookmark-processor.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: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" }, "keywords": [ @@ -29,18 +30,14 @@ "dependencies": { "js-yaml": "^4.1.1", "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" }, "devDependencies": { - "esbuild": "^0.20.2", + "esbuild": "^0.27.2", "prettier": "^3.4.2", "serve": "^14.2.5" + }, + "overrides": { + "brace-expansion": "1.1.12" } } diff --git a/scripts/format-check-changed.js b/scripts/format-check-changed.js new file mode 100644 index 0000000..b07ac91 --- /dev/null +++ b/scripts/format-check-changed.js @@ -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(); diff --git a/scripts/lint.js b/scripts/lint.js new file mode 100644 index 0000000..f2322c6 --- /dev/null +++ b/scripts/lint.js @@ -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(); diff --git a/src/generator.js b/src/generator.js index 55bbe69..341d854 100644 --- a/src/generator.js +++ b/src/generator.js @@ -413,6 +413,46 @@ function getSubmenuForNavItem(navItem, config) { 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 字符串安全嵌入到 ` 结束标签导致脚本块被提前终止。 * 说明:返回值仍是合法 JSON,JSON.parse 后数据不变。 @@ -825,6 +865,16 @@ function prepareRenderData(config) { // 首页(默认页)规则:navigation 顺序第一项即首页 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 等处理结果) renderData.configJSON = makeJsonSafeForHtmlScript( JSON.stringify({ @@ -1382,6 +1432,11 @@ function renderPage(pageId, config) { 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) { console.log(`页面 ${pageId} 使用指定模板: ${templateName}`); } diff --git a/src/script.js b/src/script.js index 745c3c2..8183753 100644 --- a/src/script.js +++ b/src/script.js @@ -153,30 +153,42 @@ window.MeNav = { return menavConfigCacheValue; }, - // 获取元素的唯一标识符 - _getElementId: function(element) { - const type = element.getAttribute('data-type'); - if (type === 'nav-item') { - return element.getAttribute('data-id'); - } else if (type === 'social-link') { - return element.getAttribute('data-url'); - } else { - return element.getAttribute('data-name'); - } - }, + // 获取元素的唯一标识符 + _getElementId: function(element) { + const type = element.getAttribute('data-type'); + if (type === 'nav-item') { + return element.getAttribute('data-id'); + } else if (type === 'social-link') { + return element.getAttribute('data-url'); + } else { + // 优先使用 data-id(例如分类 slug),回退 data-name(兼容旧扩展/旧页面) + return element.getAttribute('data-id') || element.getAttribute('data-name'); + } + }, - // 根据类型和ID查找元素 - _findElement: function(type, id) { - let selector; - if (type === 'nav-item') { - selector = `[data-type="${type}"][data-id="${id}"]`; - } else if (type === 'social-link') { - selector = `[data-type="${type}"][data-url="${id}"]`; - } else { - selector = `[data-type="${type}"][data-name="${id}"]`; - } - return document.querySelector(selector); - }, + // 根据类型和ID查找元素 + _findElement: function(type, id) { + let selector; + if (type === 'nav-item') { + selector = `[data-type="${type}"][data-id="${id}"]`; + } else if (type === 'social-link') { + 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 { + // 其他:优先 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); + }, // 更新DOM元素 updateElement: function(type, id, newData) { @@ -1929,19 +1941,33 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - // 子菜单项点击效果 - submenuItems.forEach(item => { - item.addEventListener('click', (e) => { - e.preventDefault(); + // 子菜单项点击效果 + submenuItems.forEach(item => { + item.addEventListener('click', (e) => { + e.preventDefault(); - // 获取页面ID和分类名称 - const pageId = item.getAttribute('data-page'); - const categoryName = item.getAttribute('data-category'); + // 获取页面ID和分类名称 + const pageId = item.getAttribute('data-page'); + const categoryName = item.getAttribute('data-category'); + const categoryId = item.getAttribute('data-category-id'); - if (pageId) { - // 清除所有子菜单项的激活状态 - submenuItems.forEach(subItem => { - subItem.classList.remove('active'); + 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) { + // 清除所有子菜单项的激活状态 + submenuItems.forEach(subItem => { + subItem.classList.remove('active'); }); // 激活当前子菜单项 @@ -1955,20 +1981,45 @@ document.addEventListener('DOMContentLoaded', () => { // 显示对应页面 showPage(pageId); - // 等待页面切换完成后滚动到对应分类 - setTimeout(() => { - // 查找目标分类元素 - const targetPage = document.getElementById(pageId); - if (targetPage) { - const targetCategory = Array.from(targetPage.querySelectorAll('.category h2')).find( - heading => heading.textContent.trim().includes(categoryName) - ); + // 等待页面切换完成后滚动到对应分类 + setTimeout(() => { + // 查找目标分类元素 + const targetPage = document.getElementById(pageId); + if (targetPage) { + let targetCategory = null; - if (targetCategory) { - // 优化的滚动实现:滚动到使目标分类位于视口1/4处(更靠近顶部位置) - try { - // 直接获取所需元素和属性,减少重复查询 - const contentElement = document.querySelector('.content'); + // 优先使用 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) + ); + } + + if (targetCategory) { + // 由于对子菜单 click 做了 preventDefault,这里手动同步 hash(不触发浏览器默认跳转) + const nextHash = categoryId || categoryName; + if (nextHash) { + try { + history.replaceState(null, '', `#${nextHash}`); + } catch (error) { + // 忽略 history API 失败,避免影响滚动体验 + } + } + + // 优化的滚动实现:滚动到使目标分类位于视口1/4处(更靠近顶部位置) + try { + // 直接获取所需元素和属性,减少重复查询 + const contentElement = document.querySelector('.content'); if (contentElement && contentElement.scrollHeight > contentElement.clientHeight) { // 获取目标元素相对于内容区域的位置 diff --git a/templates/components/category.hbs b/templates/components/category.hbs index 044dbd6..a7aa9d8 100644 --- a/templates/components/category.hbs +++ b/templates/components/category.hbs @@ -1,6 +1,7 @@
{{#each submenu}} - + {{name}} @@ -20,4 +20,4 @@ {{/if}} {{/each}} - \ No newline at end of file + diff --git a/test/category-slug-dedup.node-test.js b/test/category-slug-dedup.node-test.js new file mode 100644 index 0000000..be06421 --- /dev/null +++ b/test/category-slug-dedup.node-test.js @@ -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"')); + }); +}); diff --git a/test/navigation-submenu-uses-category-slug.node-test.js b/test/navigation-submenu-uses-category-slug.node-test.js new file mode 100644 index 0000000..713098d --- /dev/null +++ b/test/navigation-submenu-uses-category-slug.node-test.js @@ -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:子菜单锚点应使用分类 slug(href + 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'); + }); +});