[enh] theme/simple: custom router

Lay the foundation for loading scripts granularly depending on the endpoint it's
on.

Remove vendor specific prefixes as there are now managed by browserslist and
LightningCSS.

Enabled quite a few rules in Biome that don't come in recommended to better
catch issues and improve consistency.

Related:

- https://github.com/searxng/searxng/pull/5073#discussion_r2256037965
- https://github.com/searxng/searxng/pull/5073#discussion_r2256057100
This commit is contained in:
Ivan Gabaldon 2025-07-06 12:27:28 +02:00 committed by Markus Heiser
parent adc4361eb9
commit 60bd8b90f0
28 changed files with 1109 additions and 1039 deletions

View file

@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
"$schema": "https://biomejs.dev/schemas/2.1.3/schema.json",
"files": {
"includes": ["**", "!dist/**", "!node_modules/**"],
"ignoreUnknown": true
@ -28,7 +28,116 @@
"linter": {
"enabled": true,
"rules": {
"recommended": true
"recommended": true,
"complexity": {
"noForEach": "error",
"useSimplifiedLogicExpression": "error"
},
"correctness": {
"noUndeclaredVariables": {
"level": "error",
"options": {
"checkTypes": true
}
},
"useImportExtensions": "error"
},
"nursery": {
"noAwaitInLoop": "warn",
"noBitwiseOperators": "warn",
"noConstantBinaryExpression": "warn",
"noGlobalDirnameFilename": "warn",
"noImplicitCoercion": "warn",
"noMisusedPromises": "warn",
"noUnassignedVariables": "warn",
"noUselessBackrefInRegex": "warn",
"noUselessEscapeInString": "warn",
"noUselessUndefined": "warn",
"useAdjacentGetterSetter": "warn",
"useConsistentObjectDefinition": {
"level": "warn",
"options": {
"syntax": "explicit"
}
},
"useConsistentResponse": "warn",
"useExhaustiveSwitchCases": "warn",
"useExplicitType": "warn",
"useIndexOf": "warn",
"useIterableCallbackReturn": "warn",
"useJsonImportAttribute": "warn",
"useNumericSeparators": "warn",
"useObjectSpread": "warn",
"useParseIntRadix": "warn",
"useReadonlyClassProperties": "warn",
"useSingleJsDocAsterisk": "warn",
"useUnifiedTypeSignature": "warn"
},
"performance": {
"noBarrelFile": "error",
"noDelete": "error",
"noNamespaceImport": "error",
"noReExportAll": "error",
"useTopLevelRegex": "error"
},
"style": {
"noCommonJs": "error",
"noEnum": "error",
"noInferrableTypes": "error",
"noNamespace": "error",
"noNegationElse": "error",
"noNestedTernary": "error",
"noParameterAssign": "error",
"noParameterProperties": "error",
"noRestrictedTypes": {
"level": "error",
"options": {
"types": {
"Element": {
"message": "Element is too generic",
"use": "HTMLElement"
}
}
}
},
"noSubstr": "error",
"noUnusedTemplateLiteral": "error",
"noUselessElse": "error",
"noYodaExpression": "error",
"useAsConstAssertion": "error",
"useAtIndex": "error",
"useCollapsedElseIf": "error",
"useCollapsedIf": "error",
"useConsistentArrayType": {
"level": "error",
"options": {
"syntax": "shorthand"
}
},
"useConsistentBuiltinInstantiation": "error",
"useConsistentMemberAccessibility": {
"level": "error",
"options": {
"accessibility": "explicit"
}
},
"useDefaultSwitchClause": "error",
"useExplicitLengthCheck": "error",
"useForOf": "error",
"useNumberNamespace": "error",
"useShorthandAssign": "error",
"useSingleVarDeclarator": "error",
"useThrowNewError": "error",
"useThrowOnlyError": "error",
"useTrimStartEnd": "error"
},
"suspicious": {
"noAlert": "error",
"noEmptyBlockStatements": "error",
"noEvolvingTypes": "error",
"noVar": "error",
"useNumberToFixedDigitsArgument": "error"
}
}
},
"javascript": {

View file

@ -9,27 +9,27 @@
"version": "0.0.0",
"license": "AGPL-3.0",
"dependencies": {
"ionicons": "~8.0.13",
"ionicons": "~8.0.0",
"normalize.css": "8.0.1",
"ol": "~10.6.1",
"ol": "~10.6.0",
"swiped-events": "1.2.0"
},
"devDependencies": {
"@biomejs/biome": "2.1.2",
"@types/node": "~24.0.15",
"browserslist": "~4.25.1",
"browserslist-to-esbuild": "~2.1.1",
"edge.js": "~6.2.1",
"@biomejs/biome": "2.1.3",
"@types/node": "~24.2.0",
"browserslist": "~4.25.0",
"browserslist-to-esbuild": "~2.1.0",
"edge.js": "~6.2.0",
"less": "~4.4.0",
"lightningcss": "~1.30.1",
"sharp": "~0.34.3",
"lightningcss": "~1.30.0",
"sharp": "~0.34.0",
"sort-package-json": "~3.4.0",
"stylelint": "~16.22.0",
"stylelint-config-standard-less": "~3.0.1",
"stylelint-prettier": "~5.0.3",
"stylelint": "~16.23.0",
"stylelint-config-standard-less": "~3.0.0",
"stylelint-prettier": "~5.0.0",
"svgo": "~4.0.0",
"typescript": "~5.8.3",
"vite": "npm:rolldown-vite@~7.0.9",
"typescript": "~5.9.0",
"vite": "npm:rolldown-vite@7.0.12",
"vite-bundle-analyzer": "~1.1.0"
}
},
@ -59,9 +59,9 @@
}
},
"node_modules/@biomejs/biome": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.2.tgz",
"integrity": "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.3.tgz",
"integrity": "sha512-KE/tegvJIxTkl7gJbGWSgun7G6X/n2M6C35COT6ctYrAy7SiPyNvi6JtoQERVK/VRbttZfgGq96j2bFmhmnH4w==",
"dev": true,
"license": "MIT OR Apache-2.0",
"bin": {
@ -75,20 +75,20 @@
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "2.1.2",
"@biomejs/cli-darwin-x64": "2.1.2",
"@biomejs/cli-linux-arm64": "2.1.2",
"@biomejs/cli-linux-arm64-musl": "2.1.2",
"@biomejs/cli-linux-x64": "2.1.2",
"@biomejs/cli-linux-x64-musl": "2.1.2",
"@biomejs/cli-win32-arm64": "2.1.2",
"@biomejs/cli-win32-x64": "2.1.2"
"@biomejs/cli-darwin-arm64": "2.1.3",
"@biomejs/cli-darwin-x64": "2.1.3",
"@biomejs/cli-linux-arm64": "2.1.3",
"@biomejs/cli-linux-arm64-musl": "2.1.3",
"@biomejs/cli-linux-x64": "2.1.3",
"@biomejs/cli-linux-x64-musl": "2.1.3",
"@biomejs/cli-win32-arm64": "2.1.3",
"@biomejs/cli-win32-x64": "2.1.3"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.1.2.tgz",
"integrity": "sha512-leFAks64PEIjc7MY/cLjE8u5OcfBKkcDB0szxsWUB4aDfemBep1WVKt0qrEyqZBOW8LPHzrFMyDl3FhuuA0E7g==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.1.3.tgz",
"integrity": "sha512-LFLkSWRoSGS1wVUD/BE6Nlt2dSn0ulH3XImzg2O/36BoToJHKXjSxzPEMAqT9QvwVtk7/9AQhZpTneERU9qaXA==",
"cpu": [
"arm64"
],
@ -103,9 +103,9 @@
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.1.2.tgz",
"integrity": "sha512-Nmmv7wRX5Nj7lGmz0FjnWdflJg4zii8Ivruas6PBKzw5SJX/q+Zh2RfnO+bBnuKLXpj8kiI2x2X12otpH6a32A==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.1.3.tgz",
"integrity": "sha512-Q/4OTw8P9No9QeowyxswcWdm0n2MsdCwWcc5NcKQQvzwPjwuPdf8dpPPf4r+x0RWKBtl1FLiAUtJvBlri6DnYw==",
"cpu": [
"x64"
],
@ -120,9 +120,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.1.2.tgz",
"integrity": "sha512-NWNy2Diocav61HZiv2enTQykbPP/KrA/baS7JsLSojC7Xxh2nl9IczuvE5UID7+ksRy2e7yH7klm/WkA72G1dw==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.1.3.tgz",
"integrity": "sha512-2hS6LgylRqMFmAZCOFwYrf77QMdUwJp49oe8PX/O8+P2yKZMSpyQTf3Eo5ewnsMFUEmYbPOskafdV1ds1MZMJA==",
"cpu": [
"arm64"
],
@ -137,9 +137,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.1.2.tgz",
"integrity": "sha512-qgHvafhjH7Oca114FdOScmIKf1DlXT1LqbOrrbR30kQDLFPEOpBG0uzx6MhmsrmhGiCFCr2obDamu+czk+X0HQ==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.1.3.tgz",
"integrity": "sha512-KXouFSBnoxAWZYDQrnNRzZBbt5s9UJkIm40hdvSL9mBxSSoxRFQJbtg1hP3aa8A2SnXyQHxQfpiVeJlczZt76w==",
"cpu": [
"arm64"
],
@ -154,9 +154,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.1.2.tgz",
"integrity": "sha512-Km/UYeVowygTjpX6sGBzlizjakLoMQkxWbruVZSNE6osuSI63i4uCeIL+6q2AJlD3dxoiBJX70dn1enjQnQqwA==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.1.3.tgz",
"integrity": "sha512-NxlSCBhLvQtWGagEztfAZ4WcE1AkMTntZV65ZvR+J9jp06+EtOYEBPQndA70ZGhHbEDG57bR6uNvqkd1WrEYVA==",
"cpu": [
"x64"
],
@ -171,9 +171,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.1.2.tgz",
"integrity": "sha512-xlB3mU14ZUa3wzLtXfmk2IMOGL+S0aHFhSix/nssWS/2XlD27q+S6f0dlQ8WOCbYoXcuz8BCM7rCn2lxdTrlQA==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.1.3.tgz",
"integrity": "sha512-KaLAxnROouzIWtl6a0Y88r/4hW5oDUJTIqQorOTVQITaKQsKjZX4XCUmHIhdEk8zMnaiLZzRTAwk1yIAl+mIew==",
"cpu": [
"x64"
],
@ -188,9 +188,9 @@
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.1.2.tgz",
"integrity": "sha512-G8KWZli5ASOXA3yUQgx+M4pZRv3ND16h77UsdunUL17uYpcL/UC7RkWTdkfvMQvogVsAuz5JUcBDjgZHXxlKoA==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.1.3.tgz",
"integrity": "sha512-V9CUZCtWH4u0YwyCYbQ3W5F4ZGPWp2C2TYcsiWFNNyRfmOW1j/TY/jAurl33SaRjgZPO5UUhGyr9m6BN9t84NQ==",
"cpu": [
"arm64"
],
@ -205,9 +205,9 @@
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.1.2.tgz",
"integrity": "sha512-9zajnk59PMpjBkty3bK2IrjUsUHvqe9HWwyAWQBjGLE7MIBjbX2vwv1XPEhmO2RRuGoTkVx3WCanHrjAytICLA==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.1.3.tgz",
"integrity": "sha512-dxy599q6lgp8ANPpR8sDMscwdp9oOumEsVXuVCVT9N2vAho8uYXlCz53JhxX6LtJOXaE73qzgkGQ7QqvFlMC0g==",
"cpu": [
"x64"
],
@ -814,15 +814,15 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
"integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.1.tgz",
"integrity": "sha512-KVlQ/jgywZpixGCKMNwxStmmbYEMyokZpCf2YuIChhfJA2uqfAKNEM8INz7zzTo55iEXfBhIIs3VqYyqzDLj8g==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@emnapi/core": "^1.4.5",
"@emnapi/runtime": "^1.4.5",
"@tybys/wasm-util": "^0.10.0"
}
},
@ -865,9 +865,9 @@
}
},
"node_modules/@oxc-project/runtime": {
"version": "0.77.0",
"resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.77.0.tgz",
"integrity": "sha512-cMbHs/DaomWSjxeJ79G10GA5hzJW9A7CZ+/cO+KuPZ7Trf3Rr07qSLauC4Ns8ba4DKVDjd8VSC9nVLpw6jpoGQ==",
"version": "0.78.0",
"resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.78.0.tgz",
"integrity": "sha512-jOU7sDFMyq5ShGJC21UobalVzqcdtWGfySVp8ELvKoVLzMpLHb4kv1bs9VKxaP8XC7Z9hlAXwEKVhCTN+j21aQ==",
"dev": true,
"license": "MIT",
"engines": {
@ -875,9 +875,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.77.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.77.0.tgz",
"integrity": "sha512-iUQj185VvCPnSba+ltUV5tVDrPX6LeZVtQywnnoGbe4oJ1VKvDKisjGkD/AvVtdm98b/BdsVS35IlJV1m2mBBA==",
"version": "0.78.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.78.0.tgz",
"integrity": "sha512-8FvExh0WRWN1FoSTjah1xa9RlavZcJQ8/yxRbZ7ElmSa2Ij5f5Em7MvRbSthE6FbwC6Wh8iAw0Gpna7QdoqLGg==",
"dev": true,
"license": "MIT",
"funding": {
@ -960,9 +960,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.27.tgz",
"integrity": "sha512-IJL3efUJmvb5MfTEi7bGK4jq3ZFAzVbSy+vmul0DcdrglUd81Tfyy7Zzq2oM0tUgmACG32d8Jz/ykbpbf+3C5A==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.30.tgz",
"integrity": "sha512-4j7QBitb/WMT1fzdJo7BsFvVNaFR5WCQPdf/RPDHEsgQIYwBaHaL47KTZxncGFQDD1UAKN3XScJ0k7LAsZfsvg==",
"cpu": [
"arm64"
],
@ -974,9 +974,9 @@
]
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.27.tgz",
"integrity": "sha512-TXTiuHbtnHfb0c44vNfWfIyEFJ0BFUf63ip9Z4mj8T2zRcZXQYVger4OuAxnwGNGBgDyHo1VaNBG+Vxn2VrpqQ==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.30.tgz",
"integrity": "sha512-4vWFTe1o5LXeitI2lW8qMGRxxwrH/LhKd2HDLa/QPhdxohvdnfKyDZWN96XUhDyje2bHFCFyhMs3ak2lg2mJFA==",
"cpu": [
"arm64"
],
@ -988,9 +988,9 @@
]
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.27.tgz",
"integrity": "sha512-Jpjflgvbolh+fAaaEajPJQCOpZMawYMbNVzuZp3nidX1B7kMAP7NEKp9CWzthoL2Y8RfD7OApN6bx4+vFurTaw==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.30.tgz",
"integrity": "sha512-MxrfodqImbsDFFFU/8LxyFPZjt7s4ht8g2Zb76EmIQ+xlmit46L9IzvWiuMpEaSJ5WbnjO7fCDWwakMGyJJ+Dw==",
"cpu": [
"x64"
],
@ -1002,9 +1002,9 @@
]
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.27.tgz",
"integrity": "sha512-07ZNlXIunyS1jCTnene7aokkzCZNBUnmnJWu4Nz5X5XQvVHJNjsDhPFJTlNmneSDzA3vGkRNwdECKXiDTH/CqA==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.30.tgz",
"integrity": "sha512-c/TQXcATKoO8qE1bCjCOkymZTu7yVUAxBSNLp42Q97XHCb0Cu9v6MjZpB6c7Hq9NQ9NzW44uglak9D/r77JeDw==",
"cpu": [
"x64"
],
@ -1016,9 +1016,9 @@
]
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.27.tgz",
"integrity": "sha512-z74ah00oyKnTUtaIbg34TaIU1PYM8tGE1bK6aUs8OLZ9sWW4g3Xo5A0nit2zyeanmYFvrAUxnt3Bpk+mTZCtlg==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.30.tgz",
"integrity": "sha512-Vxci4xylM11zVqvrmezAaRjGBDyOlMRtlt7TDgxaBmSYLuiokXbZpD8aoSuOyjUAeN0/tmWItkxNGQza8UWGNQ==",
"cpu": [
"arm"
],
@ -1030,9 +1030,9 @@
]
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.27.tgz",
"integrity": "sha512-b9oKl/M5OIyAcosS73BmjOZOjvcONV97t2SnKpgwfDX/mjQO3dBgTYyvHMFA6hfhIDW1+2XVQR/k5uzBULFhoA==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.30.tgz",
"integrity": "sha512-iEBEdSs25Ol0lXyVNs763f7YPAIP0t1EAjoXME81oJ94DesJslaLTj71Rn1shoMDVA+dfkYA286w5uYnOs9ZNA==",
"cpu": [
"arm64"
],
@ -1044,9 +1044,9 @@
]
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.27.tgz",
"integrity": "sha512-RmaNSkVmAH8u/r5Q+v4O0zL4HY8pLrvlM5wBoBrb/QHDQgksGKBqhecpg1ERER0Q7gMh/GJUz6JiiD55Q+9UOA==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.30.tgz",
"integrity": "sha512-Ny684Sn1X8c+gGLuDlxkOuwiEE3C7eEOqp1/YVBzQB4HO7U/b4n7alvHvShboOEY5DP1fFUjq6Z+sBLYlCIZbQ==",
"cpu": [
"arm64"
],
@ -1058,9 +1058,9 @@
]
},
"node_modules/@rolldown/binding-linux-arm64-ohos": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-ohos/-/binding-linux-arm64-ohos-1.0.0-beta.27.tgz",
"integrity": "sha512-gq78fI/g0cp1UKFMk53kP/oZAgYOXbaqdadVMuCJc0CoSkDJcpO2YIasRs/QYlE91QWfcHD5RZl9zbf4ksTS/w==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-ohos/-/binding-linux-arm64-ohos-1.0.0-beta.30.tgz",
"integrity": "sha512-6moyULHDPKwt5RDEV72EqYw5n+s46AerTwtEBau5wCsZd1wuHS1L9z6wqhKISXAFTK9sneN0TEjvYKo+sgbbiA==",
"cpu": [
"arm64"
],
@ -1072,9 +1072,9 @@
]
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.27.tgz",
"integrity": "sha512-yS/GreJ6BT44dHu1WLigc50S8jZA+pDzzsf8tqRptUTwi5YW7dX3NqcDlc/lXsZqu57aKynLljgClYAm90LEKw==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.30.tgz",
"integrity": "sha512-p0yoPdoGg5Ow2YZKKB5Ypbn58i7u4XFk3PvMkriFnEcgtVk40c5u7miaX7jH0JdzahyXVBJ/KT5yEpJrzQn8yg==",
"cpu": [
"x64"
],
@ -1086,9 +1086,9 @@
]
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.27.tgz",
"integrity": "sha512-6FV9To1sXewGHY4NaCPeOE5p5o1qfuAjj+m75WVIPw9HEJVsQoC5QiTL5wWVNqSMch4X0eWnQ6WsQolU6sGMIA==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.30.tgz",
"integrity": "sha512-sM/KhCrsT0YdHX10mFSr0cvbfk1+btG6ftepAfqhbcDfhi0s65J4dTOxGmklJnJL9i1LXZ8WA3N4wmnqsfoK8Q==",
"cpu": [
"x64"
],
@ -1100,9 +1100,9 @@
]
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.27.tgz",
"integrity": "sha512-VcxdhF0PQda9krFJHw4DqUkdAsHWYs/Uz/Kr/zhU8zMFDzmK6OdUgl9emGj9wTzXAEHYkAMDhk+OJBRJvp424g==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.30.tgz",
"integrity": "sha512-i3kD5OWs8PQP0V+JW3TFyCLuyjuNzrB45em0g84Jc+gvnDsGVlzVjMNPo7txE/yT8CfE90HC/lDs3ry9FvaUyw==",
"cpu": [
"wasm32"
],
@ -1110,16 +1110,16 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@napi-rs/wasm-runtime": "^0.2.12"
"@napi-rs/wasm-runtime": "^1.0.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.27.tgz",
"integrity": "sha512-3bXSARqSf8jLHrQ1/tw9pX1GwIR9jA6OEsqTgdC0DdpoZ+34sbJXE9Nse3dQ0foGLKBkh4PqDv/rm2Thu9oVBw==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.30.tgz",
"integrity": "sha512-q7mrYln30V35VrCqnBVQQvNPQm8Om9HC59I3kMYiOWogvJobzSPyO+HA1MP363+Qgwe39I2I1nqBKPOtWZ33AQ==",
"cpu": [
"arm64"
],
@ -1131,9 +1131,9 @@
]
},
"node_modules/@rolldown/binding-win32-ia32-msvc": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.27.tgz",
"integrity": "sha512-xPGcKb+W8NIWAf5KApsUIrhiKH5NImTarICge5jQ2m0BBxD31crio4OXy/eYVq5CZkqkqszLQz2fWZcWNmbzlQ==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.30.tgz",
"integrity": "sha512-nUqGBt39XTpbBEREEnyKofdP3uz+SN/x2884BH+N3B2NjSUrP6NXwzltM35C0wKK42hX/nthRrwSgj715m99Jw==",
"cpu": [
"ia32"
],
@ -1145,9 +1145,9 @@
]
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.27.tgz",
"integrity": "sha512-3y1G8ARpXBAcz4RJM5nzMU6isS/gXZl8SuX8lS2piFOnQMiOp6ajeelnciD+EgG4ej793zvNvr+WZtdnao2yrw==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.30.tgz",
"integrity": "sha512-lbnvUwAXIVWSXAeZrCa4b1KvV/DW0rBnMHuX0T7I6ey1IsXZ90J37dEgt3j48Ex1Cw1E+5H7VDNP2gyOX8iu3w==",
"cpu": [
"x64"
],
@ -1159,9 +1159,9 @@
]
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.30.tgz",
"integrity": "sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw==",
"dev": true,
"license": "MIT"
},
@ -1270,9 +1270,9 @@
]
},
"node_modules/@stencil/core": {
"version": "4.36.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.1.tgz",
"integrity": "sha512-LRZN1c4X+9/7kR+zG74SrcZ6XxKlilDTkDXajw3ioeDdVlJEvW5wU8Wn3BcAAnk7fjrgLZVN7ickgeuG7u0AKg==",
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"license": "MIT",
"bin": {
"stencil": "bin/stencil"
@ -1311,13 +1311,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.0.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz",
"integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==",
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz",
"integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.8.0"
"undici-types": "~7.10.0"
}
},
"node_modules/@types/pluralize": {
@ -1526,14 +1526,14 @@
}
},
"node_modules/cacheable": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.2.tgz",
"integrity": "sha512-hMkETCRV4hwBAvjQY1/xGw15tlPj+7cM4d5HOlYJJFftLQVRCboVX+mT6AJ6eL0fsqUhSUwDiF+pgfTR2r2Hxg==",
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.3.tgz",
"integrity": "sha512-M6p10iJ/VT0wT7TLIGUnm958oVrU2cUK8pQAVU21Zu7h8rbk/PeRtRWrvHJBql97Bhzk3g1N6+2VKC+Rjxna9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"hookified": "^1.10.0",
"keyv": "^5.3.4"
"keyv": "^5.4.0"
}
},
"node_modules/callsites": {
@ -1547,9 +1547,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"version": "1.0.30001731",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
"dev": true,
"funding": [
{
@ -1986,9 +1986,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.187",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz",
"integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==",
"version": "1.5.197",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.197.tgz",
"integrity": "sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==",
"dev": true,
"license": "ISC"
},
@ -2145,13 +2145,13 @@
}
},
"node_modules/file-entry-cache": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.1.tgz",
"integrity": "sha512-zcmsHjg2B2zjuBgjdnB+9q0+cWcgWfykIcsDkWDB4GTPtl1eXUA+gTI6sO0u01AqK3cliHryTU55/b2Ow1hfZg==",
"version": "10.1.3",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.3.tgz",
"integrity": "sha512-D+w75Ub8T55yor7fPgN06rkCAUbAYw2vpxJmmjv/GDAcvCnv9g7IvHhIZoxzRZThrXPFI2maeY24pPbtyYU7Lg==",
"dev": true,
"license": "MIT",
"dependencies": {
"flat-cache": "^6.1.10"
"flat-cache": "^6.1.12"
}
},
"node_modules/fill-range": {
@ -2168,13 +2168,13 @@
}
},
"node_modules/flat-cache": {
"version": "6.1.11",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.11.tgz",
"integrity": "sha512-zfOAns94mp7bHG/vCn9Ru2eDCmIxVQ5dELUHKjHfDEOJmHNzE+uGa6208kfkgmtym4a0FFjEuFksCXFacbVhSg==",
"version": "6.1.12",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.12.tgz",
"integrity": "sha512-U+HqqpZPPXP5d24bWuRzjGqVqUcw64k4nZAbruniDwdRg0H10tvN7H6ku1tjhA4rg5B9GS3siEvwO2qjJJ6f8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"cacheable": "^1.10.1",
"cacheable": "^1.10.3",
"flatted": "^3.3.3",
"hookified": "^1.10.0"
}
@ -2355,9 +2355,9 @@
}
},
"node_modules/hookified": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.10.0.tgz",
"integrity": "sha512-dJw0492Iddsj56U1JsSTm9E/0B/29a1AuoSLRAte8vQg/kaTGF3IgjEWT8c8yG4cC10+HisE1x5QAwR0Xwc+DA==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.11.0.tgz",
"integrity": "sha512-aDdIN3GyU5I6wextPplYdfmWCo+aLmjjVbntmX6HLD5RCi/xKsivYEBhnRD+d9224zFf008ZpLMPlWF0ZodYZw==",
"dev": true,
"license": "MIT"
},
@ -2594,9 +2594,9 @@
"license": "MIT"
},
"node_modules/keyv": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.4.0.tgz",
"integrity": "sha512-TMckyVjEoacG5IteUpUrOBsFORtheqziVyyY2dLUwg1jwTb8u48LX4TgmtogkNl9Y9unaEJ1luj10fGyjMGFOQ==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.0.tgz",
"integrity": "sha512-QG7qR2tijh1ftOvClut4YKKg1iW6cx3GZsKoGyJPxHkGWK9oJhG9P3j5deP0QQOGDowBMVQFaP+Vm4NpGYvmIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3445,35 +3445,35 @@
}
},
"node_modules/rolldown": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.27.tgz",
"integrity": "sha512-aYiJmzKoUHoaaEZLRegYVfZkXW7gzdgSbq+u5cXQ6iXc/y8tnQ3zGffQo44Pr1lTKeLluw3bDIDUCx/NAzqKeA==",
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.30.tgz",
"integrity": "sha512-H/LmDTUPlm65hWOTjXvd1k0qrGinNi8LrG3JsHVm6Oit7STg0upBmgoG5PZUHbAnGTHr0MLoLyzjmH261lIqSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/runtime": "=0.77.0",
"@oxc-project/types": "=0.77.0",
"@rolldown/pluginutils": "1.0.0-beta.27",
"@oxc-project/runtime": "=0.78.0",
"@oxc-project/types": "=0.78.0",
"@rolldown/pluginutils": "1.0.0-beta.30",
"ansis": "^4.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-beta.27",
"@rolldown/binding-darwin-arm64": "1.0.0-beta.27",
"@rolldown/binding-darwin-x64": "1.0.0-beta.27",
"@rolldown/binding-freebsd-x64": "1.0.0-beta.27",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.27",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.27",
"@rolldown/binding-linux-arm64-musl": "1.0.0-beta.27",
"@rolldown/binding-linux-arm64-ohos": "1.0.0-beta.27",
"@rolldown/binding-linux-x64-gnu": "1.0.0-beta.27",
"@rolldown/binding-linux-x64-musl": "1.0.0-beta.27",
"@rolldown/binding-wasm32-wasi": "1.0.0-beta.27",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.27",
"@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.27",
"@rolldown/binding-win32-x64-msvc": "1.0.0-beta.27"
"@rolldown/binding-android-arm64": "1.0.0-beta.30",
"@rolldown/binding-darwin-arm64": "1.0.0-beta.30",
"@rolldown/binding-darwin-x64": "1.0.0-beta.30",
"@rolldown/binding-freebsd-x64": "1.0.0-beta.30",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.30",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.30",
"@rolldown/binding-linux-arm64-musl": "1.0.0-beta.30",
"@rolldown/binding-linux-arm64-ohos": "1.0.0-beta.30",
"@rolldown/binding-linux-x64-gnu": "1.0.0-beta.30",
"@rolldown/binding-linux-x64-musl": "1.0.0-beta.30",
"@rolldown/binding-wasm32-wasi": "1.0.0-beta.30",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.30",
"@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.30",
"@rolldown/binding-win32-x64-msvc": "1.0.0-beta.30"
}
},
"node_modules/run-parallel": {
@ -3778,9 +3778,9 @@
}
},
"node_modules/stylelint": {
"version": "16.22.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.22.0.tgz",
"integrity": "sha512-SVEMTdjKNV4ollUrIY9ordZ36zHv2/PHzPjfPMau370MlL2VYXeLgSNMMiEbLGRO8RmD2R8/BVUeF2DfnfkC0w==",
"version": "16.23.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.23.0.tgz",
"integrity": "sha512-69T5aS2LUY306ekt1Q1oaSPwz/jaG9HjyMix3UMrai1iEbuOafBe2Dh8xlyczrxFAy89qcKyZWWtc42XLx3Bbw==",
"dev": true,
"funding": [
{
@ -3807,7 +3807,7 @@
"debug": "^4.4.1",
"fast-glob": "^3.3.3",
"fastest-levenshtein": "^1.0.16",
"file-entry-cache": "^10.1.1",
"file-entry-cache": "^10.1.3",
"global-modules": "^2.0.0",
"globby": "^11.1.0",
"globjoin": "^0.1.4",
@ -4110,9 +4110,9 @@
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@ -4124,9 +4124,9 @@
}
},
"node_modules/undici-types": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"dev": true,
"license": "MIT"
},
@ -4170,17 +4170,17 @@
},
"node_modules/vite": {
"name": "rolldown-vite",
"version": "7.0.9",
"resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.0.9.tgz",
"integrity": "sha512-RxVP6CY9CNCEM9UecdytqeADxOGSjgkfSE/eI986sM7I3/F09lQ9UfQo3y6W10ICBppKsEHe71NbCX/tirYDFg==",
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.0.12.tgz",
"integrity": "sha512-Gr40FRnE98FwPJcMwcJgBwP6U7Qxw/VEtDsFdFjvGUTdgI/tTmF7z7dbVo/ajItM54G+Zo9w5BIrUmat6MbuWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.4.6",
"lightningcss": "^1.30.1",
"picomatch": "^4.0.2",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rolldown": "1.0.0-beta.27",
"rolldown": "1.0.0-beta.30",
"tinyglobby": "^0.2.14"
},
"bin": {

View file

@ -25,27 +25,27 @@
"not dead"
],
"dependencies": {
"ionicons": "~8.0.13",
"ionicons": "~8.0.0",
"normalize.css": "8.0.1",
"ol": "~10.6.1",
"ol": "~10.6.0",
"swiped-events": "1.2.0"
},
"devDependencies": {
"@biomejs/biome": "2.1.2",
"@types/node": "~24.0.15",
"browserslist": "~4.25.1",
"browserslist-to-esbuild": "~2.1.1",
"edge.js": "~6.2.1",
"@biomejs/biome": "2.1.3",
"@types/node": "~24.2.0",
"browserslist": "~4.25.0",
"browserslist-to-esbuild": "~2.1.0",
"edge.js": "~6.2.0",
"less": "~4.4.0",
"lightningcss": "~1.30.1",
"sharp": "~0.34.3",
"lightningcss": "~1.30.0",
"sharp": "~0.34.0",
"sort-package-json": "~3.4.0",
"stylelint": "~16.22.0",
"stylelint-config-standard-less": "~3.0.1",
"stylelint-prettier": "~5.0.3",
"stylelint": "~16.23.0",
"stylelint-config-standard-less": "~3.0.0",
"stylelint-prettier": "~5.0.0",
"svgo": "~4.0.0",
"typescript": "~5.8.3",
"vite": "npm:rolldown-vite@~7.0.9",
"typescript": "~5.9.0",
"vite": "npm:rolldown-vite@7.0.12",
"vite-bundle-analyzer": "~1.1.0"
}
}

View file

@ -4,10 +4,6 @@
* @license AGPL-3.0-or-later
*/
import "./00_toolkit.ts";
import "./infinite_scroll.ts";
import "./keyboard.ts";
import "./mapresult.ts";
import "./preferences.ts";
import "./results.ts";
import "./search.ts";
import "./router.ts";
import "./toolkit.ts";
import "./listener.ts";

View file

@ -0,0 +1,5 @@
import { listen } from "./toolkit.ts";
listen("click", ".close", function (this: HTMLElement) {
(this.parentNode as HTMLElement)?.classList.add("invisible");
});

View file

@ -0,0 +1,38 @@
import { Endpoints, endpoint, ready, settings } from "./toolkit.ts";
ready(
() => {
import("../main/keyboard.ts");
import("../main/search.ts");
if (settings.autocomplete) {
import("../main/autocomplete.ts");
}
},
{ on: [endpoint === Endpoints.index] }
);
ready(
() => {
import("../main/keyboard.ts");
import("../main/mapresult.ts");
import("../main/results.ts");
import("../main/search.ts");
if (settings.infinite_scroll) {
import("../main/infinite_scroll.ts");
}
if (settings.autocomplete) {
import("../main/autocomplete.ts");
}
},
{ on: [endpoint === Endpoints.results] }
);
ready(
() => {
import("../main/preferences.ts");
},
{ on: [endpoint === Endpoints.preferences] }
);

View file

@ -0,0 +1,140 @@
import type { KeyBindingLayout } from "../main/keyboard.ts";
// synced with searx/webapp.py get_client_settings
type Settings = {
advanced_search?: boolean;
autocomplete?: string;
autocomplete_min?: number;
doi_resolver?: string;
favicon_resolver?: string;
hotkeys?: KeyBindingLayout;
infinite_scroll?: boolean;
method?: "GET" | "POST";
query_in_title?: boolean;
results_on_new_tab?: boolean;
safesearch?: 0 | 1 | 2;
search_on_category_select?: boolean;
theme?: string;
theme_static_path?: string;
translations?: Record<string, string>;
url_formatting?: "pretty" | "full" | "host";
};
type HTTPOptions = {
body?: BodyInit;
timeout?: number;
};
type ReadyOptions = {
// all values must be truthy for the callback to be executed
on?: (boolean | undefined)[];
};
type AssertElement = (element?: HTMLElement | null) => asserts element is HTMLElement;
export type EndpointsKeys = keyof typeof Endpoints;
export const Endpoints = {
index: "index",
results: "results",
preferences: "preferences",
unknown: "unknown"
} as const;
export const mutable = {
closeDetail: undefined as (() => void) | undefined,
scrollPageToSelected: undefined as (() => void) | undefined,
selectImage: undefined as ((resultElement: HTMLElement) => void) | undefined,
selectNext: undefined as ((openDetailView?: boolean) => void) | undefined,
selectPrevious: undefined as ((openDetailView?: boolean) => void) | undefined
};
const getEndpoint = (): EndpointsKeys => {
const metaEndpoint = document.querySelector('meta[name="endpoint"]')?.getAttribute("content");
if (metaEndpoint && metaEndpoint in Endpoints) {
return metaEndpoint as EndpointsKeys;
}
return Endpoints.unknown;
};
const getSettings = (): Settings => {
const settings = document.querySelector("script[client_settings]")?.getAttribute("client_settings");
if (!settings) return {};
try {
return JSON.parse(atob(settings));
} catch (error) {
console.error("Failed to load client_settings:", error);
return {};
}
};
export const assertElement: AssertElement = (element?: HTMLElement | null): asserts element is HTMLElement => {
if (!element) {
throw new Error("Bad assertion: DOM element not found");
}
};
export const http = async (method: string, url: string | URL, options?: HTTPOptions): Promise<Response> => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), options?.timeout ?? 30_000);
const res = await fetch(url, {
body: options?.body,
method: method,
signal: controller.signal
}).finally(() => clearTimeout(timeoutId));
if (!res.ok) {
throw new Error(res.statusText);
}
return res;
};
export const listen = <K extends keyof DocumentEventMap, E extends HTMLElement>(
type: string | K,
target: string | Document | E,
listener: (this: E, event: DocumentEventMap[K]) => void | Promise<void>,
options?: AddEventListenerOptions
): void => {
if (typeof target !== "string") {
target.addEventListener(type, listener as EventListener, options);
return;
}
document.addEventListener(
type,
(event: Event) => {
for (const node of event.composedPath()) {
if (node instanceof HTMLElement && node.matches(target)) {
try {
listener.call(node as E, event as DocumentEventMap[K]);
} catch (error) {
console.error(error);
}
break;
}
}
},
options
);
};
export const ready = (callback: () => void, options?: ReadyOptions): void => {
for (const condition of options?.on ?? []) {
if (!condition) {
return;
}
}
if (document.readyState !== "loading") {
callback();
} else {
listen("DOMContentLoaded", document, callback, { once: true });
}
};
export const endpoint: EndpointsKeys = getEndpoint();
export const settings: Settings = getSettings();

View file

@ -1,118 +0,0 @@
import type { KeyBindingLayout } from "./keyboard.ts";
type Settings = {
theme_static_path?: string;
method?: string;
hotkeys?: KeyBindingLayout;
infinite_scroll?: boolean;
autocomplete?: boolean;
autocomplete_min?: number;
search_on_category_select?: boolean;
translations?: Record<string, string>;
[key: string]: unknown;
};
type ReadyOptions = {
// all values must be truthy for the callback to be executed
on?: (boolean | undefined)[];
};
const getEndpoint = (): string => {
const endpointClass = Array.from(document.body.classList).find((className) => className.endsWith("_endpoint"));
return endpointClass?.split("_")[0] ?? "";
};
const getSettings = (): Settings => {
const settings = document.querySelector("script[client_settings]")?.getAttribute("client_settings");
if (!settings) return {};
try {
return JSON.parse(atob(settings));
} catch (error) {
console.error("Failed to load client_settings:", error);
return {};
}
};
type AssertElement = (element?: Element | null) => asserts element is Element;
export const assertElement: AssertElement = (element?: Element | null): asserts element is Element => {
if (!element) {
throw new Error("Bad assertion: DOM element not found");
}
};
export const searxng = {
// dynamic functions
closeDetail: undefined as (() => void) | undefined,
scrollPageToSelected: undefined as (() => void) | undefined,
selectImage: undefined as ((resultElement: Element) => void) | undefined,
selectNext: undefined as ((openDetailView?: boolean) => void) | undefined,
selectPrevious: undefined as ((openDetailView?: boolean) => void) | undefined,
endpoint: getEndpoint(),
http: async (method: string, url: string | URL, data?: BodyInit): Promise<Response> => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
const res = await fetch(url, {
body: data,
method,
signal: controller.signal
}).finally(() => clearTimeout(timeoutId));
if (!res.ok) {
throw new Error(res.statusText);
}
return res;
},
listen: <K extends keyof DocumentEventMap, E extends Element>(
type: string | K,
target: string | Document | E,
listener: (this: E, event: DocumentEventMap[K]) => void,
options?: AddEventListenerOptions
): void => {
if (typeof target !== "string") {
target.addEventListener(type, listener as EventListener, options);
return;
}
document.addEventListener(
type,
(event: Event) => {
for (const node of event.composedPath()) {
if (node instanceof Element && node.matches(target)) {
try {
listener.call(node as E, event as DocumentEventMap[K]);
} catch (error) {
console.error(error);
}
break;
}
}
},
options
);
},
ready: (callback: () => void, options?: ReadyOptions): void => {
for (const condition of options?.on ?? []) {
if (!condition) {
return;
}
}
if (document.readyState !== "loading") {
callback();
} else {
searxng.listen("DOMContentLoaded", document, callback, { once: true });
}
},
settings: getSettings()
};
searxng.listen("click", ".close", function (this: Element) {
(this.parentNode as Element)?.classList.add("invisible");
});

View file

@ -0,0 +1,129 @@
import { assertElement, http, listen, settings } from "../core/toolkit.ts";
const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
try {
let res: Response;
if (settings.method === "GET") {
res = await http("GET", `./autocompleter?q=${query}`);
} else {
res = await http("POST", "./autocompleter", { body: new URLSearchParams({ q: query }) });
}
const results = await res.json();
const autocomplete = document.querySelector<HTMLElement>(".autocomplete");
assertElement(autocomplete);
const autocompleteList = document.querySelector<HTMLUListElement>(".autocomplete ul");
assertElement(autocompleteList);
autocomplete.classList.add("open");
autocompleteList.replaceChildren();
// show an error message that no result was found
if (results?.[1]?.length === 0) {
const noItemFoundMessage = Object.assign(document.createElement("li"), {
className: "no-item-found",
textContent: settings.translations?.no_item_found ?? "No results found"
});
autocompleteList.append(noItemFoundMessage);
return;
}
const fragment = new DocumentFragment();
for (const result of results[1]) {
const li = Object.assign(document.createElement("li"), { textContent: result });
listen("mousedown", li, () => {
qInput.value = result;
const form = document.querySelector<HTMLFormElement>("#search");
form?.submit();
autocomplete.classList.remove("open");
});
fragment.append(li);
}
autocompleteList.append(fragment);
} catch (error) {
console.error("Error fetching autocomplete results:", error);
}
};
const qInput = document.getElementById("q") as HTMLInputElement | null;
assertElement(qInput);
let timeoutId: number;
listen("input", qInput, () => {
clearTimeout(timeoutId);
const query = qInput.value;
const minLength = settings.autocomplete_min ?? 2;
if (query.length < minLength) return;
timeoutId = window.setTimeout(async () => {
if (query === qInput.value) {
await fetchResults(qInput, query);
}
}, 300);
});
const autocomplete: HTMLElement | null = document.querySelector<HTMLElement>(".autocomplete");
const autocompleteList: HTMLUListElement | null = document.querySelector<HTMLUListElement>(".autocomplete ul");
if (autocompleteList) {
listen("keyup", qInput, (event: KeyboardEvent) => {
const listItems = [...autocompleteList.children] as HTMLElement[];
const currentIndex = listItems.findIndex((item) => item.classList.contains("active"));
let newCurrentIndex = -1;
switch (event.key) {
case "ArrowUp": {
const currentItem = listItems[currentIndex];
if (currentItem && currentIndex >= 0) {
currentItem.classList.remove("active");
}
// we need to add listItems.length to the index calculation here because the JavaScript modulos
// operator doesn't work with negative numbers
newCurrentIndex = (currentIndex - 1 + listItems.length) % listItems.length;
break;
}
case "ArrowDown": {
const currentItem = listItems[currentIndex];
if (currentItem && currentIndex >= 0) {
currentItem.classList.remove("active");
}
newCurrentIndex = (currentIndex + 1) % listItems.length;
break;
}
case "Tab":
case "Enter":
if (autocomplete) {
autocomplete.classList.remove("open");
}
break;
default:
break;
}
if (newCurrentIndex !== -1) {
const selectedItem = listItems[newCurrentIndex];
if (selectedItem) {
selectedItem.classList.add("active");
if (!selectedItem.classList.contains("no-item-found")) {
const qInput = document.getElementById("q") as HTMLInputElement | null;
if (qInput) {
qInput.value = selectedItem.textContent ?? "";
}
}
}
}
});
}

View file

@ -1,4 +1,4 @@
import { assertElement, searxng } from "./00_toolkit";
import { assertElement, http, settings } from "../core/toolkit.ts";
const newLoadSpinner = (): HTMLDivElement => {
return Object.assign(document.createElement("div"), {
@ -13,12 +13,9 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise<
const form = document.querySelector<HTMLFormElement>("#pagination form.next_page");
assertElement(form);
const formData = new FormData(form);
const action = searchForm.getAttribute("action");
if (!action) {
console.error("Form action not found");
return;
throw new Error("Form action not defined");
}
const paginationElement = document.querySelector<HTMLElement>("#pagination");
@ -27,7 +24,7 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise<
paginationElement.replaceChildren(newLoadSpinner());
try {
const res = await searxng.http("POST", action, formData);
const res = await http("POST", action, { body: new FormData(form) });
const nextPage = await res.text();
if (!nextPage) return;
@ -39,8 +36,7 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise<
const urlsElement = document.querySelector<HTMLElement>("#urls");
if (!urlsElement) {
console.error("URLs element not found");
return;
throw new Error("URLs element not found");
}
if (articleList.length > 0 && !onlyImages) {
@ -59,7 +55,7 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise<
console.error("Error loading next page:", error);
const errorElement = Object.assign(document.createElement("div"), {
textContent: searxng.settings.translations?.error_loading_next_page ?? "Error loading next page",
textContent: settings.translations?.error_loading_next_page ?? "Error loading next page",
className: "dialog-error"
});
errorElement.setAttribute("role", "alert");
@ -67,42 +63,36 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise<
}
};
searxng.ready(
() => {
const resultsElement = document.getElementById("results");
if (!resultsElement) {
console.error("Results element not found");
return;
}
const resultsElement: HTMLElement | null = document.getElementById("results");
if (!resultsElement) {
throw new Error("Results element not found");
}
const onlyImages = resultsElement.classList.contains("only_template_images");
const observedSelector = "article.result:last-child";
const onlyImages: boolean = resultsElement.classList.contains("only_template_images");
const observedSelector = "article.result:last-child";
const intersectionObserveOptions: IntersectionObserverInit = {
rootMargin: "320px"
};
const intersectionObserveOptions: IntersectionObserverInit = {
rootMargin: "320px"
};
const observer = new IntersectionObserver(async (entries: IntersectionObserverEntry[]) => {
const [paginationEntry] = entries;
const observer: IntersectionObserver = new IntersectionObserver((entries: IntersectionObserverEntry[]) => {
const [paginationEntry] = entries;
if (paginationEntry?.isIntersecting) {
observer.unobserve(paginationEntry.target);
if (paginationEntry?.isIntersecting) {
observer.unobserve(paginationEntry.target);
await loadNextPage(onlyImages, () => {
const nextObservedElement = document.querySelector<HTMLElement>(observedSelector);
if (nextObservedElement) {
observer.observe(nextObservedElement);
}
});
loadNextPage(onlyImages, () => {
const nextObservedElement = document.querySelector<HTMLElement>(observedSelector);
if (nextObservedElement) {
observer.observe(nextObservedElement);
}
}, intersectionObserveOptions);
const initialObservedElement = document.querySelector<HTMLElement>(observedSelector);
if (initialObservedElement) {
observer.observe(initialObservedElement);
}
},
{
on: [searxng.endpoint === "results", searxng.settings.infinite_scroll]
}).then(() => {
// wait until promise is resolved
});
}
);
}, intersectionObserveOptions);
const initialObservedElement: HTMLElement | null = document.querySelector<HTMLElement>(observedSelector);
if (initialObservedElement) {
observer.observe(initialObservedElement);
}

View file

@ -1,4 +1,4 @@
import { assertElement, searxng } from "./00_toolkit.ts";
import { assertElement, listen, mutable, settings } from "../core/toolkit.ts";
export type KeyBindingLayout = "default" | "vim";
@ -9,11 +9,13 @@ type KeyBinding = {
cat: string;
};
type HighlightResultElement = "down" | "up" | "visible" | "bottom" | "top";
/* common base for layouts */
const baseKeyBinding: Record<string, KeyBinding> = {
Escape: {
key: "ESC",
fun: (event) => removeFocus(event),
fun: (event: KeyboardEvent) => removeFocus(event),
des: "remove focus from the focused input",
cat: "Control"
},
@ -145,12 +147,12 @@ const keyBindingLayouts: Record<KeyBindingLayout, Record<string, KeyBinding>> =
}
};
const keyBindings =
searxng.settings.hotkeys && searxng.settings.hotkeys in keyBindingLayouts
? keyBindingLayouts[searxng.settings.hotkeys]
const keyBindings: Record<string, KeyBinding> =
settings.hotkeys && settings.hotkeys in keyBindingLayouts
? keyBindingLayouts[settings.hotkeys]
: keyBindingLayouts.default;
const isElementInDetail = (element?: Element): boolean => {
const isElementInDetail = (element?: HTMLElement): boolean => {
const ancestor = element?.closest(".detail, .result");
return ancestor?.classList.contains("detail") ?? false;
};
@ -159,12 +161,12 @@ const getResultElement = (element?: HTMLElement): HTMLElement | undefined => {
return element?.closest(".result") ?? undefined;
};
const isImageResult = (resultElement?: Element): boolean => {
const isImageResult = (resultElement?: HTMLElement): boolean => {
return resultElement?.classList.contains("result-images") ?? false;
};
const highlightResult =
(which: string | HTMLElement) =>
(which: HighlightResultElement | HTMLElement) =>
(noScroll?: boolean, keepFocus?: boolean): void => {
let effectiveWhich = which;
let current = document.querySelector<HTMLElement>(".result[data-vim-selected]");
@ -210,7 +212,7 @@ const highlightResult =
next = results[results.indexOf(current) - 1] || current;
break;
case "bottom":
next = results[results.length - 1];
next = results.at(-1);
break;
// biome-ignore lint/complexity/noUselessSwitchCase: fallthrough is intended
case "top":
@ -229,7 +231,7 @@ const highlightResult =
}
if (!noScroll) {
scrollPageToSelected();
mutable.scrollPageToSelected?.();
}
}
};
@ -245,7 +247,7 @@ const removeFocus = (event: KeyboardEvent): void => {
if (document.activeElement && (tagName === "input" || tagName === "select" || tagName === "textarea")) {
(document.activeElement as HTMLElement).blur();
} else {
searxng.closeDetail?.();
mutable.closeDetail?.();
}
};
@ -256,23 +258,23 @@ const pageButtonClick = (css_selector: string): void => {
}
};
const GoToNextPage = () => {
const GoToNextPage = (): void => {
pageButtonClick('nav#pagination .next_page button[type="submit"]');
};
const GoToPreviousPage = () => {
const GoToPreviousPage = (): void => {
pageButtonClick('nav#pagination .previous_page button[type="submit"]');
};
const scrollPageToSelected = (): void => {
mutable.scrollPageToSelected = (): void => {
const sel = document.querySelector<HTMLElement>(".result[data-vim-selected]");
if (!sel) return;
const wtop = document.documentElement.scrollTop || document.body.scrollTop,
height = document.documentElement.clientHeight,
etop = sel.offsetTop,
ebot = etop + sel.clientHeight,
offset = 120;
const wtop = document.documentElement.scrollTop || document.body.scrollTop;
const height = document.documentElement.clientHeight;
const etop = sel.offsetTop;
const ebot = etop + sel.clientHeight;
const offset = 120;
// first element ?
if (!sel.previousElementSibling && ebot < height) {
@ -297,7 +299,7 @@ const scrollPage = (amount: number): void => {
highlightResult("visible")();
};
const scrollPageTo = (position: number, nav: string): void => {
const scrollPageTo = (position: number, nav: HighlightResultElement): void => {
window.scrollTo(0, position);
highlightResult(nav)();
};
@ -385,7 +387,10 @@ const initHelpContent = (divElement: HTMLElement, keyBindings: typeof baseKeyBin
const toggleHelp = (keyBindings: typeof baseKeyBinding): void => {
let helpPanel = document.querySelector<HTMLElement>("#vim-hotkeys-help");
if (!helpPanel) {
if (helpPanel) {
// toggle hidden
helpPanel.classList.toggle("invisible");
} else {
// first call
helpPanel = Object.assign(document.createElement("div"), {
id: "vim-hotkeys-help",
@ -396,9 +401,6 @@ const toggleHelp = (keyBindings: typeof baseKeyBinding): void => {
if (body) {
body.appendChild(helpPanel);
}
} else {
// toggle hidden
helpPanel.classList.toggle("invisible");
}
};
@ -412,56 +414,53 @@ const copyURLToClipboard = async (): Promise<void> => {
}
};
searxng.ready(() => {
searxng.listen("click", ".result", function (this: HTMLElement, event: Event) {
if (!isElementInDetail(event.target as HTMLElement)) {
highlightResult(this)(true, true);
listen("click", ".result", function (this: HTMLElement, event: PointerEvent) {
if (!isElementInDetail(event.target as HTMLElement)) {
highlightResult(this)(true, true);
const resultElement = getResultElement(event.target as HTMLElement);
if (resultElement && isImageResult(resultElement)) {
event.preventDefault();
mutable.selectImage?.(resultElement);
}
}
});
// FIXME: Focus might also trigger Pointer event ^^^
listen(
"focus",
".result a",
(event: FocusEvent) => {
if (!isElementInDetail(event.target as HTMLElement)) {
const resultElement = getResultElement(event.target as HTMLElement);
if (resultElement && !resultElement.hasAttribute("data-vim-selected")) {
highlightResult(resultElement)(true);
}
if (resultElement && isImageResult(resultElement)) {
event.preventDefault();
searxng.selectImage?.(resultElement);
mutable.selectImage?.(resultElement);
}
}
});
},
{ capture: true }
);
searxng.listen(
"focus",
".result a",
(event: Event) => {
if (!isElementInDetail(event.target as HTMLElement)) {
const resultElement = getResultElement(event.target as HTMLElement);
listen("keydown", document, (event: KeyboardEvent) => {
// check for modifiers so we don't break browser's hotkeys
if (Object.hasOwn(keyBindings, event.key) && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) {
const tagName = (event.target as HTMLElement)?.tagName?.toLowerCase();
if (resultElement && !resultElement.getAttribute("data-vim-selected")) {
highlightResult(resultElement)(true);
}
if (resultElement && isImageResult(resultElement)) {
searxng.selectImage?.(resultElement);
}
}
},
{ capture: true }
);
searxng.listen("keydown", document, (event: KeyboardEvent) => {
// check for modifiers so we don't break browser's hotkeys
if (Object.hasOwn(keyBindings, event.key) && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) {
const tagName = (event.target as Element)?.tagName?.toLowerCase();
if (event.key === "Escape") {
keyBindings[event.key]?.fun(event);
} else {
if (event.target === document.body || tagName === "a" || tagName === "button") {
event.preventDefault();
keyBindings[event.key]?.fun(event);
}
}
if (event.key === "Escape") {
keyBindings[event.key]?.fun(event);
} else if (event.target === document.body || tagName === "a" || tagName === "button") {
event.preventDefault();
keyBindings[event.key]?.fun(event);
}
});
searxng.scrollPageToSelected = scrollPageToSelected;
searxng.selectNext = highlightResult("down");
searxng.selectPrevious = highlightResult("up");
}
});
mutable.selectNext = highlightResult("down");
mutable.selectPrevious = highlightResult("up");

View file

@ -1,89 +1,84 @@
import { searxng } from "./00_toolkit.ts";
import { listen } from "../core/toolkit.ts";
searxng.ready(
() => {
searxng.listen("click", ".searxng_init_map", async function (this: HTMLElement, event: Event) {
event.preventDefault();
this.classList.remove("searxng_init_map");
listen("click", ".searxng_init_map", async function (this: HTMLElement, event: Event) {
event.preventDefault();
this.classList.remove("searxng_init_map");
const {
View,
OlMap,
TileLayer,
VectorLayer,
OSM,
VectorSource,
Style,
Stroke,
Fill,
Circle,
fromLonLat,
GeoJSON,
Feature,
Point
} = await import("../pkg/ol.ts");
import("ol/ol.css");
const {
View,
OlMap,
TileLayer,
VectorLayer,
OSM,
VectorSource,
Style,
Stroke,
Fill,
Circle,
fromLonLat,
GeoJSON,
Feature,
Point
} = await import("../pkg/ol.ts");
import("ol/ol.css");
const { leafletTarget: target, mapLon, mapLat, mapGeojson } = this.dataset;
const { leafletTarget: target, mapLon, mapLat, mapGeojson } = this.dataset;
const lon = parseFloat(mapLon || "0");
const lat = parseFloat(mapLat || "0");
const view = new View({ maxZoom: 16, enableRotation: false });
const map = new OlMap({
target,
layers: [new TileLayer({ source: new OSM({ maxZoom: 16 }) })],
view
const lon = Number.parseFloat(mapLon || "0");
const lat = Number.parseFloat(mapLat || "0");
const view = new View({ maxZoom: 16, enableRotation: false });
const map = new OlMap({
target: target,
layers: [new TileLayer({ source: new OSM({ maxZoom: 16 }) })],
view: view
});
try {
const markerSource = new VectorSource({
features: [
new Feature({
geometry: new Point(fromLonLat([lon, lat]))
})
]
});
const markerLayer = new VectorLayer({
source: markerSource,
style: new Style({
image: new Circle({
radius: 6,
fill: new Fill({ color: "#3050ff" })
})
})
});
map.addLayer(markerLayer);
} catch (error) {
console.error("Failed to create marker layer:", error);
}
if (mapGeojson) {
try {
const geoSource = new VectorSource({
features: new GeoJSON().readFeatures(JSON.parse(mapGeojson), {
dataProjection: "EPSG:4326",
featureProjection: "EPSG:3857"
})
});
try {
const markerSource = new VectorSource({
features: [
new Feature({
geometry: new Point(fromLonLat([lon, lat]))
})
]
});
const geoLayer = new VectorLayer({
source: geoSource,
style: new Style({
stroke: new Stroke({ color: "#3050ff", width: 2 }),
fill: new Fill({ color: "#3050ff33" })
})
});
const markerLayer = new VectorLayer({
source: markerSource,
style: new Style({
image: new Circle({
radius: 6,
fill: new Fill({ color: "#3050ff" })
})
})
});
map.addLayer(geoLayer);
map.addLayer(markerLayer);
} catch (error) {
console.error("Failed to create marker layer:", error);
}
if (mapGeojson) {
try {
const geoSource = new VectorSource({
features: new GeoJSON().readFeatures(JSON.parse(mapGeojson), {
dataProjection: "EPSG:4326",
featureProjection: "EPSG:3857"
})
});
const geoLayer = new VectorLayer({
source: geoSource,
style: new Style({
stroke: new Stroke({ color: "#3050ff", width: 2 }),
fill: new Fill({ color: "#3050ff33" })
})
});
map.addLayer(geoLayer);
view.fit(geoSource.getExtent(), { padding: [20, 20, 20, 20] });
} catch (error) {
console.error("Failed to create GeoJSON layer:", error);
}
}
});
},
{ on: [searxng.endpoint === "results"] }
);
view.fit(geoSource.getExtent(), { padding: [20, 20, 20, 20] });
} catch (error) {
console.error("Failed to create GeoJSON layer:", error);
}
}
});

View file

@ -1,9 +1,11 @@
import { searxng } from "./00_toolkit.ts";
import { http, listen, settings } from "../core/toolkit.ts";
let engineDescriptions: Record<string, [string, string]> | undefined;
const loadEngineDescriptions = async (): Promise<void> => {
let engineDescriptions: Record<string, [string, string]> | null = null;
if (engineDescriptions) return;
try {
const res = await searxng.http("GET", "engine_descriptions.json");
const res = await http("GET", "engine_descriptions.json");
engineDescriptions = await res.json();
} catch (error) {
console.error("Error fetching engineDescriptions:", error);
@ -12,7 +14,7 @@ const loadEngineDescriptions = async (): Promise<void> => {
for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) {
const elements = document.querySelectorAll<HTMLElement>(`[data-engine-name="${engine_name}"] .engine-description`);
const sourceText = ` (<i>${searxng.settings.translations?.Source}:&nbsp;${source}</i>)`;
const sourceText = ` (<i>${settings.translations?.Source}:&nbsp;${source}</i>)`;
for (const element of elements) {
element.innerHTML = description + sourceText;
@ -29,43 +31,38 @@ const toggleEngines = (enable: boolean, engineToggles: NodeListOf<HTMLInputEleme
}
};
searxng.ready(
() => {
const engineElements = document.querySelectorAll<HTMLElement>("[data-engine-name]");
for (const engineElement of engineElements) {
searxng.listen("mouseenter", engineElement, loadEngineDescriptions);
}
const engineElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>("[data-engine-name]");
for (const engineElement of engineElements) {
listen("mouseenter", engineElement, loadEngineDescriptions);
}
const engineToggles = document.querySelectorAll<HTMLInputElement>(
"tbody input[type=checkbox][class~=checkbox-onoff]"
);
const enableAllEngines = document.querySelectorAll<HTMLElement>(".enable-all-engines");
for (const engine of enableAllEngines) {
searxng.listen("click", engine, () => toggleEngines(true, engineToggles));
}
const disableAllEngines = document.querySelectorAll<HTMLElement>(".disable-all-engines");
for (const engine of disableAllEngines) {
searxng.listen("click", engine, () => toggleEngines(false, engineToggles));
}
const copyHashButton = document.querySelector<HTMLElement>("#copy-hash");
if (copyHashButton) {
searxng.listen("click", copyHashButton, async (event: Event) => {
event.preventDefault();
const { copiedText, hash } = copyHashButton.dataset;
if (!copiedText || !hash) return;
try {
await navigator.clipboard.writeText(hash);
copyHashButton.innerText = copiedText;
} catch (error) {
console.error("Failed to copy hash:", error);
}
});
}
},
{ on: [searxng.endpoint === "preferences"] }
const engineToggles: NodeListOf<HTMLInputElement> = document.querySelectorAll<HTMLInputElement>(
"tbody input[type=checkbox][class~=checkbox-onoff]"
);
const enableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".enable-all-engines");
for (const engine of enableAllEngines) {
listen("click", engine, () => toggleEngines(true, engineToggles));
}
const disableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".disable-all-engines");
for (const engine of disableAllEngines) {
listen("click", engine, () => toggleEngines(false, engineToggles));
}
const copyHashButton: HTMLElement | null = document.querySelector<HTMLElement>("#copy-hash");
if (copyHashButton) {
listen("click", copyHashButton, async (event: Event) => {
event.preventDefault();
const { copiedText, hash } = copyHashButton.dataset;
if (!(copiedText && hash)) return;
try {
await navigator.clipboard.writeText(hash);
copyHashButton.innerText = copiedText;
} catch (error) {
console.error("Failed to copy hash:", error);
}
});
}

View file

@ -1,181 +1,175 @@
import "../../../node_modules/swiped-events/src/swiped-events.js";
import { assertElement, searxng } from "./00_toolkit.ts";
import { assertElement, listen, mutable, settings } from "../core/toolkit.ts";
const loadImage = (imgSrc: string, onSuccess: () => void): void => {
// singleton image object, which is used for all loading processes of a detailed image
const imgLoader = new Image();
let imgTimeoutID: number;
// set handlers in the on-properties
imgLoader.onload = () => {
onSuccess();
};
const imageLoader = (resultElement: HTMLElement): void => {
if (imgTimeoutID) clearTimeout(imgTimeoutID);
imgLoader.src = imgSrc;
const imgElement = resultElement.querySelector<HTMLImageElement>(".result-images-source img");
if (!imgElement) return;
// use thumbnail until full image loads
const thumbnail = resultElement.querySelector<HTMLImageElement>(".image_thumbnail");
if (thumbnail) {
if (thumbnail.src === `${settings.theme_static_path}/img/img_load_error.svg`) return;
imgElement.onerror = (): void => {
imgElement.src = thumbnail.src;
};
imgElement.src = thumbnail.src;
}
const imgSource = imgElement.getAttribute("data-src");
if (!imgSource) return;
// unsafe nodejs specific, cast to https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#return_value
// https://github.com/searxng/searxng/pull/5073#discussion_r2265767231
imgTimeoutID = setTimeout(() => {
imgElement.src = imgSource;
imgElement.removeAttribute("data-src");
}, 1000) as unknown as number;
};
searxng.ready(
const imageThumbnails: NodeListOf<HTMLImageElement> =
document.querySelectorAll<HTMLImageElement>("#urls img.image_thumbnail");
for (const thumbnail of imageThumbnails) {
if (thumbnail.complete && thumbnail.naturalWidth === 0) {
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
}
thumbnail.onerror = (): void => {
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
};
}
const copyUrlButton: HTMLButtonElement | null =
document.querySelector<HTMLButtonElement>("#search_url button#copy_url");
copyUrlButton?.style.setProperty("display", "block");
mutable.selectImage = (resultElement: HTMLElement): void => {
// add a class that can be evaluated in the CSS and indicates that the
// detail view is open
const resultsElement = document.getElementById("results");
resultsElement?.classList.add("image-detail-open");
// add a hash to the browser history so that pressing back doesn't return
// to the previous page this allows us to dismiss the image details on
// pressing the back button on mobile devices
window.location.hash = "#image-viewer";
mutable.scrollPageToSelected?.();
// if there is no element given by the caller, stop here
if (!resultElement) return;
imageLoader(resultElement);
};
mutable.closeDetail = (): void => {
const resultsElement = document.getElementById("results");
resultsElement?.classList.remove("image-detail-open");
// remove #image-viewer hash from url by navigating back
if (window.location.hash === "#image-viewer") {
window.history.back();
}
mutable.scrollPageToSelected?.();
};
listen("click", ".btn-collapse", function (this: HTMLElement) {
const btnLabelCollapsed = this.getAttribute("data-btn-text-collapsed");
const btnLabelNotCollapsed = this.getAttribute("data-btn-text-not-collapsed");
const target = this.getAttribute("data-target");
if (!(target && btnLabelCollapsed && btnLabelNotCollapsed)) return;
const targetElement = document.querySelector<HTMLElement>(target);
assertElement(targetElement);
const isCollapsed = this.classList.contains("collapsed");
const newLabel = isCollapsed ? btnLabelNotCollapsed : btnLabelCollapsed;
const oldLabel = isCollapsed ? btnLabelCollapsed : btnLabelNotCollapsed;
this.innerHTML = this.innerHTML.replace(oldLabel, newLabel);
this.classList.toggle("collapsed");
targetElement.classList.toggle("invisible");
});
listen("click", ".media-loader", function (this: HTMLElement) {
const target = this.getAttribute("data-target");
if (!target) return;
const iframeLoad = document.querySelector<HTMLIFrameElement>(`${target} > iframe`);
assertElement(iframeLoad);
const srctest = iframeLoad.getAttribute("src");
if (!srctest) {
const dataSrc = iframeLoad.getAttribute("data-src");
if (dataSrc) {
iframeLoad.setAttribute("src", dataSrc);
}
}
});
listen("click", "#copy_url", async function (this: HTMLElement) {
const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
assertElement(target);
await navigator.clipboard.writeText(target.innerText);
const copiedText = this.dataset.copiedText;
if (copiedText) {
this.innerText = copiedText;
}
});
listen("click", ".result-detail-close", (event: Event) => {
event.preventDefault();
mutable.closeDetail?.();
});
listen("click", ".result-detail-previous", (event: Event) => {
event.preventDefault();
mutable.selectPrevious?.(false);
});
listen("click", ".result-detail-next", (event: Event) => {
event.preventDefault();
mutable.selectNext?.(false);
});
// listen for the back button to be pressed and dismiss the image details when called
window.addEventListener("hashchange", () => {
if (window.location.hash !== "#image-viewer") {
mutable.closeDetail?.();
}
});
const swipeHorizontal: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".swipe-horizontal");
for (const element of swipeHorizontal) {
listen("swiped-left", element, () => {
mutable.selectNext?.(false);
});
listen("swiped-right", element, () => {
mutable.selectPrevious?.(false);
});
}
window.addEventListener(
"scroll",
() => {
const imageThumbnails = document.querySelectorAll<HTMLImageElement>("#urls img.image_thumbnail");
for (const thumbnail of imageThumbnails) {
if (thumbnail.complete && thumbnail.naturalWidth === 0) {
thumbnail.src = `${searxng.settings.theme_static_path}/img/img_load_error.svg`;
}
const backToTopElement = document.getElementById("backToTop");
const resultsElement = document.getElementById("results");
thumbnail.onerror = () => {
thumbnail.src = `${searxng.settings.theme_static_path}/img/img_load_error.svg`;
};
if (backToTopElement && resultsElement) {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const isScrolling = scrollTop >= 100;
resultsElement.classList.toggle("scrolling", isScrolling);
}
const copyUrlButton = document.querySelector<HTMLButtonElement>("#search_url button#copy_url");
copyUrlButton?.style.setProperty("display", "block");
searxng.listen("click", ".btn-collapse", function (this: HTMLElement) {
const btnLabelCollapsed = this.getAttribute("data-btn-text-collapsed");
const btnLabelNotCollapsed = this.getAttribute("data-btn-text-not-collapsed");
const target = this.getAttribute("data-target");
if (!target || !btnLabelCollapsed || !btnLabelNotCollapsed) return;
const targetElement = document.querySelector<HTMLElement>(target);
assertElement(targetElement);
const isCollapsed = this.classList.contains("collapsed");
const newLabel = isCollapsed ? btnLabelNotCollapsed : btnLabelCollapsed;
const oldLabel = isCollapsed ? btnLabelCollapsed : btnLabelNotCollapsed;
this.innerHTML = this.innerHTML.replace(oldLabel, newLabel);
this.classList.toggle("collapsed");
targetElement.classList.toggle("invisible");
});
searxng.listen("click", ".media-loader", function (this: HTMLElement) {
const target = this.getAttribute("data-target");
if (!target) return;
const iframeLoad = document.querySelector<HTMLIFrameElement>(`${target} > iframe`);
assertElement(iframeLoad);
const srctest = iframeLoad.getAttribute("src");
if (!srctest) {
const dataSrc = iframeLoad.getAttribute("data-src");
if (dataSrc) {
iframeLoad.setAttribute("src", dataSrc);
}
}
});
searxng.listen("click", "#copy_url", async function (this: HTMLElement) {
const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
assertElement(target);
await navigator.clipboard.writeText(target.innerText);
const copiedText = this.dataset.copiedText;
if (copiedText) {
this.innerText = copiedText;
}
});
searxng.selectImage = (resultElement: Element): void => {
// add a class that can be evaluated in the CSS and indicates that the
// detail view is open
const resultsElement = document.getElementById("results");
resultsElement?.classList.add("image-detail-open");
// add a hash to the browser history so that pressing back doesn't return
// to the previous page this allows us to dismiss the image details on
// pressing the back button on mobile devices
window.location.hash = "#image-viewer";
searxng.scrollPageToSelected?.();
// if there is no element given by the caller, stop here
if (!resultElement) return;
// find image element, if there is none, stop here
const img = resultElement.querySelector<HTMLImageElement>(".result-images-source img");
if (!img) return;
// <img src="" data-src="http://example.org/image.jpg">
const src = img.getAttribute("data-src");
if (!src) return;
// use thumbnail until full image loads
const thumbnail = resultElement.querySelector<HTMLImageElement>(".image_thumbnail");
if (thumbnail) {
img.src = thumbnail.src;
}
// load full size image
loadImage(src, () => {
img.src = src;
img.onerror = () => {
img.src = `${searxng.settings.theme_static_path}/img/img_load_error.svg`;
};
img.removeAttribute("data-src");
});
};
searxng.closeDetail = (): void => {
const resultsElement = document.getElementById("results");
resultsElement?.classList.remove("image-detail-open");
// remove #image-viewer hash from url by navigating back
if (window.location.hash === "#image-viewer") {
window.history.back();
}
searxng.scrollPageToSelected?.();
};
searxng.listen("click", ".result-detail-close", (event: Event) => {
event.preventDefault();
searxng.closeDetail?.();
});
searxng.listen("click", ".result-detail-previous", (event: Event) => {
event.preventDefault();
searxng.selectPrevious?.(false);
});
searxng.listen("click", ".result-detail-next", (event: Event) => {
event.preventDefault();
searxng.selectNext?.(false);
});
// listen for the back button to be pressed and dismiss the image details when called
window.addEventListener("hashchange", () => {
if (window.location.hash !== "#image-viewer") {
searxng.closeDetail?.();
}
});
const swipeHorizontal = document.querySelectorAll<HTMLElement>(".swipe-horizontal");
for (const element of swipeHorizontal) {
searxng.listen("swiped-left", element, () => {
searxng.selectNext?.(false);
});
searxng.listen("swiped-right", element, () => {
searxng.selectPrevious?.(false);
});
}
window.addEventListener(
"scroll",
() => {
const backToTopElement = document.getElementById("backToTop");
const resultsElement = document.getElementById("results");
if (backToTopElement && resultsElement) {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const isScrolling = scrollTop >= 100;
resultsElement.classList.toggle("scrolling", isScrolling);
}
},
true
);
},
{ on: [searxng.endpoint === "results"] }
true
);

View file

@ -1,4 +1,4 @@
import { assertElement, searxng } from "./00_toolkit.ts";
import { assertElement, listen, settings } from "../core/toolkit.ts";
const submitIfQuery = (qInput: HTMLInputElement): void => {
if (qInput.value.length > 0) {
@ -17,217 +17,88 @@ const createClearButton = (qInput: HTMLInputElement): void => {
updateClearButton(qInput, cs);
searxng.listen("click", cs, (event: MouseEvent) => {
listen("click", cs, (event: MouseEvent) => {
event.preventDefault();
qInput.value = "";
qInput.focus();
updateClearButton(qInput, cs);
});
searxng.listen("input", qInput, () => updateClearButton(qInput, cs), { passive: true });
listen("input", qInput, () => updateClearButton(qInput, cs), { passive: true });
};
const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
try {
let res: Response;
const qInput = document.getElementById("q") as HTMLInputElement | null;
assertElement(qInput);
if (searxng.settings.method === "GET") {
res = await searxng.http("GET", `./autocompleter?q=${query}`);
} else {
res = await searxng.http("POST", "./autocompleter", new URLSearchParams({ q: query }));
}
const isMobile: boolean = window.matchMedia("(max-width: 50em)").matches;
const isResultsPage: boolean = document.querySelector("main")?.id === "main_results";
const results = await res.json();
// focus search input on large screens
if (!(isMobile || isResultsPage)) {
qInput.focus();
}
const autocomplete = document.querySelector<HTMLElement>(".autocomplete");
assertElement(autocomplete);
createClearButton(qInput);
const autocompleteList = document.querySelector<HTMLUListElement>(".autocomplete ul");
assertElement(autocompleteList);
// Additionally to searching when selecting a new category, we also
// automatically start a new search request when the user changes a search
// filter (safesearch, time range or language) (this requires JavaScript
// though)
if (
settings.search_on_category_select &&
// If .search_filters is undefined (invisible) we are on the homepage and
// hence don't have to set any listeners
document.querySelector(".search_filters")
) {
const safesearchElement = document.getElementById("safesearch");
if (safesearchElement) {
listen("change", safesearchElement, () => submitIfQuery(qInput));
}
autocomplete.classList.add("open");
autocompleteList.replaceChildren();
const timeRangeElement = document.getElementById("time_range");
if (timeRangeElement) {
listen("change", timeRangeElement, () => submitIfQuery(qInput));
}
// show an error message that no result was found
if (!results?.[1]?.length) {
const noItemFoundMessage = Object.assign(document.createElement("li"), {
className: "no-item-found",
textContent: searxng.settings.translations?.no_item_found ?? "No results found"
});
autocompleteList.append(noItemFoundMessage);
const languageElement = document.getElementById("language");
if (languageElement) {
listen("change", languageElement, () => submitIfQuery(qInput));
}
}
const categoryButtons: HTMLButtonElement[] = [
...document.querySelectorAll<HTMLButtonElement>("button.category_button")
];
for (const button of categoryButtons) {
listen("click", button, (event: MouseEvent) => {
if (event.shiftKey) {
event.preventDefault();
button.classList.toggle("selected");
return;
}
const fragment = new DocumentFragment();
for (const result of results[1]) {
const li = Object.assign(document.createElement("li"), { textContent: result });
searxng.listen("mousedown", li, () => {
qInput.value = result;
const form = document.querySelector<HTMLFormElement>("#search");
form?.submit();
autocomplete.classList.remove("open");
});
fragment.append(li);
// deselect all other categories
for (const categoryButton of categoryButtons) {
categoryButton.classList.toggle("selected", categoryButton === button);
}
});
}
autocompleteList.append(fragment);
} catch (error) {
console.error("Error fetching autocomplete results:", error);
const form: HTMLFormElement | null = document.querySelector<HTMLFormElement>("#search");
assertElement(form);
// override form submit action to update the actually selected categories
listen("submit", form, (event: Event) => {
event.preventDefault();
const categoryValuesInput = document.querySelector<HTMLInputElement>("#selected-categories");
if (categoryValuesInput) {
const categoryValues = categoryButtons
.filter((button) => button.classList.contains("selected"))
.map((button) => button.name.replace("category_", ""));
categoryValuesInput.value = categoryValues.join(",");
}
};
searxng.ready(
() => {
const qInput = document.getElementById("q") as HTMLInputElement | null;
assertElement(qInput);
const isMobile = window.matchMedia("(max-width: 50em)").matches;
const isResultsPage = document.querySelector("main")?.id === "main_results";
// focus search input on large screens
if (!isMobile && !isResultsPage) {
qInput.focus();
}
createClearButton(qInput);
// autocompleter
if (searxng.settings.autocomplete) {
let timeoutId: number;
searxng.listen("input", qInput, () => {
clearTimeout(timeoutId);
const query = qInput.value;
const minLength = searxng.settings.autocomplete_min ?? 2;
if (query.length < minLength) return;
timeoutId = window.setTimeout(async () => {
if (query === qInput.value) {
await fetchResults(qInput, query);
}
}, 300);
});
const autocomplete = document.querySelector<HTMLElement>(".autocomplete");
const autocompleteList = document.querySelector<HTMLUListElement>(".autocomplete ul");
if (autocompleteList) {
searxng.listen("keyup", qInput, (event: KeyboardEvent) => {
const listItems = [...autocompleteList.children] as HTMLElement[];
const currentIndex = listItems.findIndex((item) => item.classList.contains("active"));
let newCurrentIndex = -1;
switch (event.key) {
case "ArrowUp": {
const currentItem = listItems[currentIndex];
if (currentItem && currentIndex >= 0) {
currentItem.classList.remove("active");
}
// we need to add listItems.length to the index calculation here because the JavaScript modulos
// operator doesn't work with negative numbers
newCurrentIndex = (currentIndex - 1 + listItems.length) % listItems.length;
break;
}
case "ArrowDown": {
const currentItem = listItems[currentIndex];
if (currentItem && currentIndex >= 0) {
currentItem.classList.remove("active");
}
newCurrentIndex = (currentIndex + 1) % listItems.length;
break;
}
case "Tab":
case "Enter":
if (autocomplete) {
autocomplete.classList.remove("open");
}
break;
}
if (newCurrentIndex !== -1) {
const selectedItem = listItems[newCurrentIndex];
if (selectedItem) {
selectedItem.classList.add("active");
if (!selectedItem.classList.contains("no-item-found")) {
const qInput = document.getElementById("q") as HTMLInputElement | null;
if (qInput) {
qInput.value = selectedItem.textContent ?? "";
}
}
}
}
});
}
}
// Additionally to searching when selecting a new category, we also
// automatically start a new search request when the user changes a search
// filter (safesearch, time range or language) (this requires JavaScript
// though)
if (
searxng.settings.search_on_category_select &&
// If .search_filters is undefined (invisible) we are on the homepage and
// hence don't have to set any listeners
document.querySelector(".search_filters")
) {
const safesearchElement = document.getElementById("safesearch");
if (safesearchElement) {
searxng.listen("change", safesearchElement, () => submitIfQuery(qInput));
}
const timeRangeElement = document.getElementById("time_range");
if (timeRangeElement) {
searxng.listen("change", timeRangeElement, () => submitIfQuery(qInput));
}
const languageElement = document.getElementById("language");
if (languageElement) {
searxng.listen("change", languageElement, () => submitIfQuery(qInput));
}
}
const categoryButtons = [...document.querySelectorAll<HTMLButtonElement>("button.category_button")];
for (const button of categoryButtons) {
searxng.listen("click", button, (event: MouseEvent) => {
if (event.shiftKey) {
event.preventDefault();
button.classList.toggle("selected");
return;
}
// deselect all other categories
for (const categoryButton of categoryButtons) {
categoryButton.classList.toggle("selected", categoryButton === button);
}
});
}
const form = document.querySelector<HTMLFormElement>("#search");
assertElement(form);
// override form submit action to update the actually selected categories
searxng.listen("submit", form, (event: Event) => {
event.preventDefault();
const categoryValuesInput = document.querySelector<HTMLInputElement>("#selected-categories");
if (categoryValuesInput) {
const categoryValues = categoryButtons
.filter((button) => button.classList.contains("selected"))
.map((button) => button.name.replace("category_", ""));
categoryValuesInput.value = categoryValues.join(",");
}
form.submit();
});
},
{ on: [searxng.endpoint === "index" || searxng.endpoint === "results"] }
);
form.submit();
});

View file

@ -18,11 +18,7 @@
cursor: default;
&::selection {
background: transparent; /* WebKit/Blink Browsers */
}
&::-moz-selection {
background: transparent; /* Gecko Browsers */
background: transparent;
}
margin-right: 8px;

View file

@ -2,9 +2,6 @@
// Mixins
.text-size-adjust (@property: 100%) {
-webkit-text-size-adjust: @property;
-ms-text-size-adjust: @property;
-moz-text-size-adjust: @property;
text-size-adjust: @property;
}
@ -22,7 +19,6 @@
// disable user selection
.disable-user-select () {
-webkit-touch-callout: none;
user-select: none;
}

View file

@ -1,12 +1,6 @@
@import (inline) "../../node_modules/normalize.css/normalize.css";
@import "definitions.less";
.text-size-adjust (@property: 100%) {
-webkit-text-size-adjust: @property;
-ms-text-size-adjust: @property;
-moz-text-size-adjust: @property;
text-size-adjust: @property;
}
@import "mixins.less";
// Reset padding and margin
html,

View file

@ -196,11 +196,6 @@ html.no-js #clear_search.hide_if_nojs {
.ltr-rounded-left-corners(0.8rem);
}
#q::-ms-clear,
#q::-webkit-search-cancel-button {
display: none;
}
#send_search {
.ltr-rounded-right-corners(0.8rem);
@ -271,7 +266,6 @@ html.no-js #clear_search.hide_if_nojs {
width: 100%;
.ltr-text-align-left();
overflow: scroll hidden;
-webkit-overflow-scrolling: touch;
}
}
}
@ -374,11 +368,6 @@ html.no-js #clear_search.hide_if_nojs {
#categories {
.disable-user-select;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
#categories_container {

View file

@ -129,13 +129,7 @@
}
// select HTML element
@supports (
(background-position-x: 100%) and
(
(appearance: none) or (-webkit-appearance: none) or
(-moz-appearance: none)
)
) {
@supports ((background-position-x: 100%) and ((appearance: none))) {
select {
border-width: 0 0 0 2rem;
background-position-x: -2rem;

View file

@ -156,9 +156,7 @@ div.selectable_url {
td {
padding: 0 1em 0 0;
padding-top: 0;
.ltr-padding-right(1rem);
padding-bottom: 0;
.ltr-padding-left(0);
}
@ -307,7 +305,7 @@ html body .tabs > input:checked {
}
~ label {
position: inherited;
position: inherit;
background: inherit;
border-bottom: 2px solid transparent;
font-weight: normal;
@ -347,17 +345,9 @@ select {
}
}
@supports (
(background-position-x: 100%) and
(
(appearance: none) or (-webkit-appearance: none) or
(-moz-appearance: none)
)
) {
@supports ((background-position-x: 100%) and ((appearance: none))) {
select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
border-width: 0 2rem 0 0;
border-color: transparent;
background: data-uri("image/svg+xml;charset=UTF-8", @select-light-svg-path)
@ -400,8 +390,6 @@ select {
/* -- checkbox-onoff -- */
input.checkbox-onoff[type="checkbox"] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
cursor: pointer;
display: inline-block;
@ -475,8 +463,6 @@ input.checkbox-onoff.reversed-checkbox[type="checkbox"] {
/* -- checkbox -- */
@supports (transform: rotate(-45deg)) {
input[type="checkbox"]:not(.checkbox-onoff) {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 20px;
@ -549,33 +535,16 @@ input.checkbox-onoff.reversed-checkbox[type="checkbox"] {
border-right: 0.5em solid var(--color-toolkit-loader-border);
border-bottom: 0.5em solid var(--color-toolkit-loader-border);
border-left: 0.5em solid var(--color-toolkit-loader-borderleft);
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation: load8 1.2s infinite linear;
animation: load8 1.2s infinite linear;
}
@-webkit-keyframes load8 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load8 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@ -606,9 +575,6 @@ td:hover .engine-tooltip,
margin: 0;
padding: 0 0.125rem 0 4rem;
width: 100%;
width: -moz-available;
width: -webkit-fill-available;
width: fill;
flex-flow: row nowrap;
align-items: center;
display: inline-flex;

View file

@ -8,7 +8,7 @@ import type { Config as SvgoConfig } from "svgo";
import { type IconSet, type JinjaMacro, jinja_svg_sets } from "./tools/jinja_svg_catalog.ts";
const HERE = `${dirname(argv[1] || "")}/`;
const dest = resolve(HERE, "../../searx/templates/simple/icons.html");
const dest: string = resolve(HERE, "../../searx/templates/simple/icons.html");
const searxng_jinja_macros: JinjaMacro[] = [
{ name: "icon", class: "sxng-icon-set" },

View file

@ -17,24 +17,24 @@ export type Src2Dest = {
*
* @param items - Array of SVG files (src: SVG, dest:PNG) to convert.
*/
export const svg2png = async (items: Src2Dest[]) => {
export const svg2png = (items: Src2Dest[]): void => {
for (const item of items) {
try {
fs.mkdirSync(path.dirname(item.dest), { recursive: true });
fs.mkdirSync(path.dirname(item.dest), { recursive: true });
const info = await sharp(item.src)
.png({
force: true,
compressionLevel: 9,
palette: true
})
.toFile(item.dest);
console.log(`[svg2png] created ${item.dest} -- bytes: ${info.size}, w:${info.width}px, h:${info.height}px`);
} catch (err) {
console.error(`ERROR: ${item.dest} -- ${err}`);
throw err;
}
sharp(item.src)
.png({
force: true,
compressionLevel: 9,
palette: true
})
.toFile(item.dest)
.then((info) => {
console.log(`[svg2png] created ${item.dest} -- bytes: ${info.size}, w:${info.width}px, h:${info.height}px`);
})
.catch((error) => {
console.error(`ERROR: ${item.dest} -- ${error}`);
throw error;
});
}
};
@ -44,7 +44,7 @@ export const svg2png = async (items: Src2Dest[]) => {
* @param items - Array of SVG files (src:SVG, dest:SVG) to optimize.
* @param svgo_opts - Options passed to svgo.
*/
export const svg2svg = (items: Src2Dest[], svgo_opts: Config) => {
export const svg2svg = (items: Src2Dest[], svgo_opts: Config): void => {
for (const item of items) {
try {
fs.mkdirSync(path.dirname(item.dest), { recursive: true });
@ -54,9 +54,9 @@ export const svg2svg = (items: Src2Dest[], svgo_opts: Config) => {
fs.writeFileSync(item.dest, opt.data);
console.log(`[svg2svg] optimized: ${item.dest} -- src: ${item.src}`);
} catch (err) {
console.error(`ERROR: optimize src: ${item.src} -- ${err}`);
throw err;
} catch (error) {
console.error(`ERROR: optimize src: ${item.src} -- ${error}`);
throw error;
}
}
};

View file

@ -1,10 +1,8 @@
import fs from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { Edge } from "edge.js";
import { type Config as SvgoConfig, optimize as svgo } from "svgo";
const __dirname = dirname(fileURLToPath(import.meta.url));
const __jinja_class_placeholder__ = "__jinja_class_placeholder__";
// A set of icons
@ -43,9 +41,9 @@ export type JinjaMacro = {
* @param macros - Jinja macros to create.
* @param items - Array of SVG items.
*/
export const jinja_svg_catalog = (dest: string, macros: JinjaMacro[], items: IconSVG[]) => {
export const jinja_svg_catalog = (dest: string, macros: JinjaMacro[], items: IconSVG[]): void => {
const svg_catalog: Record<string, string> = {};
const edge_template = resolve(__dirname, "jinja_svg_catalog.html.edge");
const edge_template = resolve(import.meta.dirname, "jinja_svg_catalog.html.edge");
for (const item of items) {
// JSON.stringify & JSON.parse are used to create a deep copy of the item.svgo_opts object
@ -63,15 +61,13 @@ export const jinja_svg_catalog = (dest: string, macros: JinjaMacro[], items: Ico
const opt = svgo(raw, svgo_opts);
svg_catalog[item.name] = opt.data;
} catch (err) {
console.error(`ERROR: jinja_svg_catalog processing ${item.name} src: ${item.src} -- ${err}`);
throw err;
} catch (error) {
console.error(`ERROR: jinja_svg_catalog processing ${item.name} src: ${item.src} -- ${error}`);
throw error;
}
}
fs.mkdir(dirname(dest), { recursive: true }, (err) => {
if (err) throw err;
});
fs.mkdirSync(dirname(dest), { recursive: true });
const ctx = {
svg_catalog: svg_catalog,
@ -97,7 +93,7 @@ export const jinja_svg_catalog = (dest: string, macros: JinjaMacro[], items: Ico
* @param macros - Jinja macros to create.
* @param sets - Array of SVG sets.
*/
export const jinja_svg_sets = (dest: string, macros: JinjaMacro[], sets: IconSet[]) => {
export const jinja_svg_sets = (dest: string, macros: JinjaMacro[], sets: IconSet[]): void => {
const items: IconSVG[] = [];
const all: string[] = [];

View file

@ -20,8 +20,8 @@ export const plg_svg2png = (items: Src2Dest[]): Plugin => {
return {
name: "searxng-simple-svg2png",
apply: "build",
async writeBundle() {
await svg2png(items);
writeBundle: () => {
svg2png(items);
}
};
};
@ -36,7 +36,7 @@ export const plg_svg2svg = (items: Src2Dest[], svgo_opts: Config): Plugin => {
return {
name: "searxng-simple-svg2svg",
apply: "build",
writeBundle() {
writeBundle: () => {
svg2svg(items, svgo_opts);
}
};

View file

@ -6,11 +6,12 @@ import { resolve } from "node:path";
import { constants as zlibConstants } from "node:zlib";
import browserslistToEsbuild from "browserslist-to-esbuild";
import { browserslistToTargets } from "lightningcss";
import type { PreRenderedAsset } from "rolldown";
import type { Config } from "svgo";
import type { UserConfig } from "vite";
import analyzer from "vite-bundle-analyzer";
import manifest from "./package.json";
import { plg_svg2png, plg_svg2svg } from "./tools/plg";
import manifest from "./package.json" with { type: "json" };
import { plg_svg2png, plg_svg2svg } from "./tools/plg.ts";
const ROOT = "../../"; // root of the git repository
@ -20,7 +21,7 @@ const PATH = {
modules: "node_modules/",
src: "src/",
templates: resolve(ROOT, "searx/templates/simple/")
};
} as const;
const svg2svg_opts: Config = {
plugins: [{ name: "preset-default" }, "sortAttrs", "convertStyleToAttrs"]
@ -49,37 +50,37 @@ export default {
rollupOptions: {
input: {
// build CSS files
"searxng-ltr.min.css": `${PATH.src}/less/style-ltr.less`,
"searxng-rtl.min.css": `${PATH.src}/less/style-rtl.less`,
"rss.min.css": `${PATH.src}/less/rss.less`,
"searxng-ltr.css": `${PATH.src}/less/style-ltr.less`,
"searxng-rtl.css": `${PATH.src}/less/style-rtl.less`,
"rss.css": `${PATH.src}/less/rss.less`,
// build script files
"searxng.min": `${PATH.src}/js/main/index.ts`,
"searxng.core": `${PATH.src}/js/core/index.ts`,
// ol
"ol.min": `${PATH.src}/js/pkg/ol.ts`,
"ol.min.css": `${PATH.modules}/ol/ol.css`
ol: `${PATH.src}/js/pkg/ol.ts`,
"ol.css": `${PATH.modules}/ol/ol.css`
},
// file naming conventions / pathnames are relative to outDir (PATH.dist)
output: {
entryFileNames: "js/[name].js",
chunkFileNames: "js/[name].js",
assetFileNames: ({ names }) => {
entryFileNames: "js/[name].min.js",
chunkFileNames: "js/[name].min.js",
assetFileNames: ({ names }: PreRenderedAsset): string => {
const [name] = names;
const extension = name?.split(".").pop();
switch (extension) {
case "css":
return `css/[name][extname]`;
return "css/[name].min[extname]";
case "js":
return `js/[name][extname]`;
return "js/[name].min[extname]";
case "png":
case "svg":
return `img/[name][extname]`;
return "img/[name][extname]";
default:
console.warn("Unknown asset:", name);
return `[name][extname]`;
return "[name][extname]";
}
}
}

View file

@ -2,6 +2,7 @@
<html class="no-js theme-{{ preferences.get_value('simple_style') or 'auto' }} center-alignment-{{ preferences.get_value('center_alignment') and 'yes' or 'no' }}" lang="{{ locale_rfc5646 }}" {% if rtl %} dir="rtl"{% endif %}>
<head>
<meta charset="UTF-8">
<meta name="endpoint" content="{{ endpoint }}">
<meta name="description" content="SearXNG — a privacy-respecting, open metasearch engine">
<meta name="keywords" content="SearXNG, search, search engine, metasearch, meta search">
<meta name="generator" content="searxng/{{ searx_version }}">
@ -82,6 +83,6 @@
{% endfor %}
</p>
</footer>
<script type="module" src="{{ url_for('static', filename='js/searxng.min.js') }}" client_settings="{{ client_settings }}"></script>
<script type="module" src="{{ url_for('static', filename='js/searxng.core.min.js') }}" client_settings="{{ client_settings }}"></script>
</body>
</html>

View file

@ -358,7 +358,7 @@ def get_client_settings():
'favicon_resolver': req_pref.get_value('favicon_resolver'),
'advanced_search': req_pref.get_value('advanced_search'),
'query_in_title': req_pref.get_value('query_in_title'),
'safesearch': str(req_pref.get_value('safesearch')),
'safesearch': req_pref.get_value('safesearch'),
'theme': req_pref.get_value('theme'),
'doi_resolver': get_doi_resolver(),
}
@ -368,15 +368,7 @@ def render(template_name: str, **kwargs):
# values from the preferences
# pylint: disable=too-many-statements
client_settings = get_client_settings()
kwargs['client_settings'] = str(
base64.b64encode(
bytes(
json.dumps(client_settings),
encoding='utf-8',
)
),
encoding='utf-8',
)
kwargs['client_settings'] = base64.b64encode(json.dumps(client_settings).encode('utf-8')).decode('utf-8')
kwargs['preferences'] = sxng_request.preferences
kwargs.update(client_settings)