From 60bd8b90f04d5d825fc8ac279cb7fdfde9fe78ea Mon Sep 17 00:00:00 2001
From: Ivan Gabaldon
Date: Sun, 6 Jul 2025 12:27:28 +0200
Subject: [PATCH] [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
---
client/simple/biome.json | 113 +++++-
client/simple/package-lock.json | 344 +++++++++----------
client/simple/package.json | 28 +-
client/simple/src/js/{main => core}/index.ts | 10 +-
client/simple/src/js/core/listener.ts | 5 +
client/simple/src/js/core/router.ts | 38 ++
client/simple/src/js/core/toolkit.ts | 140 ++++++++
client/simple/src/js/main/00_toolkit.ts | 118 -------
client/simple/src/js/main/autocomplete.ts | 129 +++++++
client/simple/src/js/main/infinite_scroll.ts | 72 ++--
client/simple/src/js/main/keyboard.ts | 131 ++++---
client/simple/src/js/main/mapresult.ts | 157 ++++-----
client/simple/src/js/main/preferences.ts | 81 +++--
client/simple/src/js/main/results.ts | 334 +++++++++---------
client/simple/src/js/main/search.ts | 259 ++++----------
client/simple/src/less/code.less | 6 +-
client/simple/src/less/mixins.less | 4 -
client/simple/src/less/rss.less | 8 +-
client/simple/src/less/search.less | 11 -
client/simple/src/less/style-rtl.less | 8 +-
client/simple/src/less/toolkit.less | 38 +-
client/simple/theme_icons.ts | 2 +-
client/simple/tools/img.ts | 40 +--
client/simple/tools/jinja_svg_catalog.ts | 18 +-
client/simple/tools/plg.ts | 6 +-
client/simple/vite.config.ts | 33 +-
searx/templates/simple/base.html | 3 +-
searx/webapp.py | 12 +-
28 files changed, 1109 insertions(+), 1039 deletions(-)
rename client/simple/src/js/{main => core}/index.ts (50%)
create mode 100644 client/simple/src/js/core/listener.ts
create mode 100644 client/simple/src/js/core/router.ts
create mode 100644 client/simple/src/js/core/toolkit.ts
delete mode 100644 client/simple/src/js/main/00_toolkit.ts
create mode 100644 client/simple/src/js/main/autocomplete.ts
diff --git a/client/simple/biome.json b/client/simple/biome.json
index 1c0769fe9..e84fa5dfe 100644
--- a/client/simple/biome.json
+++ b/client/simple/biome.json
@@ -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": {
diff --git a/client/simple/package-lock.json b/client/simple/package-lock.json
index badcdb2d2..8cd59906c 100644
--- a/client/simple/package-lock.json
+++ b/client/simple/package-lock.json
@@ -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": {
diff --git a/client/simple/package.json b/client/simple/package.json
index 1f721cfc8..e1ef66884 100644
--- a/client/simple/package.json
+++ b/client/simple/package.json
@@ -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"
}
}
diff --git a/client/simple/src/js/main/index.ts b/client/simple/src/js/core/index.ts
similarity index 50%
rename from client/simple/src/js/main/index.ts
rename to client/simple/src/js/core/index.ts
index 4dc86b63b..a4021beb9 100644
--- a/client/simple/src/js/main/index.ts
+++ b/client/simple/src/js/core/index.ts
@@ -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";
diff --git a/client/simple/src/js/core/listener.ts b/client/simple/src/js/core/listener.ts
new file mode 100644
index 000000000..fb41cfa88
--- /dev/null
+++ b/client/simple/src/js/core/listener.ts
@@ -0,0 +1,5 @@
+import { listen } from "./toolkit.ts";
+
+listen("click", ".close", function (this: HTMLElement) {
+ (this.parentNode as HTMLElement)?.classList.add("invisible");
+});
diff --git a/client/simple/src/js/core/router.ts b/client/simple/src/js/core/router.ts
new file mode 100644
index 000000000..05c49ed07
--- /dev/null
+++ b/client/simple/src/js/core/router.ts
@@ -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] }
+);
diff --git a/client/simple/src/js/core/toolkit.ts b/client/simple/src/js/core/toolkit.ts
new file mode 100644
index 000000000..0e95eed14
--- /dev/null
+++ b/client/simple/src/js/core/toolkit.ts
@@ -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;
+ 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 => {
+ 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 = (
+ type: string | K,
+ target: string | Document | E,
+ listener: (this: E, event: DocumentEventMap[K]) => void | Promise,
+ 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();
diff --git a/client/simple/src/js/main/00_toolkit.ts b/client/simple/src/js/main/00_toolkit.ts
deleted file mode 100644
index 05cfc4b6b..000000000
--- a/client/simple/src/js/main/00_toolkit.ts
+++ /dev/null
@@ -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;
- [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 => {
- 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: (
- 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");
-});
diff --git a/client/simple/src/js/main/autocomplete.ts b/client/simple/src/js/main/autocomplete.ts
new file mode 100644
index 000000000..c7ed2056b
--- /dev/null
+++ b/client/simple/src/js/main/autocomplete.ts
@@ -0,0 +1,129 @@
+import { assertElement, http, listen, settings } from "../core/toolkit.ts";
+
+const fetchResults = async (qInput: HTMLInputElement, query: string): Promise => {
+ 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(".autocomplete");
+ assertElement(autocomplete);
+
+ const autocompleteList = document.querySelector(".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("#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(".autocomplete");
+const autocompleteList: HTMLUListElement | null = document.querySelector(".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 ?? "";
+ }
+ }
+ }
+ }
+ });
+}
diff --git a/client/simple/src/js/main/infinite_scroll.ts b/client/simple/src/js/main/infinite_scroll.ts
index e9f931e51..5c3350266 100644
--- a/client/simple/src/js/main/infinite_scroll.ts
+++ b/client/simple/src/js/main/infinite_scroll.ts
@@ -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("#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("#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("#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(observedSelector);
- if (nextObservedElement) {
- observer.observe(nextObservedElement);
- }
- });
+ loadNextPage(onlyImages, () => {
+ const nextObservedElement = document.querySelector(observedSelector);
+ if (nextObservedElement) {
+ observer.observe(nextObservedElement);
}
- }, intersectionObserveOptions);
-
- const initialObservedElement = document.querySelector(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(observedSelector);
+if (initialObservedElement) {
+ observer.observe(initialObservedElement);
+}
diff --git a/client/simple/src/js/main/keyboard.ts b/client/simple/src/js/main/keyboard.ts
index 3c6417bbc..46b9bcc20 100644
--- a/client/simple/src/js/main/keyboard.ts
+++ b/client/simple/src/js/main/keyboard.ts
@@ -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 = {
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> =
}
};
-const keyBindings =
- searxng.settings.hotkeys && searxng.settings.hotkeys in keyBindingLayouts
- ? keyBindingLayouts[searxng.settings.hotkeys]
+const keyBindings: Record =
+ 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(".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(".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("#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 => {
}
};
-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");
diff --git a/client/simple/src/js/main/mapresult.ts b/client/simple/src/js/main/mapresult.ts
index 421b41f77..378e1e54f 100644
--- a/client/simple/src/js/main/mapresult.ts
+++ b/client/simple/src/js/main/mapresult.ts
@@ -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);
+ }
+ }
+});
diff --git a/client/simple/src/js/main/preferences.ts b/client/simple/src/js/main/preferences.ts
index 6c66018a6..fb81e6558 100644
--- a/client/simple/src/js/main/preferences.ts
+++ b/client/simple/src/js/main/preferences.ts
@@ -1,9 +1,11 @@
-import { searxng } from "./00_toolkit.ts";
+import { http, listen, settings } from "../core/toolkit.ts";
+
+let engineDescriptions: Record | undefined;
const loadEngineDescriptions = async (): Promise => {
- let engineDescriptions: Record | 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 => {
for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) {
const elements = document.querySelectorAll(`[data-engine-name="${engine_name}"] .engine-description`);
- const sourceText = ` (${searxng.settings.translations?.Source}: ${source})`;
+ const sourceText = ` (${settings.translations?.Source}: ${source})`;
for (const element of elements) {
element.innerHTML = description + sourceText;
@@ -29,43 +31,38 @@ const toggleEngines = (enable: boolean, engineToggles: NodeListOf {
- const engineElements = document.querySelectorAll("[data-engine-name]");
- for (const engineElement of engineElements) {
- searxng.listen("mouseenter", engineElement, loadEngineDescriptions);
- }
+const engineElements: NodeListOf = document.querySelectorAll("[data-engine-name]");
+for (const engineElement of engineElements) {
+ listen("mouseenter", engineElement, loadEngineDescriptions);
+}
- const engineToggles = document.querySelectorAll(
- "tbody input[type=checkbox][class~=checkbox-onoff]"
- );
-
- const enableAllEngines = document.querySelectorAll(".enable-all-engines");
- for (const engine of enableAllEngines) {
- searxng.listen("click", engine, () => toggleEngines(true, engineToggles));
- }
-
- const disableAllEngines = document.querySelectorAll(".disable-all-engines");
- for (const engine of disableAllEngines) {
- searxng.listen("click", engine, () => toggleEngines(false, engineToggles));
- }
-
- const copyHashButton = document.querySelector("#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 = document.querySelectorAll(
+ "tbody input[type=checkbox][class~=checkbox-onoff]"
);
+
+const enableAllEngines: NodeListOf = document.querySelectorAll(".enable-all-engines");
+for (const engine of enableAllEngines) {
+ listen("click", engine, () => toggleEngines(true, engineToggles));
+}
+
+const disableAllEngines: NodeListOf = document.querySelectorAll(".disable-all-engines");
+for (const engine of disableAllEngines) {
+ listen("click", engine, () => toggleEngines(false, engineToggles));
+}
+
+const copyHashButton: HTMLElement | null = document.querySelector("#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);
+ }
+ });
+}
diff --git a/client/simple/src/js/main/results.ts b/client/simple/src/js/main/results.ts
index e278c894a..494f38cbc 100644
--- a/client/simple/src/js/main/results.ts
+++ b/client/simple/src/js/main/results.ts
@@ -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(".result-images-source img");
+ if (!imgElement) return;
+
+ // use thumbnail until full image loads
+ const thumbnail = resultElement.querySelector(".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 =
+ document.querySelectorAll("#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("#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(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(`${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("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 = document.querySelectorAll(".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("#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("#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(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(`${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("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(".result-images-source img");
- if (!img) return;
-
- //
- const src = img.getAttribute("data-src");
- if (!src) return;
-
- // use thumbnail until full image loads
- const thumbnail = resultElement.querySelector(".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(".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
);
diff --git a/client/simple/src/js/main/search.ts b/client/simple/src/js/main/search.ts
index 5e68965b1..508dc702a 100644
--- a/client/simple/src/js/main/search.ts
+++ b/client/simple/src/js/main/search.ts
@@ -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 => {
- 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(".autocomplete");
- assertElement(autocomplete);
+createClearButton(qInput);
- const autocompleteList = document.querySelector(".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("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("#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("#search");
+assertElement(form);
+
+// override form submit action to update the actually selected categories
+listen("submit", form, (event: Event) => {
+ event.preventDefault();
+
+ const categoryValuesInput = document.querySelector("#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(".autocomplete");
- const autocompleteList = document.querySelector(".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("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("#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("#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();
+});
diff --git a/client/simple/src/less/code.less b/client/simple/src/less/code.less
index d83bb1f6f..20d8c3d1e 100644
--- a/client/simple/src/less/code.less
+++ b/client/simple/src/less/code.less
@@ -18,11 +18,7 @@
cursor: default;
&::selection {
- background: transparent; /* WebKit/Blink Browsers */
- }
-
- &::-moz-selection {
- background: transparent; /* Gecko Browsers */
+ background: transparent;
}
margin-right: 8px;
diff --git a/client/simple/src/less/mixins.less b/client/simple/src/less/mixins.less
index a4bae7128..b2e0f6b16 100644
--- a/client/simple/src/less/mixins.less
+++ b/client/simple/src/less/mixins.less
@@ -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;
}
diff --git a/client/simple/src/less/rss.less b/client/simple/src/less/rss.less
index 0bc6622e3..26f960f10 100644
--- a/client/simple/src/less/rss.less
+++ b/client/simple/src/less/rss.less
@@ -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,
diff --git a/client/simple/src/less/search.less b/client/simple/src/less/search.less
index bc49ffadc..07dbf535b 100644
--- a/client/simple/src/less/search.less
+++ b/client/simple/src/less/search.less
@@ -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 {
diff --git a/client/simple/src/less/style-rtl.less b/client/simple/src/less/style-rtl.less
index 7ac1e6e20..b4b4a946f 100644
--- a/client/simple/src/less/style-rtl.less
+++ b/client/simple/src/less/style-rtl.less
@@ -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;
diff --git a/client/simple/src/less/toolkit.less b/client/simple/src/less/toolkit.less
index f9bdbf70c..1782ecdfa 100644
--- a/client/simple/src/less/toolkit.less
+++ b/client/simple/src/less/toolkit.less
@@ -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;
diff --git a/client/simple/theme_icons.ts b/client/simple/theme_icons.ts
index 5bb06a020..0babccb8e 100644
--- a/client/simple/theme_icons.ts
+++ b/client/simple/theme_icons.ts
@@ -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" },
diff --git a/client/simple/tools/img.ts b/client/simple/tools/img.ts
index db4e08645..be27f03fa 100644
--- a/client/simple/tools/img.ts
+++ b/client/simple/tools/img.ts
@@ -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;
}
}
};
diff --git a/client/simple/tools/jinja_svg_catalog.ts b/client/simple/tools/jinja_svg_catalog.ts
index 1fa1a6676..68d9a695d 100644
--- a/client/simple/tools/jinja_svg_catalog.ts
+++ b/client/simple/tools/jinja_svg_catalog.ts
@@ -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 = {};
- 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[] = [];
diff --git a/client/simple/tools/plg.ts b/client/simple/tools/plg.ts
index 2db891d4f..20c2c4e64 100644
--- a/client/simple/tools/plg.ts
+++ b/client/simple/tools/plg.ts
@@ -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);
}
};
diff --git a/client/simple/vite.config.ts b/client/simple/vite.config.ts
index a3c766418..53225f402 100644
--- a/client/simple/vite.config.ts
+++ b/client/simple/vite.config.ts
@@ -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]";
}
}
}
diff --git a/searx/templates/simple/base.html b/searx/templates/simple/base.html
index 2170ccea5..bd2e41a33 100644
--- a/searx/templates/simple/base.html
+++ b/searx/templates/simple/base.html
@@ -2,6 +2,7 @@
+
@@ -82,6 +83,6 @@
{% endfor %}
-
+
diff --git a/searx/webapp.py b/searx/webapp.py
index 4179c32b0..2dd7ddb08 100755
--- a/searx/webapp.py
+++ b/searx/webapp.py
@@ -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)