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}}
-
@@ -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');
+ });
+});