From 8c86243d3263ca084c7a03fc16fdccb42e2624d4 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Tue, 19 May 2026 22:36:56 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E5=AE=8C=E5=96=84=E8=A7=84?= =?UTF-8?q?=E5=88=99=E5=BC=95=E6=93=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + pnpm-lock.yaml | 525 ++----------------------------- src/app/api/books/image/route.ts | 42 ++- src/lib/book.types.ts | 22 ++ src/lib/legado.client.ts | 448 +++++++++++++++++++++++--- 5 files changed, 487 insertions(+), 552 deletions(-) diff --git a/package.json b/package.json index bdead72..3518e0b 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@upstash/redis": "^1.25.0", "@vercel/postgres": "^0.10.0", "@vidstack/react": "^1.12.13", + "@xmldom/xmldom": "^0.9.10", "anime4k-webgpu": "^1.0.0", "artplayer": "^5.3.0", "artplayer-plugin-danmuku": "^5.2.0", @@ -75,6 +76,7 @@ "tailwind-merge": "^2.6.0", "vidstack": "^0.6.15", "xml2js": "^0.6.2", + "xpath": "^0.0.34", "zod": "^3.24.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43824d2..9c1f076 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@vidstack/react': specifier: ^1.12.13 version: 1.12.13(@types/react@18.3.27)(react@18.3.1) + '@xmldom/xmldom': + specifier: ^0.9.10 + version: 0.9.10 anime4k-webgpu: specifier: ^1.0.0 version: 1.0.0(@webgpu/types@0.1.68) @@ -158,6 +161,9 @@ importers: xml2js: specifier: ^0.6.2 version: 0.6.2 + xpath: + specifier: ^0.0.34 + version: 0.0.34 zod: specifier: ^3.24.1 version: 3.25.76 @@ -3141,9 +3147,6 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/mdast@3.0.15': - resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -3549,6 +3552,10 @@ packages: '@webgpu/types@0.1.68': resolution: {integrity: sha512-3ab1B59Ojb6RwjOspYLsTpCzbNB3ZaamIAxBMmvnNkiDoLTZUOBXZ9p5nAYVEkQlDdf6qAZWi1pqj9+ypiqznA==} + '@xmldom/xmldom@0.9.10': + resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + engines: {node: '>=14.6'} + '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -4437,10 +4444,6 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} - diff@5.2.0: - resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} - engines: {node: '>=0.3.1'} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -4658,10 +4661,6 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} engines: {node: '>=6.0'} @@ -5935,9 +5934,6 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - markdown-table@3.0.4: - resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -5949,33 +5945,9 @@ packages: md5@2.3.0: resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} - mdast-util-find-and-replace@2.2.2: - resolution: {integrity: sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==} - - mdast-util-from-markdown@1.3.1: - resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} - mdast-util-from-markdown@2.0.2: resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} - mdast-util-gfm-autolink-literal@1.0.3: - resolution: {integrity: sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==} - - mdast-util-gfm-footnote@1.0.2: - resolution: {integrity: sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==} - - mdast-util-gfm-strikethrough@1.0.3: - resolution: {integrity: sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==} - - mdast-util-gfm-table@1.0.7: - resolution: {integrity: sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==} - - mdast-util-gfm-task-list-item@1.0.2: - resolution: {integrity: sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==} - - mdast-util-gfm@2.0.2: - resolution: {integrity: sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==} - mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -5985,24 +5957,15 @@ packages: mdast-util-mdxjs-esm@2.0.1: resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} - mdast-util-phrasing@3.0.1: - resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==} - mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} - mdast-util-to-markdown@1.5.0: - resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==} - mdast-util-to-markdown@2.1.2: resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} - mdast-util-to-string@3.2.0: - resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} - mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} @@ -6044,150 +6007,66 @@ packages: engines: {node: '>= 8.0.0'} hasBin: true - micromark-core-commonmark@1.1.0: - resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} - micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} - micromark-extension-gfm-autolink-literal@1.0.5: - resolution: {integrity: sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==} - - micromark-extension-gfm-footnote@1.1.2: - resolution: {integrity: sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==} - - micromark-extension-gfm-strikethrough@1.0.7: - resolution: {integrity: sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==} - - micromark-extension-gfm-table@1.0.7: - resolution: {integrity: sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==} - - micromark-extension-gfm-tagfilter@1.0.2: - resolution: {integrity: sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==} - - micromark-extension-gfm-task-list-item@1.0.5: - resolution: {integrity: sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==} - - micromark-extension-gfm@2.0.3: - resolution: {integrity: sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==} - - micromark-factory-destination@1.1.0: - resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} - micromark-factory-destination@2.0.1: resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} - micromark-factory-label@1.1.0: - resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==} - micromark-factory-label@2.0.1: resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} - micromark-factory-space@1.1.0: - resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} - micromark-factory-space@2.0.1: resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} - micromark-factory-title@1.1.0: - resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==} - micromark-factory-title@2.0.1: resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} - micromark-factory-whitespace@1.1.0: - resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==} - micromark-factory-whitespace@2.0.1: resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} - micromark-util-character@1.2.0: - resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} - micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} - micromark-util-chunked@1.1.0: - resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} - micromark-util-chunked@2.0.1: resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} - micromark-util-classify-character@1.1.0: - resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==} - micromark-util-classify-character@2.0.1: resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} - micromark-util-combine-extensions@1.1.0: - resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==} - micromark-util-combine-extensions@2.0.1: resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} - micromark-util-decode-numeric-character-reference@1.1.0: - resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==} - micromark-util-decode-numeric-character-reference@2.0.2: resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} - micromark-util-decode-string@1.1.0: - resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==} - micromark-util-decode-string@2.0.1: resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} - micromark-util-encode@1.1.0: - resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} - micromark-util-encode@2.0.1: resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} - micromark-util-html-tag-name@1.2.0: - resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} - micromark-util-html-tag-name@2.0.1: resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} - micromark-util-normalize-identifier@1.1.0: - resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==} - micromark-util-normalize-identifier@2.0.1: resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} - micromark-util-resolve-all@1.1.0: - resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==} - micromark-util-resolve-all@2.0.1: resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} - micromark-util-sanitize-uri@1.2.0: - resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} - micromark-util-sanitize-uri@2.0.1: resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} - micromark-util-subtokenize@1.1.0: - resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} - micromark-util-subtokenize@2.1.0: resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} - micromark-util-symbol@1.1.0: - resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} - micromark-util-symbol@2.0.1: resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} - micromark-util-types@1.1.0: - resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} - micromark-util-types@2.0.2: resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} - micromark@3.2.0: - resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} - micromark@4.0.2: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} @@ -6837,6 +6716,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.1.2: @@ -7064,7 +6944,6 @@ packages: resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} hasBin: true - remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} @@ -7151,10 +7030,6 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - sade@1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} - engines: {node: '>=6'} - safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -7873,9 +7748,6 @@ packages: resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} engines: {node: '>=4'} - unified@10.1.2: - resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} - unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -7883,30 +7755,18 @@ packages: resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} engines: {node: '>=8'} - unist-util-is@5.2.1: - resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} - unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - unist-util-stringify-position@3.0.3: - resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} - unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - unist-util-visit-parents@5.1.3: - resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} - unist-util-visit-parents@6.0.2: resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} - unist-util-visit@4.1.2: - resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} - unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} @@ -7963,11 +7823,6 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true - uvu@0.5.6: - resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} - engines: {node: '>=8'} - hasBin: true - v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -7988,15 +7843,9 @@ packages: engines: {node: '>= 18'} hasBin: true - vfile-message@3.1.4: - resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} - vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} - vfile@5.3.7: - resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} - vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} @@ -8068,10 +7917,12 @@ packages: whatwg-encoding@1.0.5: resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@2.3.0: resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} @@ -8278,6 +8129,10 @@ packages: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} + xpath@0.0.34: + resolution: {integrity: sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==} + engines: {node: '>=0.6.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -12563,10 +12418,6 @@ snapshots: '@types/json5@0.0.29': {} - '@types/mdast@3.0.15': - dependencies: - '@types/unist': 2.0.11 - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -13187,6 +13038,8 @@ snapshots: '@webgpu/types@0.1.68': {} + '@xmldom/xmldom@0.9.10': {} + '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -14075,8 +13928,6 @@ snapshots: diff@4.0.2: {} - diff@5.2.0: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -14430,8 +14281,6 @@ snapshots: escape-string-regexp@4.0.0: {} - escape-string-regexp@5.0.0: {} - escodegen@2.1.0: dependencies: esprima: 4.0.1 @@ -16128,8 +15977,6 @@ snapshots: dependencies: tmpl: 1.0.5 - markdown-table@3.0.4: {} - math-intrinsics@1.1.0: {} maverick.js@0.37.0: @@ -16143,30 +15990,6 @@ snapshots: crypt: 0.0.2 is-buffer: 1.1.6 - mdast-util-find-and-replace@2.2.2: - dependencies: - '@types/mdast': 3.0.15 - escape-string-regexp: 5.0.0 - unist-util-is: 5.2.1 - unist-util-visit-parents: 5.1.3 - - mdast-util-from-markdown@1.3.1: - dependencies: - '@types/mdast': 3.0.15 - '@types/unist': 2.0.11 - decode-named-character-reference: 1.2.0 - mdast-util-to-string: 3.2.0 - micromark: 3.2.0 - micromark-util-decode-numeric-character-reference: 1.1.0 - micromark-util-decode-string: 1.1.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - unist-util-stringify-position: 3.0.3 - uvu: 0.5.6 - transitivePeerDependencies: - - supports-color - mdast-util-from-markdown@2.0.2: dependencies: '@types/mdast': 4.0.4 @@ -16184,50 +16007,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-gfm-autolink-literal@1.0.3: - dependencies: - '@types/mdast': 3.0.15 - ccount: 2.0.1 - mdast-util-find-and-replace: 2.2.2 - micromark-util-character: 1.2.0 - - mdast-util-gfm-footnote@1.0.2: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-markdown: 1.5.0 - micromark-util-normalize-identifier: 1.1.0 - - mdast-util-gfm-strikethrough@1.0.3: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-markdown: 1.5.0 - - mdast-util-gfm-table@1.0.7: - dependencies: - '@types/mdast': 3.0.15 - markdown-table: 3.0.4 - mdast-util-from-markdown: 1.3.1 - mdast-util-to-markdown: 1.5.0 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-task-list-item@1.0.2: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-markdown: 1.5.0 - - mdast-util-gfm@2.0.2: - dependencies: - mdast-util-from-markdown: 1.3.1 - mdast-util-gfm-autolink-literal: 1.0.3 - mdast-util-gfm-footnote: 1.0.2 - mdast-util-gfm-strikethrough: 1.0.3 - mdast-util-gfm-table: 1.0.7 - mdast-util-gfm-task-list-item: 1.0.2 - mdast-util-to-markdown: 1.5.0 - transitivePeerDependencies: - - supports-color - mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -16267,11 +16046,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-phrasing@3.0.1: - dependencies: - '@types/mdast': 3.0.15 - unist-util-is: 5.2.1 - mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 @@ -16289,17 +16063,6 @@ snapshots: unist-util-visit: 5.0.0 vfile: 6.0.3 - mdast-util-to-markdown@1.5.0: - dependencies: - '@types/mdast': 3.0.15 - '@types/unist': 2.0.11 - longest-streak: 3.1.0 - mdast-util-phrasing: 3.0.1 - mdast-util-to-string: 3.2.0 - micromark-util-decode-string: 1.1.0 - unist-util-visit: 4.1.2 - zwitch: 2.0.4 - mdast-util-to-markdown@2.1.2: dependencies: '@types/mdast': 4.0.4 @@ -16312,10 +16075,6 @@ snapshots: unist-util-visit: 5.0.0 zwitch: 2.0.4 - mdast-util-to-string@3.2.0: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-string@4.0.0: dependencies: '@types/mdast': 4.0.4 @@ -16344,25 +16103,6 @@ snapshots: content-type: 1.0.4 raw-body: 2.4.1 - micromark-core-commonmark@1.1.0: - dependencies: - decode-named-character-reference: 1.2.0 - micromark-factory-destination: 1.1.0 - micromark-factory-label: 1.1.0 - micromark-factory-space: 1.1.0 - micromark-factory-title: 1.1.0 - micromark-factory-whitespace: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-chunked: 1.1.0 - micromark-util-classify-character: 1.1.0 - micromark-util-html-tag-name: 1.2.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-resolve-all: 1.1.0 - micromark-util-subtokenize: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.2.0 @@ -16382,83 +16122,12 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-extension-gfm-autolink-literal@1.0.5: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-sanitize-uri: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - - micromark-extension-gfm-footnote@1.1.2: - dependencies: - micromark-core-commonmark: 1.1.0 - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-sanitize-uri: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - - micromark-extension-gfm-strikethrough@1.0.7: - dependencies: - micromark-util-chunked: 1.1.0 - micromark-util-classify-character: 1.1.0 - micromark-util-resolve-all: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - - micromark-extension-gfm-table@1.0.7: - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - - micromark-extension-gfm-tagfilter@1.0.2: - dependencies: - micromark-util-types: 1.1.0 - - micromark-extension-gfm-task-list-item@1.0.5: - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - - micromark-extension-gfm@2.0.3: - dependencies: - micromark-extension-gfm-autolink-literal: 1.0.5 - micromark-extension-gfm-footnote: 1.1.2 - micromark-extension-gfm-strikethrough: 1.0.7 - micromark-extension-gfm-table: 1.0.7 - micromark-extension-gfm-tagfilter: 1.0.2 - micromark-extension-gfm-task-list-item: 1.0.5 - micromark-util-combine-extensions: 1.1.0 - micromark-util-types: 1.1.0 - - micromark-factory-destination@1.1.0: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-factory-destination@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-factory-label@1.1.0: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-factory-label@2.0.1: dependencies: devlop: 1.1.0 @@ -16466,23 +16135,11 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-factory-space@1.1.0: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-types: 1.1.0 - micromark-factory-space@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-types: 2.0.2 - micromark-factory-title@1.1.0: - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-factory-title@2.0.1: dependencies: micromark-factory-space: 2.0.1 @@ -16490,13 +16147,6 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-factory-whitespace@1.1.0: - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-factory-whitespace@2.0.1: dependencies: micromark-factory-space: 2.0.1 @@ -16504,61 +16154,30 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-util-character@1.2.0: - dependencies: - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-util-character@2.1.1: dependencies: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-util-chunked@1.1.0: - dependencies: - micromark-util-symbol: 1.1.0 - micromark-util-chunked@2.0.1: dependencies: micromark-util-symbol: 2.0.1 - micromark-util-classify-character@1.1.0: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-util-classify-character@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-util-combine-extensions@1.1.0: - dependencies: - micromark-util-chunked: 1.1.0 - micromark-util-types: 1.1.0 - micromark-util-combine-extensions@2.0.1: dependencies: micromark-util-chunked: 2.0.1 micromark-util-types: 2.0.2 - micromark-util-decode-numeric-character-reference@1.1.0: - dependencies: - micromark-util-symbol: 1.1.0 - micromark-util-decode-numeric-character-reference@2.0.2: dependencies: micromark-util-symbol: 2.0.1 - micromark-util-decode-string@1.1.0: - dependencies: - decode-named-character-reference: 1.2.0 - micromark-util-character: 1.2.0 - micromark-util-decode-numeric-character-reference: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-decode-string@2.0.1: dependencies: decode-named-character-reference: 1.2.0 @@ -16566,49 +16185,24 @@ snapshots: micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-symbol: 2.0.1 - micromark-util-encode@1.1.0: {} - micromark-util-encode@2.0.1: {} - micromark-util-html-tag-name@1.2.0: {} - micromark-util-html-tag-name@2.0.1: {} - micromark-util-normalize-identifier@1.1.0: - dependencies: - micromark-util-symbol: 1.1.0 - micromark-util-normalize-identifier@2.0.1: dependencies: micromark-util-symbol: 2.0.1 - micromark-util-resolve-all@1.1.0: - dependencies: - micromark-util-types: 1.1.0 - micromark-util-resolve-all@2.0.1: dependencies: micromark-util-types: 2.0.2 - micromark-util-sanitize-uri@1.2.0: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-encode: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-sanitize-uri@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-encode: 2.0.1 micromark-util-symbol: 2.0.1 - micromark-util-subtokenize@1.1.0: - dependencies: - micromark-util-chunked: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-util-subtokenize@2.1.0: dependencies: devlop: 1.1.0 @@ -16616,36 +16210,10 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-util-symbol@1.1.0: {} - micromark-util-symbol@2.0.1: {} - micromark-util-types@1.1.0: {} - micromark-util-types@2.0.2: {} - micromark@3.2.0: - dependencies: - '@types/debug': 4.1.12 - debug: 4.4.3 - decode-named-character-reference: 1.2.0 - micromark-core-commonmark: 1.1.0 - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-chunked: 1.1.0 - micromark-util-combine-extensions: 1.1.0 - micromark-util-decode-numeric-character-reference: 1.1.0 - micromark-util-encode: 1.1.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-resolve-all: 1.1.0 - micromark-util-sanitize-uri: 1.2.0 - micromark-util-subtokenize: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - transitivePeerDependencies: - - supports-color - micromark@4.0.2: dependencies: '@types/debug': 4.1.12 @@ -17471,7 +17039,6 @@ snapshots: dependencies: jsesc: 3.1.0 - remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -17576,10 +17143,6 @@ snapshots: dependencies: queue-microtask: 1.2.3 - sade@1.8.1: - dependencies: - mri: 1.2.0 - safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -18427,16 +17990,6 @@ snapshots: unicode-property-aliases-ecmascript@2.2.0: {} - unified@10.1.2: - dependencies: - '@types/unist': 2.0.11 - bail: 2.0.2 - extend: 3.0.2 - is-buffer: 2.0.5 - is-plain-obj: 4.1.0 - trough: 2.2.0 - vfile: 5.3.7 - unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -18451,10 +18004,6 @@ snapshots: dependencies: crypto-random-string: 2.0.0 - unist-util-is@5.2.1: - dependencies: - '@types/unist': 2.0.11 - unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -18463,30 +18012,15 @@ snapshots: dependencies: '@types/unist': 3.0.3 - unist-util-stringify-position@3.0.3: - dependencies: - '@types/unist': 2.0.11 - unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-visit-parents@5.1.3: - dependencies: - '@types/unist': 2.0.11 - unist-util-is: 5.2.1 - unist-util-visit-parents@6.0.2: dependencies: '@types/unist': 3.0.3 unist-util-is: 6.0.1 - unist-util-visit@4.1.2: - dependencies: - '@types/unist': 2.0.11 - unist-util-is: 5.2.1 - unist-util-visit-parents: 5.1.3 - unist-util-visit@5.0.0: dependencies: '@types/unist': 3.0.3 @@ -18560,13 +18094,6 @@ snapshots: uuid@9.0.1: {} - uvu@0.5.6: - dependencies: - dequal: 2.0.3 - diff: 5.2.0 - kleur: 4.1.5 - sade: 1.8.1 - v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@8.1.1: @@ -18615,23 +18142,11 @@ snapshots: - supports-color - typescript - vfile-message@3.1.4: - dependencies: - '@types/unist': 2.0.11 - unist-util-stringify-position: 3.0.3 - vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 - vfile@5.3.7: - dependencies: - '@types/unist': 2.0.11 - is-buffer: 2.0.5 - unist-util-stringify-position: 3.0.3 - vfile-message: 3.1.4 - vfile@6.0.3: dependencies: '@types/unist': 3.0.3 @@ -19013,6 +18528,8 @@ snapshots: xmlhttprequest-ssl@2.1.2: {} + xpath@0.0.34: {} + xtend@4.0.2: {} y18n@5.0.8: {} diff --git a/src/app/api/books/image/route.ts b/src/app/api/books/image/route.ts index a9a735c..17435c3 100644 --- a/src/app/api/books/image/route.ts +++ b/src/app/api/books/image/route.ts @@ -1,12 +1,15 @@ import { NextRequest, NextResponse } from 'next/server'; import { legadoClient } from '@/lib/legado.client'; +import crypto from 'crypto'; import { validateProxyUrlServerSide } from '@/lib/server/ssrf'; import { getAuthorizedBooksUsername } from '../_utils'; export const runtime = 'nodejs'; +const imageCache = new Map(); + function asObjectHeader(value?: string | Record): Record { if (!value) return {}; if (typeof value === 'object') return value; @@ -33,19 +36,52 @@ export async function GET(request: NextRequest) { if (!sourceId || !url) return NextResponse.json({ error: '缺少 sourceId 或 url' }, { status: 400 }); if (!(await validateProxyUrlServerSide(url))) return NextResponse.json({ error: '图片地址未通过安全校验' }, { status: 400 }); const source = await legadoClient.getSourceById(sourceId); + let imageOptions: any = {}; + try { + const rawOptions = searchParams.get('options') || ''; + imageOptions = rawOptions ? JSON.parse(rawOptions) : {}; + } catch { + imageOptions = {}; + } const headers: Record = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36', Referer: source.legado?.bookSourceUrl || source.url, ...asObjectHeader(source.legado?.header), + ...(imageOptions.headers && typeof imageOptions.headers === 'object' ? imageOptions.headers : {}), }; delete headers.Host; delete headers.host; - const res = await fetch(url, { headers, cache: 'no-store' }); + const cacheKey = crypto.createHash('sha1').update(`${sourceId}|${url}|${JSON.stringify(imageOptions)}`).digest('hex'); + const cached = imageCache.get(cacheKey); + if (cached && cached.expiresAt > Date.now() && !request.headers.get('range')) { + return new NextResponse(cached.data, { headers: { 'Content-Type': cached.contentType, 'Cache-Control': 'public, max-age=86400' } }); + } + const range = request.headers.get('range'); + if (range) headers.Range = range; + const res = await fetch(url, { method: imageOptions.method || 'GET', headers, body: imageOptions.body, cache: 'no-store', redirect: 'manual' }); + if ([301, 302, 303, 307, 308].includes(res.status)) { + const location = res.headers.get('location'); + if (!location) return NextResponse.json({ error: '图片重定向缺少地址' }, { status: 502 }); + const redirected = new URL(location, url).toString(); + if (!(await validateProxyUrlServerSide(redirected))) return NextResponse.json({ error: '图片重定向地址未通过安全校验' }, { status: 400 }); + const second = await fetch(redirected, { method: imageOptions.method || 'GET', headers, body: imageOptions.body, cache: 'no-store' }); + if (!second.ok) return NextResponse.json({ error: `图片请求失败: ${second.status}` }, { status: second.status }); + const data = new Uint8Array(await second.arrayBuffer()); + const contentType = second.headers.get('content-type') || 'image/jpeg'; + if (!range) imageCache.set(cacheKey, { data, contentType, expiresAt: Date.now() + 86400_000 }); + return new NextResponse(data, { status: second.status, headers: { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=86400' } }); + } if (!res.ok) return NextResponse.json({ error: `图片请求失败: ${res.status}` }, { status: res.status }); - return new NextResponse(res.body, { + const data = new Uint8Array(await res.arrayBuffer()); + const contentType = res.headers.get('content-type') || 'image/jpeg'; + if (!range) imageCache.set(cacheKey, { data, contentType, expiresAt: Date.now() + 86400_000 }); + return new NextResponse(data, { + status: res.status, headers: { - 'Content-Type': res.headers.get('content-type') || 'image/jpeg', + 'Content-Type': contentType, 'Cache-Control': 'public, max-age=86400', + ...(res.headers.get('content-range') ? { 'Content-Range': res.headers.get('content-range') as string } : {}), + ...(res.headers.get('accept-ranges') ? { 'Accept-Ranges': res.headers.get('accept-ranges') as string } : {}), }, }); } catch (error) { diff --git a/src/lib/book.types.ts b/src/lib/book.types.ts index 5ae0c27..07d799d 100644 --- a/src/lib/book.types.ts +++ b/src/lib/book.types.ts @@ -17,8 +17,11 @@ export interface LegadoRuleSearch { bookUrl?: string; kind?: string; lastChapter?: string; + checkKeyWord?: string; + wordCount?: string; } + export type LegadoRuleExplore = LegadoRuleSearch; export interface LegadoRuleBookInfo { @@ -29,6 +32,7 @@ export interface LegadoRuleBookInfo { tocUrl?: string; kind?: string; lastChapter?: string; + wordCount?: string; } export interface LegadoRuleToc { @@ -37,11 +41,18 @@ export interface LegadoRuleToc { chapterUrl?: string; isVip?: string; isPay?: string; + nextTocUrl?: string; + updateTime?: string; + wordCount?: string; + chapterInfo?: string; } export interface LegadoRuleContent { content?: string; nextContentUrl?: string; + imageStyle?: string; + sourceRegex?: string; + payAction?: string; } export interface LegadoBookSourceRule { @@ -49,13 +60,23 @@ export interface LegadoBookSourceRule { bookSourceUrl?: string; bookSourceGroup?: string; bookSourceType?: number; + customButton?: boolean | unknown; + eventListener?: boolean | unknown; + variable?: string | Record; + loginUi?: string | unknown; + concurrentRate?: string | number; + respondTime?: number; + lastUpdateTime?: string | number; enabled?: boolean; + enabledCookieJar?: boolean; enabledExplore?: boolean; exploreUrl?: string; header?: string | Record; loginUrl?: string; searchUrl?: string; bookInfoUrl?: string; + bookInfoInit?: string; + bookUrlPattern?: string; tocUrl?: string; chapterUrl?: string; ruleSearch?: LegadoRuleSearch; @@ -63,6 +84,7 @@ export interface LegadoBookSourceRule { ruleBookInfo?: LegadoRuleBookInfo; ruleToc?: LegadoRuleToc; ruleContent?: LegadoRuleContent; + ruleReview?: unknown; customOrder?: number; weight?: number; } diff --git a/src/lib/legado.client.ts b/src/lib/legado.client.ts index f503175..88308e0 100644 --- a/src/lib/legado.client.ts +++ b/src/lib/legado.client.ts @@ -2,6 +2,9 @@ import * as cheerio from 'cheerio/slim'; import crypto from 'crypto'; import he from 'he'; +import vm from 'vm'; +import { DOMParser } from '@xmldom/xmldom'; +import xpath from 'xpath'; import { getConfig } from './config'; import { @@ -36,6 +39,19 @@ const detailCache = new Map(); const tocCache = new Map(); const chapterCache = new Map(); +interface RequestOptions { + url: string; + method?: string; + body?: string; + headers?: Record; + charset?: string; + retry?: number; +} + +const cookieJar = new Map(); +const variableStore = new Map(); +const imageMemoryCache = new Map(); + function stableId(input: string) { return crypto.createHash('sha1').update(input).digest('hex').slice(0, 16); } @@ -87,18 +103,84 @@ function encodeRuleParam(value: string) { return encodeURIComponent(value).replace(/%20/g, '+'); } +function safeEvalTemplateExpression(expr: string, keyword: string, page: number) { + const key = keyword; + const searchTerms = keyword; + const java = { + base64Encode: (value: unknown) => Buffer.from(String(value ?? ''), 'utf8').toString('base64'), + base64Decode: (value: unknown) => Buffer.from(String(value ?? ''), 'base64').toString('utf8'), + encodeURI: (value: unknown) => encodeURIComponent(String(value ?? '')), + encodeURIComponent: (value: unknown) => encodeURIComponent(String(value ?? '')), + }; + try { + // eslint-disable-next-line no-new-func + const fn = new Function('key', 'keyword', 'searchTerms', 'page', 'java', `return (${expr});`); + return jsonPrimitiveToString(fn(key, keyword, searchTerms, page, java)); + } catch { + return ''; + } +} + +function renderTemplateExpressions(raw: string, keyword = '', page = 1) { + return raw.replace(/<([^,<>]*),\s*\{\{(.*?)\}\}>/g, (_, prefix, expr) => { + const value = safeEvalTemplateExpression(String(expr).trim(), keyword, page); + return value ? `${prefix || ''}${value}` : ''; + }).replace(/\{\{(.*?)\}\}/g, (_, expr) => { + const value = String(expr).trim(); + if (/^(key|keyword|searchTerms)$/.test(value)) return encodeRuleParam(keyword || ''); + if (/^(page|pageIndex)$/.test(value)) return String(page); + const evaluated = safeEvalTemplateExpression(value, keyword, page); + return encodeRuleParam(evaluated); + }); +} + +function runJsSnippet(code: string, context: Record, timeout = 1000): string { + const java = { + base64Encode: (value: unknown) => Buffer.from(String(value ?? ''), 'utf8').toString('base64'), + base64Decode: (value: unknown) => Buffer.from(String(value ?? ''), 'base64').toString('utf8'), + md5Encode: (value: unknown) => crypto.createHash('md5').update(String(value ?? '')).digest('hex'), + encodeURI: (value: unknown) => encodeURIComponent(String(value ?? '')), + encodeURIComponent: (value: unknown) => encodeURIComponent(String(value ?? '')), + put: (key: string, value: any) => { variableStore.set(key, value); return value; }, + get: (key: string) => variableStore.get(key), + htmlFormat: (value: unknown) => he.decode(String(value ?? '')).replace(/]+>/g, ''), + }; + const sandbox: Record = { ...context, java, Buffer, JSON, String, Number, Math, Array, Object, console: { log: () => undefined } }; + try { + const script = new vm.Script(`(function(){ ${code.replace(/^@js:/, '')}\n})()`); + const result = script.runInNewContext(sandbox, { timeout }); + return jsonPrimitiveToString(result ?? sandbox.result ?? ''); + } catch { + return ''; + } +} + +function applyPutGetRules(rule: string, value: string) { + const putMatch = rule.match(/@put:\s*\{([\s\S]*?)\}/); + if (putMatch) { + try { + const obj = JSON.parse(`{${putMatch[1]}}`); + Object.entries(obj).forEach(([key, val]) => variableStore.set(key, val)); + } catch { + const pair = putMatch[1].match(/([A-Za-z0-9_$-]+)\s*:\s*['"]?([^,'"]+)['"]?/); + if (pair) variableStore.set(pair[1], value || pair[2]); + } + } + const getMatch = rule.match(/@get:\s*\{\s*([A-Za-z0-9_$-]+)\s*\}/); + if (getMatch) return jsonPrimitiveToString(variableStore.get(getMatch[1])); + return value; +} + function buildUrlFromTemplate(template: string, source: BookSource, keyword?: string, page = 1, baseOverride?: string) { const base = baseOverride || sourceBase(source); let raw = template || base; - raw = raw.replace(/\{\{(?:key|keyword|searchTerms)\}\}/g, encodeRuleParam(keyword || '')); - raw = raw.replace(/\{\{(?:page|pageIndex)\}\}/g, String(page)); + raw = renderTemplateExpressions(raw, keyword || '', page); raw = raw .replace(/\{searchTerms\}/g, encodeRuleParam(keyword || '')) .replace(/\{key\}/g, encodeRuleParam(keyword || '')) .replace(/\{keyword\}/g, encodeRuleParam(keyword || '')) .replace(/\{page\}/g, String(page)) .replace(/\{pageIndex\}/g, String(page)); - if (raw.includes('{{') && keyword) raw = raw.replace(/\{\{.*?\}\}/g, encodeRuleParam(keyword)); return normalizeUrl(base, raw); } @@ -129,33 +211,54 @@ function readJsonPath(input: any, path?: string): any { if (normalized.startsWith('-@json:')) normalized = normalized.slice(7); if (!normalized || normalized === '$') return input; - const recursive = normalized.match(/^\$\.\.([A-Za-z0-9_$-]+)\[\*\]$/); + const filterMatch = normalized.match(/^(.*)\[\?\(@\.([A-Za-z0-9_$-]+)\s*(==|=|!=)\s*['"]?([^'"\]]+)['"]?\)\](.*)$/); + if (filterMatch) { + const base = readJsonPath(input, filterMatch[1] || '$'); + const list = Array.isArray(base) ? base : []; + const filtered = list.filter((item) => { + const actual = jsonPrimitiveToString(item?.[filterMatch[2]]); + return filterMatch[3] === '!=' ? actual !== filterMatch[4] : actual === filterMatch[4]; + }); + return filterMatch[5] ? readJsonPath(filtered, `$${filterMatch[5]}`) : filtered; + } + + const recursive = normalized.match(/^\$\.\.([A-Za-z0-9_$-]+)(.*)$/); if (recursive) { const key = recursive[1]; + const rest = recursive[2] || ''; const out: any[] = []; const walk = (node: any) => { if (!node || typeof node !== 'object') return; - if (Array.isArray(node)) { - node.forEach(walk); - return; - } - if (Array.isArray(node[key])) out.push(...node[key]); + if (Array.isArray(node)) return node.forEach(walk); + if (node[key] !== undefined) out.push(node[key]); Object.values(node).forEach(walk); }; walk(input); - return out; + return rest ? readJsonPath(out.flat(), `$${rest}`) : out.flat(); } normalized = normalized.replace(/^\$\.?/, ''); - const tokens = normalized.match(/[^.[\]]+|\[\*\]|\[\d+\]/g) || []; + const tokens = normalized.match(/[^.[\]]+|\[\*\]|\[-?\d+\]|\[-?\d*:-?\d*(?::-?\d+)?\]|\[['"][^\]]+['"](?:,\s*['"][^\]]+['"])*\]/g) || []; let current = input; for (const token of tokens) { if (current === undefined || current === null) return undefined; if (token === '[*]') { + current = Array.isArray(current) ? current.flat() : []; + } else if (/^\[-?\d+\]$/.test(token)) { + const idx = Number(token.slice(1, -1)); + current = Array.isArray(current) ? current[idx < 0 ? current.length + idx : idx] : undefined; + } else if (/^\[-?\d*:-?\d*/.test(token)) { if (!Array.isArray(current)) return []; - current = current.flat(); - } else if (/^\[\d+\]$/.test(token)) { - current = Array.isArray(current) ? current[Number(token.slice(1, -1))] : undefined; + const parts = token.slice(1, -1).split(':').map((item) => item === '' ? undefined : Number(item)); + const start = parts[0] === undefined ? 0 : parts[0] < 0 ? current.length + parts[0] : parts[0]; + const end = parts[1] === undefined ? current.length : parts[1] < 0 ? current.length + parts[1] : parts[1]; + const step = parts[2] || 1; + const sliced = current.slice(start, end); + current = step === 1 ? sliced : sliced.filter((_, index) => index % Math.abs(step) === 0); + } else if (/^\[/.test(token)) { + const keys = Array.from(token.matchAll(/['"]([^'"]+)['"]/g)).map((m) => m[1]); + if (Array.isArray(current)) current = current.map((item) => keys.map((key) => item?.[key])).flat().filter((item) => item !== undefined); + else current = keys.map((key) => current?.[key]).filter((item) => item !== undefined); } else if (Array.isArray(current)) { current = current.map((item) => item?.[token]).filter((item) => item !== undefined); } else { @@ -201,7 +304,15 @@ function readJsonRule(json: any, rule?: string, source?: BookSource, baseUrl?: s .filter(Boolean) .join(''); } - return ''; + const resultText = typeof json === 'string' ? json : JSON.stringify(json); + if (/java\.base64Decode/.test(trimmed)) return Buffer.from(resultText, 'base64').toString('utf8'); + if (/java\.base64Encode/.test(trimmed)) return Buffer.from(resultText, 'utf8').toString('base64'); + if (/java\.md5Encode/.test(trimmed)) return crypto.createHash('md5').update(resultText).digest('hex'); + const replaceMatch = trimmed.match(/result\.replace\(\s*\/([^/]+)\/[gimuy]*\s*,\s*['"]([^'"]*)['"]\s*\)/); + if (replaceMatch) return resultText.replace(new RegExp(replaceMatch[1], 'g'), replaceMatch[2]); + const matchMatch = trimmed.match(/result\.match\(\s*\/([^/]+)\/[gimuy]*\s*\)/); + if (matchMatch) return resultText.match(new RegExp(matchMatch[1]))?.[1] || resultText.match(new RegExp(matchMatch[1]))?.[0] || ''; + return runJsSnippet(trimmed, { result: json, baseUrl, src: json }); } const value = readJsonPath(json, trimmed); const text = jsonPrimitiveToString(value); @@ -233,7 +344,7 @@ function applyRuleFilters(value: string, filters: string[]) { const replacement = filters[index + 1] ?? ''; if (!pattern) continue; try { - result = result.replace(new RegExp(pattern, 'g'), replacement); + result = result.replace(new RegExp(pattern, 'g'), replacement.replace(/\$(\d+)/g, '$$$$1')); } catch { result = result.split(pattern).join(replacement); } @@ -241,8 +352,37 @@ function applyRuleFilters(value: string, filters: string[]) { return result; } +function readAllInOneList(raw: string, rule?: string): Array> { + const trimmed = (rule || '').trim(); + if (!trimmed.startsWith(':')) return []; + const pattern = trimmed.slice(1); + try { + const regex = new RegExp(pattern, 'gs'); + const out: Array> = []; + let match: RegExpExecArray | null; + while ((match = regex.exec(raw)) !== null) { + const groups = (match.groups || {}) as Record; + const item: Record = { _0: match[0] }; + Array.from(match).forEach((value: string | undefined, index: number) => { item[`_${index}`] = value || ''; }); + Object.entries(groups).forEach(([key, value]) => { item[key] = String(value || ''); }); + out.push(item); + if (match[0] === '') regex.lastIndex += 1; + } + return out; + } catch { + return []; + } +} + +function readRegexItem(item: Record, rule?: string, baseUrl?: string) { + const key = (rule || '').trim().replace(/^\$?\{?/, '').replace(/\}?$/, ''); + const value = item[key] || item[`_${key}`] || ''; + if ((/url|href|src|cover/i.test(key) || /^https?:\/\//i.test(value)) && value && baseUrl) return normalizeUrl(baseUrl, value); + return value; +} + function isLegadoAttrToken(value: string) { - return /^(href|src|title|alt|text|textNodes|html|content|value|data-[\w-]+)$/i.test(value.trim()); + return /^(href|src|title|alt|text|textNodes|ownText|all|html|content|value|data-[\w-]+)$/i.test(value.trim()); } function normalizeLegadoSelector(selector: string) { @@ -272,7 +412,7 @@ function parseStep(step: string): { selector: string; attr: string } { }; } - const attrMatch = trimmed.match(/(?:@|::)(text|textNodes|html|href|src|title|alt|content|value|data-[\w-]+)$/i); + const attrMatch = trimmed.match(/(?:@|::)(text|textNodes|ownText|all|html|href|src|title|alt|content|value|data-[\w-]+)$/i); if (attrMatch) { return { selector: normalizeLegadoSelector(trimmed.slice(0, attrMatch.index).trim()), attr: attrMatch[1] }; } @@ -285,28 +425,125 @@ function stripFilters(rule: string) { return splitRuleFilters(rule).base; } +function applyLegadoIndexSelector(current: cheerio.Cheerio, selector: string): { current: cheerio.Cheerio; selector: string } { + let normalized = selector.trim(); + const exclude = normalized.match(/\[!(-?\d+)\]$/); + if (exclude) { + normalized = normalized.slice(0, exclude.index).trim(); + current = normalized ? current.find(normalized) : current; + const idx = Number(exclude[1]); + const real = idx < 0 ? current.length + idx : idx; + return { current: current.filter((index) => index !== real), selector: '' }; + } + const range = normalized.match(/\[(-?\d*):(-?\d*)(?::(-?\d+))?\]$/); + if (range) { + normalized = normalized.slice(0, range.index).trim(); + current = normalized ? current.find(normalized) : current; + const length = current.length; + const start = range[1] ? Number(range[1]) : 0; + const end = range[2] ? Number(range[2]) : length; + const realStart = start < 0 ? length + start : start; + const realEnd = end < 0 ? length + end : end; + return { current: current.slice(realStart, realEnd), selector: '' }; + } + const indexMatch = normalized.match(/(?:\[(-?\d+)\]|\.(-?\d+))$/); + if (indexMatch) { + normalized = normalized.slice(0, indexMatch.index).trim(); + current = normalized ? current.find(normalized) : current; + const idx = Number(indexMatch[1] ?? indexMatch[2]); + const real = idx < 0 ? current.length + idx : idx; + return { current: current.eq(real), selector: '' }; + } + return { current, selector: normalized }; +} + function selectElements($: cheerio.CheerioAPI, root: cheerio.Cheerio, rule?: string): cheerio.Cheerio { const normalized = stripFilters(rule || ''); - if (!normalized) return root; - const steps = normalized.split(/&&|@css:/).map((item) => item.trim()).filter(Boolean); + const reverse = normalized.trim().startsWith('-') && !normalized.trim().startsWith('-@'); + const effective = reverse ? normalized.trim().slice(1).trim() : normalized; + if (!effective) return root; + const steps = effective.split(/&&|@css:/).map((item) => item.trim()).filter(Boolean); let current = root; for (const rawStep of steps) { const { selector, attr } = parseStep(rawStep); - if (!selector || attr) break; - current = current.find(selector); + if (attr) break; + if (!selector) continue; + if (/^children$/i.test(selector)) { + current = current.children(); + continue; + } + const indexed = applyLegadoIndexSelector(current, selector); + current = indexed.selector ? current.find(indexed.selector) : indexed.current; } + if (reverse) current = $(current.toArray().reverse()); return current; } + +function selectXPath($: cheerio.CheerioAPI, root: cheerio.Cheerio, rule: string): { nodes: cheerio.Cheerio; attr: string; value?: string } { + const expr = rule.trim().replace(/^@XPath:/i, ''); + try { + const html = $.html(root); + const doc = new DOMParser({ errorHandler: () => undefined }).parseFromString(html, 'text/html'); + const selected = xpath.select(expr, doc as any) as any; + const list = Array.isArray(selected) ? selected : [selected]; + const values = list.map((node) => { + if (node === undefined || node === null) return ''; + if (typeof node === 'string' || typeof node === 'number' || typeof node === 'boolean') return String(node); + if (node.nodeType === 2) return node.nodeValue || ''; + if (node.nodeType === 3 || node.nodeType === 4) return node.nodeValue || ''; + return node.textContent || ''; + }).filter(Boolean); + if (values.length > 0) return { nodes: root, attr: '', value: values.join('\n') }; + } catch { + // fallback below + } + + let fallbackExpr = expr; + let attr = ''; + const attrMatch = fallbackExpr.match(/\/@([A-Za-z0-9_-]+)$/); + if (attrMatch) { + attr = attrMatch[1]; + fallbackExpr = fallbackExpr.slice(0, attrMatch.index); + } else if (/\/text\(\)$/.test(fallbackExpr)) { + attr = 'text'; + fallbackExpr = fallbackExpr.replace(/\/text\(\)$/, ''); + } + const parts = fallbackExpr.split('/').filter(Boolean).map((part) => { + const match = part.match(/^([A-Za-z0-9_*.-]+)(?:\[@([A-Za-z0-9_-]+)=['"]([^'"]+)['"]\])?(?:\[(\d+)\])?$/); + if (!match) return ''; + const tag = match[1] === '*' ? '*' : match[1]; + const filter = match[2] ? `[${match[2]}="${match[3]}"]` : ''; + const index = match[4] ? `:nth-of-type(${match[4]})` : ''; + return `${tag}${filter}${index}`; + }).filter(Boolean); + return { nodes: parts.length ? root.find(parts.join(' ')) : root, attr }; +} + function readValue($: cheerio.CheerioAPI, root: cheerio.Cheerio, rule?: string, baseUrl?: string): string { for (const alternative of splitAlternatives(rule)) { const normalized = stripFilters(alternative); + if (/^@?XPath:/i.test(normalized) || normalized.startsWith('//')) { + const { nodes, attr, value: xpathValue } = selectXPath($, root, normalized); + const node = nodes.first(); + let value = xpathValue !== undefined ? xpathValue : attr === 'text' || !attr ? node.text() : node.attr(attr) || ''; + value = he.decode(value || '').replace(/\u00a0/g, ' ').trim(); + value = applyRuleFilters(value, splitRuleFilters(alternative).filters); + if (value) return value; + continue; + } const steps = normalized.split(/&&|@css:/).map((item) => item.trim()).filter(Boolean); let current = root; let attr = ''; for (const rawStep of steps) { const parsed = parseStep(rawStep); - if (parsed.selector) current = current.find(parsed.selector); + if (parsed.selector) { + if (/^children$/i.test(parsed.selector)) current = current.children(); + else { + const indexed = applyLegadoIndexSelector(current, parsed.selector); + current = indexed.selector ? current.find(indexed.selector) : indexed.current; + } + } if (parsed.attr) attr = parsed.attr; } if (current.length === 0 && steps.length === 1) { @@ -317,11 +554,14 @@ function readValue($: cheerio.CheerioAPI, root: cheerio.Cheerio, rule?: str let value = ''; const normalizedAttr = attr.toLowerCase(); if (!attr || normalizedAttr === 'text' || normalizedAttr === 'textnodes') value = node.text(); + else if (normalizedAttr === 'owntext') value = node.clone().children().remove().end().text(); + else if (normalizedAttr === 'all') value = current.toArray().map((el) => $(el).text()).join('\n'); else if (normalizedAttr === 'html') value = node.html() || ''; else value = node.attr(attr) || ''; value = he.decode(value || '').replace(/\u00a0/g, ' ').trim(); if ((normalizedAttr === 'href' || normalizedAttr === 'src') && value && baseUrl) value = normalizeUrl(baseUrl, value); value = applyRuleFilters(value, splitRuleFilters(alternative).filters); + value = applyPutGetRules(alternative, value); if (value) return value; } return ''; @@ -335,7 +575,13 @@ function readValues($: cheerio.CheerioAPI, root: cheerio.Cheerio, rule?: st let attr = ''; for (const rawStep of steps) { const parsed = parseStep(rawStep); - if (parsed.selector) current = current.find(parsed.selector); + if (parsed.selector) { + if (/^children$/i.test(parsed.selector)) current = current.children(); + else { + const indexed = applyLegadoIndexSelector(current, parsed.selector); + current = indexed.selector ? current.find(indexed.selector) : indexed.current; + } + } if (parsed.attr) attr = parsed.attr; } if (current.length === 0 && steps.length === 1) { @@ -347,6 +593,8 @@ function readValues($: cheerio.CheerioAPI, root: cheerio.Cheerio, rule?: st const node = $(element); let value = ''; if (!attr || normalizedAttr === 'text' || normalizedAttr === 'textnodes') value = node.text(); + else if (normalizedAttr === 'owntext') value = node.clone().children().remove().end().text(); + else if (normalizedAttr === 'all') value = node.text(); else if (normalizedAttr === 'html') value = node.html() || ''; else value = node.attr(attr) || ''; value = he.decode(value || '').replace(/\u00a0/g, ' ').trim(); @@ -367,7 +615,7 @@ function applyContentJsRule(value: string, jsRule: string): string { .map((src) => ``) .join(''); } - return value; + return runJsSnippet(jsRule, { result: value, src: value }) || value; } function contentFromRule(raw: string, rule?: string, baseUrl?: string): string { @@ -406,9 +654,12 @@ function proxyChapterImages(content: string, source: BookSource) { if (!/]*?)\bsrc=(['"])(.*?)\2([^>]*)>/gi, (match, before, quote, src, after) => { - if (!src || src.startsWith('/api/books/image')) return match; - const proxied = `/api/books/image?sourceId=${encodeURIComponent(source.id)}&url=${encodeURIComponent(src)}`; + return content.replace(/]*?)\bsrc=(['"])(.*?)\2([^>]*)>/gi, (match, before, quote, rawSrc, after) => { + if (!rawSrc || rawSrc.startsWith('/api/books/image')) return match; + const optionIndex = rawSrc.indexOf(',{'); + const src = optionIndex > 0 ? rawSrc.slice(0, optionIndex) : rawSrc; + const options = optionIndex > 0 ? rawSrc.slice(optionIndex + 1) : ''; + const proxied = `/api/books/image?sourceId=${encodeURIComponent(source.id)}&url=${encodeURIComponent(src)}${options ? `&options=${encodeURIComponent(options)}` : ''}`; return ``; }); } @@ -488,27 +739,83 @@ function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } +function splitUrlOptions(input: string): RequestOptions { + const raw = input.trim(); + const comma = raw.indexOf(','); + if (comma <= 0) return { url: raw }; + const candidate = raw.slice(comma + 1).trim(); + if (!candidate.startsWith('{')) return { url: raw }; + try { + const options = JSON.parse(candidate); + return { + url: raw.slice(0, comma).trim(), + method: options.method, + body: options.body, + headers: options.headers && typeof options.headers === 'object' ? options.headers : undefined, + charset: options.charset, + retry: Number.isFinite(Number(options.retry)) ? Number(options.retry) : undefined, + }; + } catch { + return { url: raw }; + } +} + +function getCookieHeader(sourceId: string) { + return cookieJar.get(sourceId) || ''; +} + +function mergeSetCookie(sourceId: string, setCookie: string | null) { + if (!setCookie) return; + const current = new Map(); + (cookieJar.get(sourceId) || '').split(/;\s*/).filter(Boolean).forEach((item) => { + const idx = item.indexOf('='); + if (idx > 0) current.set(item.slice(0, idx), item.slice(idx + 1)); + }); + setCookie.split(/,(?=\s*[^;,]+=)/).forEach((cookie) => { + const pair = cookie.split(';')[0]?.trim(); + const idx = pair?.indexOf('=') ?? -1; + if (idx > 0) current.set(pair.slice(0, idx), pair.slice(idx + 1)); + }); + cookieJar.set(sourceId, Array.from(current.entries()).map(([key, value]) => `${key}=${value}`).join('; ')); +} + async function fetchText(source: BookSource, url: string): Promise { if (!url?.trim()) throw new Error('书源请求地址为空'); - const safe = await validateProxyUrlServerSide(url); - if (!safe) throw new Error(`书源地址未通过安全校验: ${url}`); - const cacheKey = `${LEGADO_CACHE_VERSION}|text|${source.id}|${url}`; + const request = splitUrlOptions(url); + const safe = await validateProxyUrlServerSide(request.url); + if (!safe) throw new Error(`书源地址未通过安全校验: ${request.url}`); + const cacheKey = `${LEGADO_CACHE_VERSION}|text|${source.id}|${request.method || 'GET'}|${request.url}|${request.body || ''}`; const cached = textCache.get(cacheKey); const { cacheTTL } = await resolveLegadoConfig(); if (cached && cached.expiresAt > Date.now()) return cached.data; let lastError: unknown; - for (let attempt = 0; attempt < 3; attempt += 1) { + const maxAttempts = Math.max(1, (request.retry ?? 2) + 1); + for (let attempt = 0; attempt < maxAttempts; attempt += 1) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS); try { - const response = await fetch(url, { headers: buildHeaders(source), signal: controller.signal, cache: 'no-store' }); + const headers: Record = { + ...(buildHeaders(source) as Record), + ...(request.headers || {}), + }; + const cookie = getCookieHeader(source.id); + if (source.legado?.enabledCookieJar && cookie) headers.Cookie = cookie; + const method = (request.method || (request.body ? 'POST' : 'GET')).toUpperCase(); + const response = await fetch(request.url, { + method, + headers, + body: method === 'GET' || method === 'HEAD' ? undefined : request.body, + signal: controller.signal, + cache: 'no-store', + }); + if (source.legado?.enabledCookieJar) mergeSetCookie(source.id, response.headers.get('set-cookie')); if (!response.ok) throw new Error(`请求失败: ${response.status}`); const contentLength = Number(response.headers.get('content-length') || '0'); if (contentLength > MAX_TEXT_BYTES) throw new Error('响应内容过大'); const buffer = await response.arrayBuffer(); const contentType = response.headers.get('content-type') || ''; - const charset = contentType.match(/charset=([^;]+)/i)?.[1]?.trim().replace(/^['"]|['"]$/g, ''); + const charset = request.charset || contentType.match(/charset=([^;]+)/i)?.[1]?.trim().replace(/^['"]|['"]$/g, ''); const decoderName = /gbk|gb2312|gb18030/i.test(charset || '') ? 'gb18030' : (charset || 'utf-8'); let text: string; try { @@ -521,7 +828,7 @@ async function fetchText(source: BookSource, url: string): Promise { return text; } catch (error) { lastError = error; - if (attempt < 2) await wait(300 * (attempt + 1)); + if (attempt < maxAttempts - 1) await wait(300 * (attempt + 1)); } finally { clearTimeout(timeout); } @@ -573,8 +880,8 @@ function parseExploreUrl(exploreUrl?: string): Array<{ title: string; template: const json = parseJsonMaybe(raw); if (Array.isArray(json)) { return json - .map((item) => ({ title: jsonPrimitiveToString(item?.title), template: jsonPrimitiveToString(item?.url) })) - .filter((item) => !!item.title && !!item.template); + .map((item) => ({ title: jsonPrimitiveToString(item?.title), template: jsonPrimitiveToString(item?.url) || `__group__:${jsonPrimitiveToString(item?.title)}` })) + .filter((item) => !!item.title); } return raw .split('&&') @@ -642,6 +949,9 @@ export class LegadoClient { for (let page = 1; page <= DEFAULT_LEGADO_SEARCH_PAGES; page += 1) { const targetUrl = buildUrlFromTemplate(rule.searchUrl, source, q, page); const html = await fetchText(source, targetUrl); + if (page === 1 && rule.ruleSearch.checkKeyWord && !html.includes(rule.ruleSearch.checkKeyWord) && !html.includes(q)) { + throw new Error('搜索结果校验失败'); + } let pageCount = 0; const json = parseJsonMaybe(html); if (json && ruleIsJson(rule.ruleSearch.bookList)) { @@ -666,6 +976,27 @@ export class LegadoClient { tags: readJsonRule(item, rule.ruleSearch?.kind, source, targetUrl).split(/[,,\s]+/).filter(Boolean), })); }); + } else if (rule.ruleSearch.bookList.trim().startsWith(':')) { + const items = readAllInOneList(html, rule.ruleSearch.bookList); + pageCount = items.length; + items.forEach((item) => { + const detailHref = readRegexItem(item, rule.ruleSearch?.bookUrl, targetUrl); + const title = readRegexItem(item, rule.ruleSearch?.name, targetUrl); + if (!title && !detailHref) return; + const cover = readRegexItem(item, rule.ruleSearch?.coverUrl, targetUrl); + const dedupeKey = detailHref || `${title}|${cover}`; + if (dedupeKey && seen.has(dedupeKey)) return; + if (dedupeKey) seen.add(dedupeKey); + results.push(makeItem(source, { + id: detailHref || undefined, + title, + author: readRegexItem(item, rule.ruleSearch?.author, targetUrl), + summary: readRegexItem(item, rule.ruleSearch?.intro, targetUrl), + cover: cover || undefined, + detailHref, + tags: readRegexItem(item, rule.ruleSearch?.kind, targetUrl).split(/[,,\s]+/).filter(Boolean), + })); + }); } else { const $ = cheerio.load(html); const items = selectElements($, $.root(), rule.ruleSearch.bookList); @@ -722,8 +1053,8 @@ export class LegadoClient { const target = decodeExploreTarget(href) || null; const navigation = categories.map((item) => ({ title: item.title, - href: encodeExploreTarget({ title: item.title, template: item.template, page: 1 }), - rel: 'legado:explore', + href: item.template.startsWith('__group__:') ? '' : encodeExploreTarget({ title: item.title, template: item.template, page: 1 }), + rel: item.template.startsWith('__group__:') ? 'legado:group' : 'legado:explore', type: 'application/x-legado-explore', })); @@ -731,6 +1062,9 @@ export class LegadoClient { return { sourceId: source.id, sourceName: source.name, title: source.name, subtitle: '请选择分类', href: href || source.url, entries: [], navigation }; } + if (target.template.startsWith('__group__:')) { + return { sourceId: source.id, sourceName: source.name, title: target.title, href: href || '', entries: [], navigation }; + } const targetUrl = buildExploreTargetUrl(source, target); const html = await fetchText(source, targetUrl); const exploreRule = rule.ruleExplore || rule.ruleSearch; @@ -823,12 +1157,15 @@ export class LegadoClient { if (detailHref && rule.ruleBookInfo) { const targetUrl = rule.bookInfoUrl ? buildUrlFromTemplate(rule.bookInfoUrl, source, undefined, 1, detailHref).replace(/\{bookUrl\}/g, encodeURIComponent(detailHref)) : detailHref; const html = await fetchText(source, targetUrl); - const json = parseJsonMaybe(html); - const $ = json ? null : cheerio.load(html); + const initData = rule.bookInfoInit?.trim().startsWith(':') ? readAllInOneList(html, rule.bookInfoInit)[0] : null; + const json = initData || parseJsonMaybe(html); + const $ = json && initData ? null : json ? null : cheerio.load(html); const root = $?.root(); - const read = (itemRule?: string) => json - ? readJsonRule(json, itemRule, source, targetUrl) - : readValue($ as cheerio.CheerioAPI, root as cheerio.Cheerio, itemRule, targetUrl); + const read = (itemRule?: string) => initData + ? readRegexItem(initData, itemRule, targetUrl) + : json + ? readJsonRule(json, itemRule, source, targetUrl) + : readValue($ as cheerio.CheerioAPI, root as cheerio.Cheerio, itemRule, targetUrl); const tocUrl = read(rule.ruleBookInfo.tocUrl) || (rule.tocUrl ? buildUrlFromTemplate(rule.tocUrl, source, undefined, 1, detailHref).replace(/\{bookUrl\}/g, encodeURIComponent(detailHref)) : targetUrl); const cover = read(rule.ruleBookInfo.coverUrl) || fallback?.cover; const title = read(rule.ruleBookInfo.name) || fallback?.title || '未命名电子书'; @@ -908,6 +1245,27 @@ export class LegadoClient { chapters.push({ id: stableId(`${source.id}|${normalizedHref}`), title, href: normalizedHref, order: index }); }); } + if (rule.ruleToc.nextTocUrl) { + let nextTocUrl = contentFromRule(html, rule.ruleToc.nextTocUrl, targetUrl); + const visited = new Set([targetUrl]); + for (let page = 0; page < 8 && nextTocUrl; page += 1) { + const normalizedNext = normalizeUrl(targetUrl, nextTocUrl); + if (!normalizedNext || visited.has(normalizedNext)) break; + visited.add(normalizedNext); + const nextHtml = await fetchText(source, normalizedNext); + const $next = cheerio.load(nextHtml); + const nextItems = selectElements($next, $next.root(), rule.ruleToc.chapterList); + nextItems.each((index, element) => { + const root = $next(element); + const title = readValue($next, root, rule.ruleToc?.chapterName, normalizedNext) || `第 ${chapters.length + index + 1} 章`; + const href = readValue($next, root, rule.ruleToc?.chapterUrl, normalizedNext) || root.attr('href') || ''; + if (!href) return; + const normalizedHref = normalizeUrl(normalizedNext, href); + chapters.push({ id: stableId(`${source.id}|${normalizedHref}`), title, href: normalizedHref, order: chapters.length }); + }); + nextTocUrl = contentFromRule(nextHtml, rule.ruleToc.nextTocUrl, normalizedNext); + } + } tocCache.set(cacheKey, { data: chapters, expiresAt: Date.now() + cacheTTL }); return chapters; } @@ -941,7 +1299,7 @@ export class LegadoClient { id: stableId(`${source.id}|${targetUrl}`), title: index >= 0 ? chapters[index].title : '', href: targetUrl, - content: proxyChapterImages(cleanContent(rawContent), source), + content: proxyChapterImages(cleanContent(rule.ruleContent.sourceRegex ? applyRuleFilters(rawContent, [rule.ruleContent.sourceRegex, '']) : rawContent), source), previousHref: index > 0 ? chapters[index - 1].href : undefined, nextHref: index >= 0 && index + 1 < chapters.length ? chapters[index + 1].href : undefined, };