mirror of
https://github.com/searxng/searxng.git
synced 2026-07-26 09:51:34 +00:00
Compare commits
1 Commits
translatio
...
6cc4350a34
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cc4350a34 |
@@ -1,22 +1,4 @@
|
||||
ARG DEBIAN_CODENAME="bookworm"
|
||||
|
||||
FROM mcr.microsoft.com/devcontainers/base:$DEBIAN_CODENAME
|
||||
|
||||
ARG DEBIAN_CODENAME="bookworm"
|
||||
|
||||
RUN cat <<EOF > /etc/apt/sources.list.d/debian.sources
|
||||
Types: deb
|
||||
URIs: http://deb.debian.org/debian
|
||||
Suites: $DEBIAN_CODENAME $DEBIAN_CODENAME-updates $DEBIAN_CODENAME-backports
|
||||
Components: main
|
||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
|
||||
Types: deb
|
||||
URIs: http://security.debian.org/debian-security
|
||||
Suites: $DEBIAN_CODENAME-security
|
||||
Components: main
|
||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
EOF
|
||||
FROM mcr.microsoft.com/devcontainers/base:debian
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y install python3 python3-venv valkey-server firefox-esr graphviz imagemagick librsvg2-bin fonts-dejavu shellcheck
|
||||
apt-get -y install python3 python3-venv redis firefox-esr graphviz imagemagick librsvg2-bin fonts-dejavu shellcheck
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"build": {
|
||||
"args": {
|
||||
"DEBIAN_CODENAME": "bookworm",
|
||||
},
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"features": {
|
||||
|
||||
163
.dir-locals-template.el
Normal file
163
.dir-locals-template.el
Normal file
@@ -0,0 +1,163 @@
|
||||
;;; .dir-locals.el
|
||||
;;
|
||||
;; Per-Directory Local Variables:
|
||||
;; https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html
|
||||
;;
|
||||
;; For full fledge developer tools install emacs packages:
|
||||
;;
|
||||
;; M-x package-install ...
|
||||
;;
|
||||
;; magit gitconfig
|
||||
;; nvm lsp-mode lsp-pyright lsp-eslint
|
||||
;; pyvenv pylint pip-requirements
|
||||
;; jinja2-mode
|
||||
;; json-mode
|
||||
;; company company-jedi company-quickhelp company-shell
|
||||
;; realgud
|
||||
;; sphinx-doc markdown-mode graphviz-dot-mode
|
||||
;; apache-mode nginx-mode
|
||||
;;
|
||||
;; To setup a developer environment, build target::
|
||||
;;
|
||||
;; $ make node.env.dev pyenv.install
|
||||
;;
|
||||
;; Some buffer locals are referencing the project environment:
|
||||
;;
|
||||
;; - prj-root --> <repo>/
|
||||
;; - nvm-dir --> <repo>/.nvm
|
||||
;; - python-environment-directory --> <repo>/local
|
||||
;; - python-environment-default-root-name --> py3
|
||||
;; - python-shell-virtualenv-root --> <repo>/local/py3
|
||||
;; When this variable is set with the path of the virtualenv to use,
|
||||
;; `process-environment' and `exec-path' get proper values in order to run
|
||||
;; shells inside the specified virtualenv, example::
|
||||
;; (setq python-shell-virtualenv-root "/path/to/env/")
|
||||
;; - python-shell-interpreter --> <repo>/local/py3/bin/python
|
||||
;;
|
||||
;; Python development:
|
||||
;;
|
||||
;; Jedi, flycheck & other python stuff should use the 'python-shell-interpreter'
|
||||
;; from the local py3 environment.
|
||||
;;
|
||||
|
||||
((nil
|
||||
. ((fill-column . 80)
|
||||
(indent-tabs-mode . nil)
|
||||
(eval . (progn
|
||||
|
||||
(add-to-list 'auto-mode-alist '("\\.html\\'" . jinja2-mode))
|
||||
|
||||
;; project root folder is where the `.dir-locals.el' is located
|
||||
(setq-local prj-root
|
||||
(locate-dominating-file default-directory ".dir-locals.el"))
|
||||
|
||||
(setq-local python-environment-directory
|
||||
(expand-file-name "./local" prj-root))
|
||||
|
||||
;; to get in use of NVM environment, install https://github.com/rejeep/nvm.el
|
||||
(setq-local nvm-dir (expand-file-name "./.nvm" prj-root))
|
||||
|
||||
;; use nodejs from the (local) NVM environment (see nvm-dir)
|
||||
(nvm-use-for-buffer)
|
||||
(ignore-errors (require 'lsp))
|
||||
(setq-local lsp-server-install-dir (car (cdr nvm-current-version)))
|
||||
(setq-local lsp-enable-file-watchers nil)
|
||||
|
||||
;; use 'py3' environment as default
|
||||
(setq-local python-environment-default-root-name
|
||||
"py3")
|
||||
|
||||
(setq-local python-shell-virtualenv-root
|
||||
(expand-file-name
|
||||
python-environment-default-root-name python-environment-directory))
|
||||
|
||||
(setq-local python-shell-interpreter
|
||||
(expand-file-name
|
||||
"bin/python" python-shell-virtualenv-root))))))
|
||||
(makefile-gmake-mode
|
||||
. ((indent-tabs-mode . t)))
|
||||
|
||||
(yaml-mode
|
||||
. ((eval . (progn
|
||||
|
||||
;; flycheck should use the local py3 environment
|
||||
(setq-local flycheck-yaml-yamllint-executable
|
||||
(expand-file-name "bin/yamllint" python-shell-virtualenv-root))
|
||||
|
||||
(setq-local flycheck-yamllintrc
|
||||
(expand-file-name ".yamllint.yml" prj-root))
|
||||
|
||||
(flycheck-checker . yaml-yamllint)))))
|
||||
|
||||
(json-mode
|
||||
. ((eval . (progn
|
||||
(setq-local js-indent-level 4)
|
||||
(flycheck-checker . json-python-json)))))
|
||||
|
||||
(js-mode
|
||||
. ((eval . (progn
|
||||
(ignore-errors (require 'lsp-eslint))
|
||||
(setq-local js-indent-level 2)
|
||||
;; flycheck should use the eslint checker from developer tools
|
||||
(setq-local flycheck-javascript-eslint-executable
|
||||
(expand-file-name "node_modules/.bin/eslint" prj-root))
|
||||
;; (flycheck-mode)
|
||||
|
||||
(if (featurep 'lsp-eslint)
|
||||
(lsp))
|
||||
))))
|
||||
|
||||
(python-mode
|
||||
. ((eval . (progn
|
||||
(ignore-errors (require 'jedi-core))
|
||||
(ignore-errors (require 'lsp-pyright))
|
||||
(ignore-errors (sphinx-doc-mode))
|
||||
(setq-local python-environment-virtualenv
|
||||
(list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root)
|
||||
;;"--system-site-packages"
|
||||
"--quiet"))
|
||||
|
||||
(setq-local pylint-command
|
||||
(expand-file-name "bin/pylint" python-shell-virtualenv-root))
|
||||
|
||||
(if (featurep 'lsp-pyright)
|
||||
(lsp))
|
||||
|
||||
;; pylint will find the '.pylintrc' file next to the CWD
|
||||
;; https://pylint.readthedocs.io/en/latest/user_guide/run.html#command-line-options
|
||||
(setq-local flycheck-pylintrc
|
||||
".pylintrc")
|
||||
|
||||
;; flycheck & other python stuff should use the local py3 environment
|
||||
(setq-local flycheck-python-pylint-executable
|
||||
python-shell-interpreter)
|
||||
|
||||
;; use 'M-x jedi:show-setup-info' and 'M-x epc:controller' to inspect jedi server
|
||||
;; https://tkf.github.io/emacs-jedi/latest/#jedi:environment-root -- You
|
||||
;; can specify a full path instead of a name (relative path). In that case,
|
||||
;; python-environment-directory is ignored and Python virtual environment
|
||||
;; is created at the specified path.
|
||||
(setq-local jedi:environment-root
|
||||
python-shell-virtualenv-root)
|
||||
|
||||
;; https://tkf.github.io/emacs-jedi/latest/#jedi:server-command
|
||||
(setq-local jedi:server-command
|
||||
(list python-shell-interpreter
|
||||
jedi:server-script))
|
||||
|
||||
;; jedi:environment-virtualenv --> see above 'python-environment-virtualenv'
|
||||
;; is set buffer local! No need to setup jedi:environment-virtualenv:
|
||||
;;
|
||||
;; Virtualenv command to use. A list of string. If it is nil,
|
||||
;; python-environment-virtualenv is used instead. You must set non-nil
|
||||
;; value to jedi:environment-root in order to make this setting work.
|
||||
;;
|
||||
;; https://tkf.github.io/emacs-jedi/latest/#jedi:environment-virtualenv
|
||||
;;
|
||||
;; (setq-local jedi:environment-virtualenv
|
||||
;; (list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root)
|
||||
;; "--python"
|
||||
;; "/usr/bin/python3.4"
|
||||
;; ))
|
||||
))))
|
||||
)
|
||||
@@ -1,6 +1,44 @@
|
||||
*
|
||||
*~
|
||||
*/*~
|
||||
*/*/*~
|
||||
*/*/*/*~
|
||||
*/*/*/*/*~
|
||||
|
||||
!container/*.template.*
|
||||
!container/entrypoint.sh
|
||||
!searx/**
|
||||
!requirements*.txt
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# CI
|
||||
.codeclimate.yml
|
||||
.travis.yml
|
||||
.taskcluster.yml
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*/__pycache__/
|
||||
*/*/__pycache__/
|
||||
*/*/*/__pycache__/
|
||||
*.py[cod]
|
||||
*/*.py[cod]
|
||||
*/*/*.py[cod]
|
||||
*/*/*/*.py[cod]
|
||||
|
||||
# node_modules
|
||||
node_modules/
|
||||
*/node_modules/
|
||||
*/*/node_modules/
|
||||
*/*/*/node_modules/
|
||||
*/*/*/*/node_modules/
|
||||
|
||||
.tx/
|
||||
|
||||
# to sync with .gitignore
|
||||
geckodriver.log
|
||||
.coverage
|
||||
coverage/
|
||||
cache/
|
||||
build/
|
||||
dist/
|
||||
local/
|
||||
gh-pages/
|
||||
*.egg-info/
|
||||
|
||||
@@ -10,22 +10,11 @@ trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
|
||||
[{*.py,*.pyi}]
|
||||
# code formatter accepts length of 120, but editor should prefer 80
|
||||
max_line_length = 80
|
||||
|
||||
[{*.sh,manage}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# shfmt options
|
||||
shell_variant = bash
|
||||
switch_case_indent = true
|
||||
[*.py]
|
||||
max_line_length = 119
|
||||
|
||||
[*.html]
|
||||
# in the jinja templates we use indent size of 2 and we do not use tabs
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.css]
|
||||
indent_size = 2
|
||||
@@ -36,24 +25,12 @@ indent_size = 2
|
||||
[*.js]
|
||||
indent_size = 2
|
||||
|
||||
[*.ts]
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
insert_final_newline = ignore
|
||||
|
||||
[*.map]
|
||||
indent_size = ignore
|
||||
insert_final_newline = ignore
|
||||
|
||||
# Minified JavaScript files shouldn't be changed
|
||||
[*.min.js]
|
||||
indent_style = ignore
|
||||
insert_final_newline = ignore
|
||||
|
||||
# Minified CSS files shouldn't be changed
|
||||
[*.min.css]
|
||||
[**.min.js]
|
||||
indent_style = ignore
|
||||
insert_final_newline = ignore
|
||||
|
||||
|
||||
65
.github/ISSUE_TEMPLATE/bug-report.md
vendored
65
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -1,50 +1,39 @@
|
||||
---
|
||||
name: "Bug report"
|
||||
about: Report a bug in SearXNG"
|
||||
labels: ["bug"]
|
||||
type: "bug"
|
||||
name: Bug report
|
||||
about: Report a bug in SearXNG
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- PLEASE FILL THESE FIELDS, IT REALLY HELPS THE MAINTAINERS OF SearXNG -->
|
||||
|
||||
_Replace this placeholder with a meaningful and precise description of the bug._
|
||||
**Version of SearXNG, commit number if you are using on master branch and stipulate if you forked SearXNG**
|
||||
<!-- If you are running on master branch using git execute this command
|
||||
in order to fetch the latest commit ID:
|
||||
```
|
||||
git log -1
|
||||
```
|
||||
If you are using searxng-docker then look at the bottom of the SearXNG page
|
||||
and check for the version after "Powered by SearXNG"
|
||||
|
||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
||||
|
||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
||||
Please also stipulate if you are using a forked version of SearXNG and
|
||||
include a link to the fork source code.
|
||||
-->
|
||||
**How did you install SearXNG?**
|
||||
<!-- Did you install SearXNG using the official wiki or using searxng-docker
|
||||
or manually by executing the searx/webapp.py file? -->
|
||||
**What happened?**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
### How To Reproduce?
|
||||
|
||||
<!-- How can we reproduce this issue? (as minimally and as precisely as
|
||||
possible) -->
|
||||
|
||||
### Expected behavior
|
||||
**How To Reproduce**
|
||||
<!-- How can we reproduce this issue? (as minimally and as precisely as possible) -->
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
### Screenshots & Logs
|
||||
|
||||
**Screenshots & Logs**
|
||||
<!-- If applicable, add screenshots, logs to help explain your problem. -->
|
||||
|
||||
### Version of SearXNG
|
||||
|
||||
<!-- Commit number if you are using on master branch and stipulate if you forked
|
||||
SearXNG -->
|
||||
|
||||
<!-- Look at the bottom of the SearXNG page and check for the version after
|
||||
"Powered by SearXNG" If you are using a forked version of SearXNG include a
|
||||
link to the fork source code. -->
|
||||
|
||||
### How did you install SearXNG?
|
||||
|
||||
<!-- Did you install SearXNG using the official documentation or using
|
||||
searxng-docker? -->
|
||||
|
||||
### Additional context
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
|
||||
- [ ] I read the [AI Policy] and hereby confirm that this issue conforms with the policy.
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Questions & Answers (Q&A)
|
||||
url: https://github.com/searxng/searxng/discussions/categories/q-a
|
||||
|
||||
61
.github/ISSUE_TEMPLATE/engine-request.md
vendored
61
.github/ISSUE_TEMPLATE/engine-request.md
vendored
@@ -1,46 +1,31 @@
|
||||
---
|
||||
name: Engine request"
|
||||
about: Request a new engine in SearXNG"
|
||||
labels: ["engine request"]
|
||||
type: "feature"
|
||||
name: Engine request
|
||||
about: Request a new engine in SearXNG
|
||||
title: ''
|
||||
labels: enhancement, engine request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- PLEASE FILL THESE FIELDS, IT REALLY HELPS THE MAINTAINERS OF SearXNG -->
|
||||
|
||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
||||
**Working URL to the engine**
|
||||
<!-- Please check if the engine is responding correctly before submitting it. -->
|
||||
|
||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
||||
-->
|
||||
**Why do you want to add this engine?**
|
||||
<!-- What's special about this engine? Is it open source or libre? -->
|
||||
|
||||
### Working URL to the engine
|
||||
**Features of this engine**
|
||||
<!-- Features of this engine: Doesn't track its users, fast, easy to integrate, ... -->
|
||||
|
||||
<!-- Please check if the engine is responding correctly before submitting -->
|
||||
**How can SearXNG fetch the information from this engine?**
|
||||
<!-- List API URL, example code (using the correct markdown) and more
|
||||
that could be useful for the developers in order to implement this engine.
|
||||
If you don't know what to write, let this part blank. -->
|
||||
|
||||
### Why do you want to add this engine?
|
||||
**Applicable category of this engine**
|
||||
<!-- Where should this new engine fit in SearXNG? Current categories in SearXNG:
|
||||
general, files, images, it, map, music, news, science, social media and videos.
|
||||
You can add multiple categories at the same time. -->
|
||||
|
||||
<!-- What's special about this engine? -->
|
||||
|
||||
### Features of this engine
|
||||
|
||||
<!-- Features of this engine: Serves special content, is fast, is easy to
|
||||
integrate, ... ? -->
|
||||
|
||||
### How can SearXNG fetch results from this engine?
|
||||
|
||||
<!-- List API URL, example code and more that could be useful for the developers
|
||||
in order to implement this engine. If you don't know what to write, let
|
||||
this part blank. -->
|
||||
|
||||
### Applicable category of this engine
|
||||
|
||||
<!-- Where should this new engine fit in SearXNG? Current categories in
|
||||
SearXNG: general, files, images, it, map, music, news, science, social
|
||||
media and videos. -->
|
||||
|
||||
### Additional context
|
||||
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
|
||||
- [ ] I read the [AI Policy] and hereby confirm that this issue conforms with the policy.
|
||||
**Additional context**
|
||||
<!-- Add any other context about this engine here. -->
|
||||
|
||||
37
.github/ISSUE_TEMPLATE/feature-request.md
vendored
37
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@@ -1,32 +1,21 @@
|
||||
---
|
||||
name: "Feature request"
|
||||
about: "Request a new feature in SearXNG"
|
||||
labels: ["new feature"]
|
||||
type: "feature"
|
||||
name: Feature request
|
||||
about: Request a new feature in SearXNG
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- PLEASE FILL THESE FIELDS, IT REALLY HELPS THE MAINTAINERS OF SearXNG -->
|
||||
|
||||
_Replace this placeholder with a concise description of the feature._
|
||||
|
||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
||||
|
||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
||||
-->
|
||||
|
||||
### Is your feature request related to a problem?
|
||||
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always
|
||||
frustrated when [...] -->
|
||||
|
||||
### Describe the solution you'd like
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
### Describe alternatives you've considered
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
|
||||
- [ ] I read the [AI Policy] and hereby confirm that this issue conforms with the policy.
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
|
||||
53
.github/dependabot.yml
vendored
53
.github/dependabot.yml
vendored
@@ -10,12 +10,6 @@ updates:
|
||||
target-branch: "master"
|
||||
commit-message:
|
||||
prefix: "[upd] pypi:"
|
||||
groups:
|
||||
minor:
|
||||
applies-to: version-updates
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/client/simple"
|
||||
@@ -26,50 +20,3 @@ updates:
|
||||
target-branch: "master"
|
||||
commit-message:
|
||||
prefix: "[upd] web-client (simple):"
|
||||
groups:
|
||||
minor:
|
||||
applies-to: version-updates
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "friday"
|
||||
open-pull-requests-limit: 5
|
||||
target-branch: "master"
|
||||
commit-message:
|
||||
prefix: "[upd] searxng.org/devtools (Node.js):"
|
||||
groups:
|
||||
minor:
|
||||
applies-to: version-updates
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "friday"
|
||||
open-pull-requests-limit: 5
|
||||
target-branch: "master"
|
||||
commit-message:
|
||||
prefix: "[upd] searxng.org/devtools (Go):"
|
||||
groups:
|
||||
minor:
|
||||
applies-to: version-updates
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "friday"
|
||||
target-branch: "master"
|
||||
commit-message:
|
||||
prefix: "[upd] github-actions:"
|
||||
|
||||
31
.github/workflows/checker.yml
vendored
Normal file
31
.github/workflows/checker.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: "Checker"
|
||||
on: # yamllint disable-line rule:truthy
|
||||
schedule:
|
||||
- cron: "0 4 * * 5"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
checker:
|
||||
name: Checker
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Ubuntu packages
|
||||
run: |
|
||||
sudo ./utils/searxng.sh install packages
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
architecture: 'x64'
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
make V=1 install
|
||||
|
||||
- name: Checker
|
||||
run: |
|
||||
make search.checker
|
||||
170
.github/workflows/container.yml
vendored
170
.github/workflows/container.yml
vendored
@@ -1,170 +0,0 @@
|
||||
---
|
||||
name: Container
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Integration
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch'
|
||||
|| (github.repository_owner == 'searxng' && github.event.workflow_run.conclusion == 'success')
|
||||
name: Build (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-26.04
|
||||
arch: amd64
|
||||
- runner: ubuntu-26.04-arm
|
||||
arch: arm64
|
||||
- runner: ubuntu-26.04-arm
|
||||
arch: armv7
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
outputs:
|
||||
docker_tag: ${{ steps.build.outputs.docker_tag }}
|
||||
git_url: ${{ steps.build.outputs.git_url }}
|
||||
|
||||
steps:
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: "${{ github.repository_owner }}"
|
||||
password: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||
persist-credentials: "false"
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: |
|
||||
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup cache container
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
key: "container-${{ matrix.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: |
|
||||
container-${{ matrix.arch }}-
|
||||
path: "/var/tmp/buildah-cache-*/*"
|
||||
|
||||
- name: Build
|
||||
id: build
|
||||
env:
|
||||
OVERRIDE_ARCH: "${{ matrix.arch }}"
|
||||
run: make container.build
|
||||
|
||||
test:
|
||||
name: Test (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-26.04
|
||||
arch: amd64
|
||||
- runner: ubuntu-26.04-arm
|
||||
arch: arm64
|
||||
- runner: ubuntu-26.04-arm
|
||||
arch: armv7
|
||||
|
||||
steps:
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: "${{ github.repository_owner }}"
|
||||
password: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Test
|
||||
env:
|
||||
OVERRIDE_ARCH: "${{ matrix.arch }}"
|
||||
GIT_URL: "${{ needs.build.outputs.git_url }}"
|
||||
run: make container.test
|
||||
|
||||
release:
|
||||
if: github.repository_owner == 'searxng' && github.ref_name == 'master'
|
||||
name: Release
|
||||
runs-on: ubuntu-26.04-arm
|
||||
needs:
|
||||
- build
|
||||
- test
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
registry: "docker.io"
|
||||
username: "${{ secrets.DOCKER_USER }}"
|
||||
password: "${{ secrets.DOCKER_TOKEN }}"
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: "${{ github.repository_owner }}"
|
||||
password: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
GIT_URL: "${{ needs.build.outputs.git_url }}"
|
||||
DOCKER_TAG: "${{ needs.build.outputs.docker_tag }}"
|
||||
run: make container.push
|
||||
95
.github/workflows/data-update.yml
vendored
95
.github/workflows/data-update.yml
vendored
@@ -1,27 +1,14 @@
|
||||
---
|
||||
name: Update searx.data
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
name: "Update searx.data"
|
||||
on: # yamllint disable-line rule:truthy
|
||||
schedule:
|
||||
- cron: "59 23 28 * *"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
data:
|
||||
if: github.repository_owner == 'searxng'
|
||||
name: ${{ matrix.fetch }}
|
||||
runs-on: ubuntu-26.04-arm
|
||||
updateData:
|
||||
name: Update data - ${{ matrix.fetch }}
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ github.repository_owner == 'searxng'}}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -33,48 +20,48 @@ jobs:
|
||||
- update_engine_traits.py
|
||||
- update_wikidata_units.py
|
||||
- update_engine_descriptions.py
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: |
|
||||
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
|
||||
path: "./local/"
|
||||
- name: Install Ubuntu packages
|
||||
run: |
|
||||
sudo ./utils/searxng.sh install packages
|
||||
|
||||
- name: Setup venv
|
||||
run: make V=1 install
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
architecture: 'x64'
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
make V=1 install
|
||||
|
||||
- name: Fetch data
|
||||
run: V=1 ./manage pyenv.cmd python "./searxng_extra/update/${{ matrix.fetch }}"
|
||||
env:
|
||||
FETCH_SCRIPT: ./searxng_extra/update/${{ matrix.fetch }}
|
||||
run: |
|
||||
V=1 ./manage pyenv.cmd python "$FETCH_SCRIPT"
|
||||
|
||||
- name: Create PR
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
author: "searxng-bot <searxng-bot@users.noreply.github.com>"
|
||||
committer: "searxng-bot <searxng-bot@users.noreply.github.com>"
|
||||
title: "[mod] data: update searx.data - ${{ matrix.fetch }}"
|
||||
commit-message: "[mod] data: update searx.data - ${{ matrix.fetch }}"
|
||||
branch: "ci-data-${{ matrix.fetch }}"
|
||||
delete-branch: "true"
|
||||
draft: "false"
|
||||
signoff: "false"
|
||||
commit-message: '[data] update searx.data - ${{ matrix.fetch }}'
|
||||
committer: searxng-bot <noreply@github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
||||
signoff: false
|
||||
branch: update_data_${{ matrix.fetch }}
|
||||
delete-branch: true
|
||||
draft: false
|
||||
title: '[data] update searx.data - ${{ matrix.fetch }}'
|
||||
body: |
|
||||
Update searx.data - ${{ matrix.fetch }}
|
||||
update searx.data - ${{ matrix.fetch }}
|
||||
labels: |
|
||||
data
|
||||
|
||||
- name: Check outputs
|
||||
run: |
|
||||
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
|
||||
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
|
||||
|
||||
71
.github/workflows/documentation.yml
vendored
71
.github/workflows/documentation.yml
vendored
@@ -1,71 +0,0 @@
|
||||
---
|
||||
name: Documentation
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch'
|
||||
name: Release
|
||||
runs-on: ubuntu-26.04-arm
|
||||
permissions:
|
||||
# for JamesIves/github-pages-deploy-action to push
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: |
|
||||
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup dependencies
|
||||
run: sudo ./utils/searxng.sh install buildhost
|
||||
|
||||
- name: Setup venv
|
||||
run: make V=1 install
|
||||
|
||||
- name: Build documentation
|
||||
run: make V=1 docs.html
|
||||
|
||||
- if: github.ref_name == 'master'
|
||||
name: Release
|
||||
uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4.8.0
|
||||
with:
|
||||
folder: "dist/docs"
|
||||
branch: "gh-pages"
|
||||
commit-message: "[mod] docs: build from commit ${{ github.sha }}"
|
||||
# Automatically remove deleted files from the deploy branch
|
||||
clean: "true"
|
||||
single-commit: "true"
|
||||
238
.github/workflows/integration.yml
vendored
238
.github/workflows/integration.yml
vendored
@@ -1,103 +1,179 @@
|
||||
---
|
||||
name: Integration
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
on: # yamllint disable-line rule:truthy
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
branches: ["master"]
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
branches: ["master"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
python:
|
||||
name: Python ${{ matrix.python-version }}
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
- "3.14"
|
||||
|
||||
os: [ubuntu-24.04]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "${{ matrix.python-version }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Ubuntu packages
|
||||
run: |
|
||||
sudo ./utils/searxng.sh install packages
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: |
|
||||
python-${{ matrix.python-version }}-${{ runner.arch }}-
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup venv
|
||||
run: make V=1 install
|
||||
|
||||
python-version: ${{ matrix.python-version }}
|
||||
architecture: 'x64'
|
||||
- name: Run tests
|
||||
run: make V=1 ci.test
|
||||
|
||||
theme:
|
||||
name: Theme
|
||||
runs-on: ubuntu-26.04-arm
|
||||
themes:
|
||||
name: Themes
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "26"
|
||||
check-latest: "true"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Ubuntu packages
|
||||
run: sudo ./utils/searxng.sh install buildhost
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: |
|
||||
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup cache Node.js
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
key: "nodejs-${{ runner.arch }}-${{ hashFiles('**/package-lock.json') }}"
|
||||
restore-keys: |
|
||||
nodejs-${{ runner.arch }}-
|
||||
path: "./client/simple/node_modules/"
|
||||
|
||||
- name: Setup venv
|
||||
run: make V=1 install
|
||||
|
||||
- name: Lint
|
||||
run: make themes.lint
|
||||
|
||||
- name: Build
|
||||
python-version: '3.12'
|
||||
architecture: 'x64'
|
||||
- name: Build themes
|
||||
run: make themes.all
|
||||
|
||||
documentation:
|
||||
name: Documentation
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write # for JamesIves/github-pages-deploy-action to push changes in repo
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
persist-credentials: false
|
||||
- name: Install Ubuntu packages
|
||||
run: sudo ./utils/searxng.sh install buildhost
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
architecture: 'x64'
|
||||
- name: Cache Python dependencies
|
||||
id: cache-python
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./local
|
||||
./.nvm
|
||||
./node_modules
|
||||
key: python-ubuntu-24.04-3.12-${{ hashFiles('requirements*.txt', 'setup.py','.nvmrc', 'package.json') }}
|
||||
- name: Build documentation
|
||||
run: |
|
||||
make V=1 docs.clean docs.html
|
||||
- name: Deploy
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
BRANCH: gh-pages
|
||||
FOLDER: dist/docs
|
||||
CLEAN: true # Automatically remove deleted files from the deploy branch
|
||||
SINGLE_COMMIT: true
|
||||
COMMIT_MESSAGE: '[doc] build from commit ${{ github.sha }}'
|
||||
|
||||
babel:
|
||||
name: Update translations branch
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ github.repository_owner == 'searxng' && github.ref == 'refs/heads/master' }}
|
||||
needs:
|
||||
- python
|
||||
- themes
|
||||
- documentation
|
||||
permissions:
|
||||
contents: write # for make V=1 weblate.push.translations
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
token: ${{ secrets.WEBLATE_GITHUB_TOKEN }}
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
architecture: 'x64'
|
||||
- name: Cache Python dependencies
|
||||
id: cache-python
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./local
|
||||
./.nvm
|
||||
./node_modules
|
||||
key: python-ubuntu-20.04-3.12-${{ hashFiles('requirements*.txt', 'setup.py','.nvmrc', 'package.json') }}
|
||||
- name: weblate & git setup
|
||||
env:
|
||||
WEBLATE_CONFIG: ${{ secrets.WEBLATE_CONFIG }}
|
||||
run: |
|
||||
mkdir -p ~/.config
|
||||
echo "${WEBLATE_CONFIG}" > ~/.config/weblate
|
||||
git config --global user.email "searxng-bot@users.noreply.github.com"
|
||||
git config --global user.name "searxng-bot"
|
||||
- name: Update transations
|
||||
id: update
|
||||
run: |
|
||||
make V=1 weblate.push.translations
|
||||
|
||||
dockers:
|
||||
name: Docker
|
||||
if: github.ref == 'refs/heads/master'
|
||||
needs:
|
||||
- python
|
||||
- themes
|
||||
- documentation
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: env.DOCKERHUB_USERNAME != null
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# make sure "make docker.push" can get the git history
|
||||
fetch-depth: '0'
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
architecture: 'x64'
|
||||
- name: Cache Python dependencies
|
||||
id: cache-python
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./local
|
||||
./.nvm
|
||||
./node_modules
|
||||
key: python-ubuntu-20.04-3.12-${{ hashFiles('requirements*.txt', 'setup.py','.nvmrc', 'package.json') }}
|
||||
- name: Set up QEMU
|
||||
if: env.DOCKERHUB_USERNAME != null
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
if: env.DOCKERHUB_USERNAME != null
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Login to DockerHub
|
||||
if: env.DOCKERHUB_USERNAME != null
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
if: env.DOCKERHUB_USERNAME != null
|
||||
run: make -e GIT_URL=$(git remote get-url origin) docker.buildx
|
||||
|
||||
134
.github/workflows/l10n.yml
vendored
134
.github/workflows/l10n.yml
vendored
@@ -1,134 +0,0 @@
|
||||
---
|
||||
name: Translation
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Integration
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: "05 07 * * 5"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
|
||||
jobs:
|
||||
update:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'searxng'
|
||||
name: Update
|
||||
runs-on: ubuntu-26.04-arm
|
||||
permissions:
|
||||
# For "make V=1 weblate.push.translations"
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: |
|
||||
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup venv
|
||||
run: make V=1 install
|
||||
|
||||
- name: Setup Weblate
|
||||
run: |
|
||||
mkdir -p ~/.config
|
||||
echo "${{ secrets.WEBLATE_CONFIG }}" >~/.config/weblate
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.email "searxng-bot@users.noreply.github.com"
|
||||
git config --global user.name "searxng-bot"
|
||||
|
||||
- name: Update translations
|
||||
run: make V=1 weblate.push.translations
|
||||
|
||||
pr:
|
||||
if: |
|
||||
github.repository_owner == 'searxng'
|
||||
&& (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule')
|
||||
name: Pull Request
|
||||
runs-on: ubuntu-26.04-arm
|
||||
permissions:
|
||||
# For "make V=1 weblate.translations.commit"
|
||||
contents: write
|
||||
# For action "peter-evans/create-pull-request"
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: |
|
||||
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup venv
|
||||
run: make V=1 install
|
||||
|
||||
- name: Setup Weblate
|
||||
run: |
|
||||
mkdir -p ~/.config
|
||||
echo "${{ secrets.WEBLATE_CONFIG }}" >~/.config/weblate
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.email "searxng-bot@users.noreply.github.com"
|
||||
git config --global user.name "searxng-bot"
|
||||
|
||||
- name: Merge and push translation updates
|
||||
run: make V=1 weblate.translations.commit
|
||||
|
||||
- name: Create PR
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
author: "searxng-bot <searxng-bot@users.noreply.github.com>"
|
||||
committer: "searxng-bot <searxng-bot@users.noreply.github.com>"
|
||||
title: "[mod] i18n: update translations from Weblate"
|
||||
commit-message: "[mod] i18n: update translations from Weblate"
|
||||
branch: "translations_update"
|
||||
delete-branch: "true"
|
||||
draft: "false"
|
||||
signoff: "false"
|
||||
body: |
|
||||
Update translations from Weblate
|
||||
labels: |
|
||||
area:i18n
|
||||
28
.github/workflows/security.yml
vendored
Normal file
28
.github/workflows/security.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: "Security checks"
|
||||
on: # yamllint disable-line rule:truthy
|
||||
schedule:
|
||||
- cron: "42 05 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
dockers:
|
||||
name: Trivy ${{ matrix.image }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: 'searxng/searxng:latest'
|
||||
ignore-unfixed: false
|
||||
vuln-type: 'os,library'
|
||||
severity: 'UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL'
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
59
.github/workflows/translations-update.yml
vendored
Normal file
59
.github/workflows/translations-update.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: "Update translations"
|
||||
on: # yamllint disable-line rule:truthy
|
||||
schedule:
|
||||
- cron: "05 07 * * 5"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
babel:
|
||||
name: "create PR for additions from weblate"
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ github.repository_owner == 'searxng' && github.ref == 'refs/heads/master' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
token: ${{ secrets.WEBLATE_GITHUB_TOKEN }}
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
architecture: 'x64'
|
||||
- name: Cache Python dependencies
|
||||
id: cache-python
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./local
|
||||
./.nvm
|
||||
./node_modules
|
||||
key: python-ubuntu-24.04-3.12-${{ hashFiles('requirements*.txt', 'setup.py','.nvmrc', 'package.json') }}
|
||||
- name: weblate & git setup
|
||||
env:
|
||||
WEBLATE_CONFIG: ${{ secrets.WEBLATE_CONFIG }}
|
||||
run: |
|
||||
mkdir -p ~/.config
|
||||
echo "${WEBLATE_CONFIG}" > ~/.config/weblate
|
||||
git config --global user.email "searxng-bot@users.noreply.github.com"
|
||||
git config --global user.name "searxng-bot"
|
||||
- name: Merge and push transation updates
|
||||
run: |
|
||||
make V=1 weblate.translations.commit
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
token: ${{ secrets.WEBLATE_GITHUB_TOKEN }}
|
||||
commit-message: '[l10n] update translations from Weblate'
|
||||
committer: searxng-bot <searxng-bot@users.noreply.github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
||||
signoff: false
|
||||
branch: translations_update
|
||||
delete-branch: true
|
||||
draft: false
|
||||
title: '[l10n] update translations from Weblate'
|
||||
body: |
|
||||
update translations from Weblate
|
||||
labels: |
|
||||
translation
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,7 +9,6 @@ geckodriver.log
|
||||
.coverage
|
||||
coverage/
|
||||
|
||||
.govm/
|
||||
.nvm/
|
||||
cache/
|
||||
build/
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
[[language]]
|
||||
name = "python"
|
||||
language-servers = ["basedpyright", "pylsp"]
|
||||
auto-format = true
|
||||
|
||||
[language-server.pylsp.config.pylsp]
|
||||
plugins.pylint.enabled = true
|
||||
plugins.isort.enabled = true
|
||||
plugins.black.enabled = true
|
||||
plugins.black.skip_string_normalization = true
|
||||
plugins.black.line_length = 120
|
||||
@@ -162,7 +162,7 @@ no-docstring-rgx=^_
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Regular expression matching correct variable names
|
||||
variable-rgx=([a-zA-Z0-9_]*)$
|
||||
variable-rgx=(([a-z][a-zA-Z0-9_]{2,30})|(_[a-z0-9_]*)|([a-z]))$
|
||||
|
||||
|
||||
[FORMAT]
|
||||
@@ -311,7 +311,7 @@ dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=yes
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
|
||||
4
.tool-versions
Normal file
4
.tool-versions
Normal file
@@ -0,0 +1,4 @@
|
||||
nodejs 23.5.0
|
||||
python 3.13.1
|
||||
shellcheck 0.10.0
|
||||
sqlite 3.47.2
|
||||
@@ -1,20 +0,0 @@
|
||||
.. SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
AI Policy
|
||||
=========
|
||||
|
||||
Restrictions on Generative AI Usage
|
||||
-----------------------------------
|
||||
- **All AI usage in any form must be disclosed.** You must state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted.
|
||||
- **The human-in-the-loop must fully understand all code.** If you use generative AI tools as an aid in developing code or documentation changes, ensure that you fully understand the proposed changes and can explain why they are the correct approach.
|
||||
- **AI should never be the main author of the PR.** AI may be used as a tool to help with developing, but the human contribution to the code changes should always be reasonably larger than the part written by AI. For example, you should be the one that decides about the structure of the PR, not the LLM.
|
||||
- **Issues and PR descriptions must be fully human-written.** Do not post output from Large Language Models or similar generative AI as comments on any of our discussion forums (e.g. GitHub Issues, Matrix, ...), as such comments tend to be formulaic and low content. If you're a not a native English speaker, using AI for translating self-written issue texts to English is okay, but please keep the wording as close as possible to the original wording.
|
||||
- **Bad AI drivers will be denounced.** People who produce bad contributions that are clearly AI (slop) will be blocked for all future contributions.
|
||||
|
||||
There are Humans Here
|
||||
---------------------
|
||||
Every discussion, issue, and pull request is read and reviewed by humans. It is a boundary point at which people interact with each other and the work done. It is rude and disrespectful to approach this boundary with low-effort, unqualified work, since it puts the burden of validation on the maintainer.
|
||||
|
||||
It takes a lot of maintainer time and energy to review AI-generated contributions! Sending the output of an LLM to open source project maintainers extracts work from them in the form of design and code review, so we call this kind of contribution an "extractive contribution".
|
||||
|
||||
The *golden rule* is that a contribution should be worth more to the project than the time it takes to review it, which is usually not the case if large parts of your PR were written by LLMs.
|
||||
@@ -2,7 +2,7 @@ searxng is a fork from `searx <https://github.com/searx/searx>`_ and is
|
||||
maintained by Markus Heiser (`@return42 <https://github.com/return42>`_)
|
||||
|
||||
People who have submitted patches/translations, reported bugs, consulted
|
||||
features or generally made SearXNG better:
|
||||
features or generally made searx better:
|
||||
|
||||
- Adam Tauber `@asciimoo <https://github.com/asciimoo>`_
|
||||
- Matej Cotman `@matejc <https://github.com/matejc>`_
|
||||
@@ -177,5 +177,3 @@ features or generally made SearXNG better:
|
||||
- Daniel Mowitz `<https://daniel.mowitz.rocks>`
|
||||
- `Bearz314 <https://github.com/bearz314>`_
|
||||
- Tommaso Colella `<https://github.com/gioleppe>`
|
||||
- @AgentScrubbles
|
||||
- Filip Mikina `<https://github.com/fiffek>`
|
||||
|
||||
49
CONTRIBUTING.md
Normal file
49
CONTRIBUTING.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# How to contribute
|
||||
|
||||
## Resources in the documentation
|
||||
|
||||
* [Development quickstart](https://docs.searxng.org/dev/quickstart.html)
|
||||
* [Contribution guide](https://docs.searxng.org/dev/contribution_guide.html)
|
||||
|
||||
## Submitting PRs
|
||||
|
||||
Please follow the provided PR template when writing a description for your changes.
|
||||
|
||||
Do not take criticism personally. When you get feedback, it is about your work,
|
||||
not your character, personality, etc. Keep in mind we all want to make the project better.
|
||||
|
||||
When something is not clear, please ask questions to clear things up.
|
||||
|
||||
If you would like to introduce a big architectural changes or do a refactoring
|
||||
either in the codebase or the development tools, please open an issue with a proposal
|
||||
first. This way we can think together about the problem and probably come up
|
||||
with a better solution.
|
||||
|
||||
## Coding conventions and guidelines
|
||||
|
||||
### Commit messages
|
||||
|
||||
* Always write descriptive commit messages ("fix bug" is not acceptable).
|
||||
* Use the present tense ("Add feature" not "Added feature").
|
||||
* Use the imperative mood ("Move cursor to..." not "Moves cursor to...").
|
||||
* Limit the first line to 72 characters or less.
|
||||
* Include the number of the issue you are fixing.
|
||||
|
||||
### Coding guidelines
|
||||
|
||||
As a Python project, we must follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) and [PEP 20](https://www.python.org/dev/peps/pep-0020/) guidelines.
|
||||
|
||||
Furthermore, follow the Clean code conventions. The most important
|
||||
in this project are the following rules:
|
||||
|
||||
* Simpler is better. [KISS principle](https://en.wikipedia.org/wiki/KISS_principle)
|
||||
* Be consistent.
|
||||
* Every function must do one thing.
|
||||
* Use descriptive names for functions and variables.
|
||||
* Always look for the root cause.
|
||||
* Keep configurable data high level.
|
||||
* Avoid negative conditionals.
|
||||
* Prefer fewer arguments.
|
||||
* Do not add obvious comment to code.
|
||||
* Do not comment out code, just delete lines.
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
.. SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.. _Quickstart guide: https://docs.searxng.org/dev/quickstart.html
|
||||
.. _Commits guide: https://docs.searxng.org/dev/commits.html
|
||||
.. _AI Policy: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
.. _Weblate: https://translate.codeberg.org/projects/searxng/searxng/
|
||||
.. _GitHub Codespaces: https://docs.github.com/en/codespaces/overview
|
||||
.. _120 hours per month: https://github.com/settings/billing
|
||||
.. _list of existing Codespaces: https://github.com/codespaces
|
||||
|
||||
Thank you for your interest in SearXNG.
|
||||
|
||||
Have a look at our `Quickstart guide`_, it's very easy to contribute.
|
||||
|
||||
Further information on *how-to* can be found
|
||||
`here <https://docs.searxng.org/dev/index.html>`_.
|
||||
|
||||
Translations
|
||||
============
|
||||
|
||||
Help translate SearXNG at `Weblate`_.
|
||||
|
||||
.. image:: https://translate.codeberg.org/widget/searxng/searxng/horizontal-auto.svg
|
||||
:target: https://translate.codeberg.org/engage/searxng/
|
||||
:alt: Weblate
|
||||
:width: 768px
|
||||
|
||||
Cloud development
|
||||
=================
|
||||
|
||||
You can contribute from your browser using `GitHub Codespaces`_:
|
||||
|
||||
- Fork the repository.
|
||||
- Click on the ``<> Code`` green button.
|
||||
- Click on the ``Codespaces`` tab instead of ``Local``.
|
||||
- Click on ``Create codespace on master``.
|
||||
- VSCode is going to start in the browser.
|
||||
- Wait for ``git pull && make install`` to appear and then disappear.
|
||||
- You have `120 hours per month`_ (see also your `list of existing Codespaces`_).
|
||||
- You can start SearXNG using ``make run`` in the terminal or by pressing ``Ctrl+Shift+B``.
|
||||
|
||||
How-to contribute
|
||||
=================
|
||||
|
||||
Submitting pull requests
|
||||
------------------------
|
||||
|
||||
Please follow the provided PR template when writing a description for your
|
||||
changes.
|
||||
|
||||
Do not take criticism personally. When you get feedback, it is about your work,
|
||||
not your character or personality. Keep in mind we all want to make SearXNG
|
||||
better.
|
||||
|
||||
When something is not clear, please ask questions to clear things up.
|
||||
|
||||
If you would like to introduce a big architectural change or do a refactor,
|
||||
either in the codebase or the development tooling, please open an issue with a
|
||||
proposal first. This way we can think together about the problem and probably
|
||||
come up with a better solution.
|
||||
|
||||
Coding conventions and guidelines
|
||||
---------------------------------
|
||||
|
||||
Commit messages
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
- Always write descriptive commit messages *("fix bug" is not acceptable)*.
|
||||
- Use the present tense *("Add feature", not "Added feature")*.
|
||||
- Use the imperative mood *("Move cursor to...", not "Moves cursor to...")*.
|
||||
- Limit the first line (commit title) to 72 characters or less.
|
||||
|
||||
See `Commits guide`_ for more details.
|
||||
|
||||
Coding guidelines
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
As a Python project, we must follow `PEP 8 <https://www.python.org/dev/peps/pep-0008/>`_
|
||||
and `PEP 20 <https://www.python.org/dev/peps/pep-0020/>`_ guidelines.
|
||||
|
||||
Furthermore, follow Clean Code conventions. The most important
|
||||
rules in this project are:
|
||||
|
||||
- Simpler is better. `KISS principle <https://en.wikipedia.org/wiki/KISS_principle>`_
|
||||
- Be consistent.
|
||||
- Every function must do one thing.
|
||||
- Use descriptive names for functions and variables.
|
||||
- Always look for the root cause.
|
||||
- Keep configurable data high level.
|
||||
- Avoid negative conditionals.
|
||||
- Prefer fewer arguments.
|
||||
- Do not add obvious comments to code.
|
||||
- Do not comment out code, delete lines instead.
|
||||
|
||||
AI Policy
|
||||
~~~~~~~~~
|
||||
|
||||
For our policy on the use of AI tools, please read `AI Policy`_.
|
||||
92
Dockerfile
Normal file
92
Dockerfile
Normal file
@@ -0,0 +1,92 @@
|
||||
FROM alpine:3.20
|
||||
ENTRYPOINT ["/sbin/tini","--","/usr/local/searxng/dockerfiles/docker-entrypoint.sh"]
|
||||
EXPOSE 8080
|
||||
VOLUME /etc/searxng
|
||||
|
||||
ARG SEARXNG_GID=977
|
||||
ARG SEARXNG_UID=977
|
||||
|
||||
RUN addgroup -g ${SEARXNG_GID} searxng && \
|
||||
adduser -u ${SEARXNG_UID} -D -h /usr/local/searxng -s /bin/sh -G searxng searxng
|
||||
|
||||
ENV INSTANCE_NAME=searxng \
|
||||
AUTOCOMPLETE= \
|
||||
BASE_URL= \
|
||||
BIND_ADDRESS=[::]:8080 \
|
||||
MORTY_KEY= \
|
||||
MORTY_URL= \
|
||||
SEARXNG_SETTINGS_PATH=/etc/searxng/settings.yml \
|
||||
UWSGI_SETTINGS_PATH=/etc/searxng/uwsgi.ini \
|
||||
UWSGI_WORKERS=%k \
|
||||
UWSGI_THREADS=4
|
||||
|
||||
WORKDIR /usr/local/searxng
|
||||
|
||||
COPY requirements.txt ./requirements.txt
|
||||
|
||||
RUN apk add --no-cache -t build-dependencies \
|
||||
build-base \
|
||||
py3-setuptools \
|
||||
python3-dev \
|
||||
libffi-dev \
|
||||
libxslt-dev \
|
||||
libxml2-dev \
|
||||
openssl-dev \
|
||||
tar \
|
||||
git \
|
||||
&& apk add --no-cache \
|
||||
ca-certificates \
|
||||
python3 \
|
||||
py3-pip \
|
||||
libxml2 \
|
||||
libxslt \
|
||||
openssl \
|
||||
tini \
|
||||
uwsgi \
|
||||
uwsgi-python3 \
|
||||
brotli \
|
||||
&& pip3 install --break-system-packages --no-cache -r requirements.txt \
|
||||
&& apk del build-dependencies \
|
||||
&& rm -rf /root/.cache
|
||||
|
||||
COPY --chown=searxng:searxng dockerfiles ./dockerfiles
|
||||
COPY --chown=searxng:searxng searx ./searx
|
||||
|
||||
ARG TIMESTAMP_SETTINGS=0
|
||||
ARG TIMESTAMP_UWSGI=0
|
||||
ARG VERSION_GITCOMMIT=unknown
|
||||
|
||||
RUN su searxng -c "/usr/bin/python3 -m compileall -q searx" \
|
||||
&& touch -c --date=@${TIMESTAMP_SETTINGS} searx/settings.yml \
|
||||
&& touch -c --date=@${TIMESTAMP_UWSGI} dockerfiles/uwsgi.ini \
|
||||
&& find /usr/local/searxng/searx/static -a \( -name '*.html' -o -name '*.css' -o -name '*.js' \
|
||||
-o -name '*.svg' -o -name '*.ttf' -o -name '*.eot' \) \
|
||||
-type f -exec gzip -9 -k {} \+ -exec brotli --best {} \+
|
||||
|
||||
HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:8080/healthz || exit 1
|
||||
|
||||
# Keep these arguments at the end to prevent redundant layer rebuilds
|
||||
ARG LABEL_DATE=
|
||||
ARG GIT_URL=unknown
|
||||
ARG SEARXNG_GIT_VERSION=unknown
|
||||
ARG SEARXNG_DOCKER_TAG=unknown
|
||||
ARG LABEL_VCS_REF=
|
||||
ARG LABEL_VCS_URL=
|
||||
LABEL maintainer="searxng <${GIT_URL}>" \
|
||||
description="A privacy-respecting, hackable metasearch engine." \
|
||||
version="${SEARXNG_GIT_VERSION}" \
|
||||
org.label-schema.schema-version="1.0" \
|
||||
org.label-schema.name="searxng" \
|
||||
org.label-schema.version="${SEARXNG_GIT_VERSION}" \
|
||||
org.label-schema.url="${LABEL_VCS_URL}" \
|
||||
org.label-schema.vcs-ref=${LABEL_VCS_REF} \
|
||||
org.label-schema.vcs-url=${LABEL_VCS_URL} \
|
||||
org.label-schema.build-date="${LABEL_DATE}" \
|
||||
org.label-schema.usage="https://github.com/searxng/searxng-docker" \
|
||||
org.opencontainers.image.title="searxng" \
|
||||
org.opencontainers.image.version="${SEARXNG_DOCKER_TAG}" \
|
||||
org.opencontainers.image.url="${LABEL_VCS_URL}" \
|
||||
org.opencontainers.image.revision=${LABEL_VCS_REF} \
|
||||
org.opencontainers.image.source=${LABEL_VCS_URL} \
|
||||
org.opencontainers.image.created="${LABEL_DATE}" \
|
||||
org.opencontainers.image.documentation="https://github.com/searxng/searxng-docker"
|
||||
45
Makefile
45
Makefile
@@ -17,6 +17,7 @@ help:
|
||||
@echo 'install - developer install of SearxNG into virtualenv'
|
||||
@echo 'uninstall - uninstall developer installation'
|
||||
@echo 'clean - clean up working tree'
|
||||
@echo 'search.checker - check search engines'
|
||||
@echo 'test - run shell & CI tests'
|
||||
@echo 'test.shell - test shell scripts'
|
||||
@echo 'ci.test - run CI tests'
|
||||
@@ -31,53 +32,61 @@ install uninstall:
|
||||
$(Q)./manage pyenv.$@
|
||||
|
||||
PHONY += clean
|
||||
clean: py.clean docs.clean node.clean nvm.clean go.clean test.clean
|
||||
clean: py.clean docs.clean node.clean nvm.clean test.clean
|
||||
$(Q)./manage build_msg CLEAN "common files"
|
||||
$(Q)find . -name '*.orig' -exec rm -f {} +
|
||||
$(Q)find . -name '*.rej' -exec rm -f {} +
|
||||
$(Q)find . -name '*~' -exec rm -f {} +
|
||||
$(Q)find . -name '*.bak' -exec rm -f {} +
|
||||
|
||||
lxc.clean:
|
||||
$(Q)rm -rf lxc-env
|
||||
|
||||
PHONY += search.checker search.checker.%
|
||||
search.checker: install
|
||||
$(Q)./manage pyenv.cmd searxng-checker -v
|
||||
|
||||
search.checker.%: install
|
||||
$(Q)./manage pyenv.cmd searxng-checker -v "$(subst _, ,$(patsubst search.checker.%,%,$@))"
|
||||
|
||||
PHONY += test ci.test test.shell
|
||||
test: test.yamllint test.black test.pyright_modified test.pylint test.unit test.robot test.rst test.shell test.shfmt
|
||||
ci.test: test test.pybabel
|
||||
ci.test: test.yamllint test.black test.types.ci test.pylint test.unit test.robot test.rst test.shell test.pybabel
|
||||
test: test.yamllint test.black test.types.dev test.pylint test.unit test.robot test.rst test.shell
|
||||
test.shell:
|
||||
$(Q)shellcheck -x -s dash \
|
||||
container/entrypoint.sh
|
||||
dockerfiles/docker-entrypoint.sh
|
||||
$(Q)shellcheck -x -s bash \
|
||||
utils/brand.sh \
|
||||
$(MTOOLS) \
|
||||
utils/lib.sh \
|
||||
utils/lib_sxng*.sh \
|
||||
utils/lib_govm.sh \
|
||||
utils/lib_go.sh \
|
||||
utils/lib_nvm.sh \
|
||||
utils/lib_redis.sh \
|
||||
utils/lib_valkey.sh \
|
||||
utils/searxng.sh
|
||||
utils/searxng.sh \
|
||||
utils/lxc.sh \
|
||||
utils/lxc-searxng.env \
|
||||
utils/searx.sh \
|
||||
utils/filtron.sh \
|
||||
utils/morty.sh
|
||||
$(Q)$(MTOOLS) build_msg TEST "$@ OK"
|
||||
|
||||
PHONY += format
|
||||
format: format.python format.shell
|
||||
|
||||
# wrap ./manage script
|
||||
|
||||
MANAGE += weblate.translations.commit weblate.push.translations
|
||||
MANAGE += data.all data.traits data.useragents data.locales data.currencies
|
||||
MANAGE += docs.html docs.live docs.gh-pages docs.prebuild docs.clean
|
||||
MANAGE += podman.build
|
||||
MANAGE += docker.build docker.buildx
|
||||
MANAGE += container.build container.test container.push
|
||||
MANAGE += docker.build docker.push docker.buildx
|
||||
MANAGE += gecko.driver
|
||||
MANAGE += node.env node.env.dev node.clean
|
||||
MANAGE += py.build py.clean
|
||||
MANAGE += pyenv pyenv.install pyenv.uninstall
|
||||
MANAGE += format.python format.shell
|
||||
MANAGE += test.yamllint test.pylint test.black test.pybabel test.unit test.coverage test.robot test.rst test.clean test.themes test.pyright test.pyright_modified test.shfmt
|
||||
MANAGE += themes.all themes.simple themes.simple.analyze themes.fix themes.lint themes.test
|
||||
MANAGE += format.python
|
||||
MANAGE += test.yamllint test.pylint test.black test.pybabel test.unit test.coverage test.robot test.rst test.clean test.themes test.types.dev test.types.ci
|
||||
MANAGE += themes.all themes.fix themes.test
|
||||
MANAGE += static.build.commit static.build.drop static.build.restore
|
||||
MANAGE += nvm.install nvm.clean nvm.status nvm.nodejs
|
||||
MANAGE += go.env.dev go.clean
|
||||
|
||||
PHONY += $(MANAGE)
|
||||
|
||||
@@ -86,8 +95,8 @@ $(MANAGE):
|
||||
|
||||
# short hands of selected targets
|
||||
|
||||
PHONY += docs container themes
|
||||
PHONY += docs docker themes
|
||||
|
||||
docs: docs.html
|
||||
container: container.build
|
||||
docker: docker.build
|
||||
themes: themes.all
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
||||
## What does this PR do?
|
||||
|
||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
||||
-->
|
||||
<!-- MANDATORY -->
|
||||
|
||||
### What does this PR do?
|
||||
<!-- explain the changes in your PR, algorithms, design, architecture -->
|
||||
|
||||
<!-- Explain the motivation and changes in your pull request. -->
|
||||
## Why is this change important?
|
||||
|
||||
### How to test this PR locally?
|
||||
<!-- MANDATORY -->
|
||||
|
||||
<!-- Commands to run the tests or instructions to test the changes. Are there
|
||||
any edge cases (environment, language, or other contexts) to take into
|
||||
account? -->
|
||||
<!-- explain the motivation behind your PR -->
|
||||
|
||||
### Related issues
|
||||
## How to test this PR locally?
|
||||
|
||||
<!-- commands to run the tests or instructions to test the changes -->
|
||||
|
||||
## Author's checklist
|
||||
|
||||
<!-- additional notes for reviewers -->
|
||||
|
||||
## Related issues
|
||||
|
||||
<!--
|
||||
Closes: #234
|
||||
Closes #234
|
||||
-->
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
<!-- ⚠️ Bad AI drivers will be denounced: People who produce bad contributions
|
||||
that are clearly AI (slop) will be blocked for all future contributions.
|
||||
-->
|
||||
|
||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
|
||||
- [ ] **I hereby confirm that this PR conforms with the [AI Policy].**
|
||||
|
||||
If I have used AI tools for working on the changes in this PR, I will
|
||||
attach a list of all AI tools I used and how I used them. I hereby confirm
|
||||
that I haven't used any other tools than the ones I mention below.
|
||||
|
||||
134
README.rst
134
README.rst
@@ -1,62 +1,130 @@
|
||||
.. SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.. _metasearch engine: https://en.wikipedia.org/wiki/Metasearch_engine
|
||||
.. _Installation guide: https://docs.searxng.org/admin/installation.html
|
||||
.. _Configuration guide: https://docs.searxng.org/admin/settings/index.html
|
||||
.. _CONTRIBUTING: https://github.com/searxng/searxng/blob/master/CONTRIBUTING.rst
|
||||
.. _LICENSE: https://github.com/searxng/searxng/blob/master/LICENSE
|
||||
----
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/searxng/searxng/master/client/simple/src/brand/searxng.svg
|
||||
:target: https://searxng.org
|
||||
:target: https://docs.searxng.org/
|
||||
:alt: SearXNG
|
||||
:width: 512px
|
||||
:width: 100%
|
||||
:align: center
|
||||
|
||||
----
|
||||
|
||||
SearXNG is a `metasearch engine`_. Users are neither tracked nor profiled.
|
||||
Privacy-respecting, hackable `metasearch engine`_
|
||||
|
||||
.. image:: https://img.shields.io/badge/organization-3050ff?style=flat-square&logo=searxng&logoColor=fff&cacheSeconds=86400
|
||||
:target: https://github.com/searxng
|
||||
:alt: Organization
|
||||
Searx.space_ lists ready-to-use running instances.
|
||||
|
||||
.. image:: https://img.shields.io/badge/documentation-3050ff?style=flat-square&logo=readthedocs&logoColor=fff&cacheSeconds=86400
|
||||
:target: https://docs.searxng.org
|
||||
:alt: Documentation
|
||||
A user_, admin_ and developer_ handbook is available on the homepage_.
|
||||
|
||||
.. image:: https://img.shields.io/github/license/searxng/searxng?style=flat-square&label=license&color=3050ff&cacheSeconds=86400
|
||||
|SearXNG install|
|
||||
|SearXNG homepage|
|
||||
|SearXNG wiki|
|
||||
|AGPL License|
|
||||
|Issues|
|
||||
|commits|
|
||||
|weblate|
|
||||
|SearXNG logo|
|
||||
|
||||
----
|
||||
|
||||
.. _searx.space: https://searx.space
|
||||
.. _user: https://docs.searxng.org/user
|
||||
.. _admin: https://docs.searxng.org/admin
|
||||
.. _developer: https://docs.searxng.org/dev
|
||||
.. _homepage: https://docs.searxng.org/
|
||||
.. _metasearch engine: https://en.wikipedia.org/wiki/Metasearch_engine
|
||||
|
||||
.. |SearXNG logo| image:: https://raw.githubusercontent.com/searxng/searxng/master/client/simple/src/brand/searxng-wordmark.svg
|
||||
:target: https://docs.searxng.org/
|
||||
:width: 5%
|
||||
|
||||
.. |SearXNG install| image:: https://img.shields.io/badge/-install-blue
|
||||
:target: https://docs.searxng.org/admin/installation.html
|
||||
|
||||
.. |SearXNG homepage| image:: https://img.shields.io/badge/-homepage-blue
|
||||
:target: https://docs.searxng.org/
|
||||
|
||||
.. |SearXNG wiki| image:: https://img.shields.io/badge/-wiki-blue
|
||||
:target: https://github.com/searxng/searxng/wiki
|
||||
|
||||
.. |AGPL License| image:: https://img.shields.io/badge/license-AGPL-blue.svg
|
||||
:target: https://github.com/searxng/searxng/blob/master/LICENSE
|
||||
:alt: License
|
||||
|
||||
.. image:: https://img.shields.io/github/commit-activity/y/searxng/searxng/master?style=flat-square&label=commits&color=3050ff&cacheSeconds=3600
|
||||
:target: https://github.com/searxng/searxng/commits/master/
|
||||
:alt: Commits
|
||||
.. |Issues| image:: https://img.shields.io/github/issues/searxng/searxng?color=yellow&label=issues
|
||||
:target: https://github.com/searxng/searxng/issues
|
||||
|
||||
.. image:: https://img.shields.io/weblate/progress/searxng?server=https%3A%2F%2Ftranslate.codeberg.org&style=flat-square&label=translated&color=3050ff&cacheSeconds=86400
|
||||
.. |PR| image:: https://img.shields.io/github/issues-pr-raw/searxng/searxng?color=yellow&label=PR
|
||||
:target: https://github.com/searxng/searxng/pulls
|
||||
|
||||
.. |commits| image:: https://img.shields.io/github/commit-activity/y/searxng/searxng?color=yellow&label=commits
|
||||
:target: https://github.com/searxng/searxng/commits/master
|
||||
|
||||
.. |weblate| image:: https://translate.codeberg.org/widgets/searxng/-/searxng/svg-badge.svg
|
||||
:target: https://translate.codeberg.org/projects/searxng/
|
||||
:alt: Translated
|
||||
|
||||
|
||||
Contact
|
||||
=======
|
||||
|
||||
Ask questions or chat with the SearXNG community (this not a chatbot) on
|
||||
|
||||
IRC
|
||||
`#searxng on libera.chat <https://web.libera.chat/?channel=#searxng>`_
|
||||
which is bridged to Matrix.
|
||||
|
||||
Matrix
|
||||
`#searxng:matrix.org <https://matrix.to/#/#searxng:matrix.org>`_
|
||||
|
||||
|
||||
Setup
|
||||
=====
|
||||
|
||||
To install SearXNG, see `Installation guide`_.
|
||||
- A well maintained `Docker image`_, also built for ARM64 and ARM/v7
|
||||
architectures.
|
||||
- Alternatively there are *up to date* `installation scripts`_.
|
||||
- For individual setup consult our detailed `Step by step`_ instructions.
|
||||
- To fine-tune your instance, take a look at the `Administrator documentation`_.
|
||||
|
||||
To fine-tune SearXNG, see `Configuration guide`_.
|
||||
.. _Administrator documentation: https://docs.searxng.org/admin/index.html
|
||||
.. _Step by step: https://docs.searxng.org/admin/installation-searxng.html
|
||||
.. _installation scripts: https://docs.searxng.org/admin/installation-scripts.html
|
||||
.. _Docker image: https://github.com/searxng/searxng-docker
|
||||
|
||||
Further information on *how-to* can be found `here <https://docs.searxng.org/admin/index.html>`_.
|
||||
Translations
|
||||
============
|
||||
|
||||
Connect
|
||||
=======
|
||||
.. _Weblate: https://translate.codeberg.org/projects/searxng/searxng/
|
||||
|
||||
If you have questions or want to connect with others in the community:
|
||||
Help translate SearXNG at `Weblate`_
|
||||
|
||||
.. figure:: https://translate.codeberg.org/widgets/searxng/-/multi-auto.svg
|
||||
:target: https://translate.codeberg.org/projects/searxng/
|
||||
|
||||
- `#searxng:matrix.org <https://matrix.to/#/#searxng:matrix.org>`_
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
See CONTRIBUTING_ for more details.
|
||||
.. _development quickstart: https://docs.searxng.org/dev/quickstart.html
|
||||
.. _developer documentation: https://docs.searxng.org/dev/index.html
|
||||
|
||||
License
|
||||
=======
|
||||
Are you a developer? Have a look at our `development quickstart`_ guide, it's
|
||||
very easy to contribute. Additionally we have a `developer documentation`_.
|
||||
|
||||
This project is licensed under the GNU Affero General Public License (AGPL-3.0).
|
||||
See LICENSE_ for more details.
|
||||
|
||||
Codespaces
|
||||
==========
|
||||
|
||||
You can contribute from your browser using `GitHub Codespaces`_:
|
||||
|
||||
- Fork the repository
|
||||
- Click on the ``<> Code`` green button
|
||||
- Click on the ``Codespaces`` tab instead of ``Local``
|
||||
- Click on ``Create codespace on master``
|
||||
- VSCode is going to start in the browser
|
||||
- Wait for ``git pull && make install`` to appear and then disappear
|
||||
- You have `120 hours per month`_ (see also your `list of existing Codespaces`_)
|
||||
- You can start SearXNG using ``make run`` in the terminal or by pressing ``Ctrl+Shift+B``
|
||||
|
||||
.. _GitHub Codespaces: https://docs.github.com/en/codespaces/overview
|
||||
.. _120 hours per month: https://github.com/settings/billing
|
||||
.. _list of existing Codespaces: https://github.com/codespaces
|
||||
|
||||
1
client/simple/.gitignore
vendored
1
client/simple/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
dist
|
||||
node_modules
|
||||
.stylelintcache
|
||||
@@ -1,19 +1,17 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/stylelintrc.json",
|
||||
"plugins": ["stylelint-prettier"],
|
||||
"extends": ["stylelint-config-standard-less"],
|
||||
"formatter": "unix",
|
||||
"plugins": [ "stylelint-prettier" ],
|
||||
"extends": [ "stylelint-config-standard-less" ],
|
||||
"rules": {
|
||||
"at-rule-no-vendor-prefix": null,
|
||||
"at-rule-prelude-no-invalid": null,
|
||||
"declaration-empty-line-before": null,
|
||||
"declaration-property-value-no-unknown": null,
|
||||
"no-invalid-position-at-import-rule": null,
|
||||
"prettier/prettier": true,
|
||||
"declaration-empty-line-before": null,
|
||||
"no-invalid-position-at-import-rule": null,
|
||||
"property-no-vendor-prefix": null,
|
||||
"selector-attribute-quotes": null,
|
||||
"selector-class-pattern": null,
|
||||
"selector-id-pattern": null,
|
||||
"selector-no-vendor-prefix": null,
|
||||
"shorthand-property-no-redundant-values": null
|
||||
"selector-attribute-quotes": null,
|
||||
"shorthand-property-no-redundant-values": null,
|
||||
"at-rule-no-vendor-prefix": null,
|
||||
"selector-id-pattern": null,
|
||||
"selector-class-pattern": null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": ["**", "!node_modules", "!src/brand", "!src/svg"]
|
||||
},
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": {
|
||||
"preset": "recommended",
|
||||
"source": {
|
||||
"useSortedAttributes": "on",
|
||||
"useSortedProperties": "on"
|
||||
}
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 120
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"preset": "recommended",
|
||||
"complexity": {
|
||||
"noForEach": "error",
|
||||
"noImplicitCoercions": "error",
|
||||
"noRedundantDefaultExport": "error",
|
||||
"noUselessCatchBinding": "error",
|
||||
"noUselessUndefined": "error",
|
||||
"useArrayFind": "error",
|
||||
"useSimplifiedLogicExpression": "error"
|
||||
},
|
||||
"correctness": {
|
||||
"noGlobalDirnameFilename": "error",
|
||||
"useImportExtensions": "error",
|
||||
"useJsonImportAttributes": "error",
|
||||
"useSingleJsDocAsterisk": "error"
|
||||
},
|
||||
"nursery": {
|
||||
"noFloatingPromises": "warn",
|
||||
"noMisusedPromises": "warn",
|
||||
"useAwaitThenable": "off",
|
||||
"useExhaustiveSwitchCases": "warn",
|
||||
"useExplicitType": "off",
|
||||
"useRegexpExec": "warn"
|
||||
},
|
||||
"performance": {
|
||||
"noAwaitInLoops": "error",
|
||||
"noBarrelFile": "error",
|
||||
"noDelete": "error",
|
||||
"noNamespaceImport": "error",
|
||||
"noReExportAll": "error",
|
||||
"useTopLevelRegex": "error"
|
||||
},
|
||||
"style": {
|
||||
"noCommonJs": "error",
|
||||
"noEnum": "error",
|
||||
"noImplicitBoolean": "error",
|
||||
"noIncrementDecrement": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noMultiAssign": "error",
|
||||
"noMultilineString": "error",
|
||||
"noNamespace": "error",
|
||||
"noNegationElse": "error",
|
||||
"noNestedTernary": "error",
|
||||
"noParameterAssign": "error",
|
||||
"noParameterProperties": "error",
|
||||
"noSubstr": "error",
|
||||
"noUnusedTemplateLiteral": "error",
|
||||
"noUselessElse": "error",
|
||||
"noYodaExpression": "error",
|
||||
"useAsConstAssertion": "error",
|
||||
"useAtIndex": "error",
|
||||
"useCollapsedElseIf": "error",
|
||||
"useCollapsedIf": "error",
|
||||
"useConsistentArrayType": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"syntax": "shorthand"
|
||||
}
|
||||
},
|
||||
"useConsistentBuiltinInstantiation": "error",
|
||||
"useConsistentEnumValueType": "error",
|
||||
"useConsistentMemberAccessibility": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"accessibility": "explicit"
|
||||
}
|
||||
},
|
||||
"useConsistentObjectDefinitions": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"syntax": "explicit"
|
||||
}
|
||||
},
|
||||
"useConsistentTypeDefinitions": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"style": "type"
|
||||
}
|
||||
},
|
||||
"useDefaultSwitchClause": "error",
|
||||
"useDestructuring": "error",
|
||||
"useExplicitLengthCheck": "error",
|
||||
"useForOf": "error",
|
||||
"useGroupedAccessorPairs": "error",
|
||||
"useNumberNamespace": "error",
|
||||
"useNumericSeparators": "error",
|
||||
"useObjectSpread": "error",
|
||||
"useReadonlyClassProperties": "error",
|
||||
"useSelfClosingElements": "error",
|
||||
"useShorthandAssign": "error",
|
||||
"useSingleVarDeclarator": "error",
|
||||
"useThrowNewError": "error",
|
||||
"useThrowOnlyError": "error",
|
||||
"useTrimStartEnd": "error",
|
||||
"useUnifiedTypeSignatures": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noBitwiseOperators": "error",
|
||||
"noConstantBinaryExpressions": "error",
|
||||
"noDeprecatedImports": "error",
|
||||
"noEmptyBlockStatements": "error",
|
||||
"noEqualsToNull": "error",
|
||||
"noEvolvingTypes": "error",
|
||||
"noForIn": "error",
|
||||
"noImportCycles": "error",
|
||||
"noNestedPromises": "error",
|
||||
"noParametersOnlyUsedInRecursion": "error",
|
||||
"noReturnAssign": "error",
|
||||
"noUnassignedVariables": "error",
|
||||
"noVar": "error",
|
||||
"useNumberToFixedDigitsArgument": "error",
|
||||
"useStaticResponseMethods": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"arrowParentheses": "always",
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"quoteStyle": "double",
|
||||
"semicolons": "always",
|
||||
"trailingCommas": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
34
client/simple/eslint.config.js
Normal file
34
client/simple/eslint.config.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
pluginJs.configs.recommended,
|
||||
|
||||
// global "ignores"
|
||||
// https://eslint.org/docs/latest/use/configure/configuration-files#globally-ignoring-files-with-ignores
|
||||
{
|
||||
ignores: ["node_modules/", "dist/"]
|
||||
},
|
||||
|
||||
{
|
||||
files: [
|
||||
"**/*.js",
|
||||
],
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: "error",
|
||||
// noInlineConfig: true
|
||||
},
|
||||
languageOptions: {
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
...globals.browser,
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
indent: ["error", 2],
|
||||
},
|
||||
},
|
||||
|
||||
];
|
||||
@@ -1,16 +1,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
this file is generated automatically by searxng_extra/update/update_pygments.py
|
||||
using pygments version 2.20.0:
|
||||
|
||||
./manage templates.simple.pygments
|
||||
using pygments version 2.19.1
|
||||
*/
|
||||
|
||||
|
||||
.code-highlight {
|
||||
|
||||
pre { line-height: 125%; }
|
||||
pre { line-height: 100%; }
|
||||
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
@@ -89,89 +85,89 @@
|
||||
.code-highlight-dark(){
|
||||
.code-highlight {
|
||||
|
||||
pre { line-height: 125%; }
|
||||
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
.hll { background-color: #49483e }
|
||||
.c { color: #959077 } /* Comment */
|
||||
.err { color: #ED007E; background-color: #1E0010 } /* Error */
|
||||
.esc { color: #F8F8F2 } /* Escape */
|
||||
.g { color: #F8F8F2 } /* Generic */
|
||||
.k { color: #66D9EF } /* Keyword */
|
||||
.l { color: #AE81FF } /* Literal */
|
||||
.n { color: #F8F8F2 } /* Name */
|
||||
.o { color: #FF4689 } /* Operator */
|
||||
.x { color: #F8F8F2 } /* Other */
|
||||
.p { color: #F8F8F2 } /* Punctuation */
|
||||
.ch { color: #959077 } /* Comment.Hashbang */
|
||||
.cm { color: #959077 } /* Comment.Multiline */
|
||||
.cp { color: #959077 } /* Comment.Preproc */
|
||||
.cpf { color: #959077 } /* Comment.PreprocFile */
|
||||
.c1 { color: #959077 } /* Comment.Single */
|
||||
.cs { color: #959077 } /* Comment.Special */
|
||||
.gd { color: #FF4689 } /* Generic.Deleted */
|
||||
.ge { color: #F8F8F2; font-style: italic } /* Generic.Emph */
|
||||
.ges { color: #F8F8F2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
.gr { color: #F8F8F2 } /* Generic.Error */
|
||||
.gh { color: #F8F8F2 } /* Generic.Heading */
|
||||
.gi { color: #A6E22E } /* Generic.Inserted */
|
||||
.go { color: #66D9EF } /* Generic.Output */
|
||||
.gp { color: #FF4689; font-weight: bold } /* Generic.Prompt */
|
||||
.gs { color: #F8F8F2; font-weight: bold } /* Generic.Strong */
|
||||
.gu { color: #959077 } /* Generic.Subheading */
|
||||
.gt { color: #F8F8F2 } /* Generic.Traceback */
|
||||
.kc { color: #66D9EF } /* Keyword.Constant */
|
||||
.kd { color: #66D9EF } /* Keyword.Declaration */
|
||||
.kn { color: #FF4689 } /* Keyword.Namespace */
|
||||
.kp { color: #66D9EF } /* Keyword.Pseudo */
|
||||
.kr { color: #66D9EF } /* Keyword.Reserved */
|
||||
.kt { color: #66D9EF } /* Keyword.Type */
|
||||
.ld { color: #E6DB74 } /* Literal.Date */
|
||||
.m { color: #AE81FF } /* Literal.Number */
|
||||
.s { color: #E6DB74 } /* Literal.String */
|
||||
.na { color: #A6E22E } /* Name.Attribute */
|
||||
.nb { color: #F8F8F2 } /* Name.Builtin */
|
||||
.nc { color: #A6E22E } /* Name.Class */
|
||||
.no { color: #66D9EF } /* Name.Constant */
|
||||
.nd { color: #A6E22E } /* Name.Decorator */
|
||||
.ni { color: #F8F8F2 } /* Name.Entity */
|
||||
.ne { color: #A6E22E } /* Name.Exception */
|
||||
.nf { color: #A6E22E } /* Name.Function */
|
||||
.nl { color: #F8F8F2 } /* Name.Label */
|
||||
.nn { color: #F8F8F2 } /* Name.Namespace */
|
||||
.nx { color: #A6E22E } /* Name.Other */
|
||||
.py { color: #F8F8F2 } /* Name.Property */
|
||||
.nt { color: #FF4689 } /* Name.Tag */
|
||||
.nv { color: #F8F8F2 } /* Name.Variable */
|
||||
.ow { color: #FF4689 } /* Operator.Word */
|
||||
.pm { color: #F8F8F2 } /* Punctuation.Marker */
|
||||
.w { color: #F8F8F2 } /* Text.Whitespace */
|
||||
.mb { color: #AE81FF } /* Literal.Number.Bin */
|
||||
.mf { color: #AE81FF } /* Literal.Number.Float */
|
||||
.mh { color: #AE81FF } /* Literal.Number.Hex */
|
||||
.mi { color: #AE81FF } /* Literal.Number.Integer */
|
||||
.mo { color: #AE81FF } /* Literal.Number.Oct */
|
||||
.sa { color: #E6DB74 } /* Literal.String.Affix */
|
||||
.sb { color: #E6DB74 } /* Literal.String.Backtick */
|
||||
.sc { color: #E6DB74 } /* Literal.String.Char */
|
||||
.dl { color: #E6DB74 } /* Literal.String.Delimiter */
|
||||
.sd { color: #E6DB74 } /* Literal.String.Doc */
|
||||
.s2 { color: #E6DB74 } /* Literal.String.Double */
|
||||
.se { color: #AE81FF } /* Literal.String.Escape */
|
||||
.sh { color: #E6DB74 } /* Literal.String.Heredoc */
|
||||
.si { color: #E6DB74 } /* Literal.String.Interpol */
|
||||
.sx { color: #E6DB74 } /* Literal.String.Other */
|
||||
.sr { color: #E6DB74 } /* Literal.String.Regex */
|
||||
.s1 { color: #E6DB74 } /* Literal.String.Single */
|
||||
.ss { color: #E6DB74 } /* Literal.String.Symbol */
|
||||
.bp { color: #F8F8F2 } /* Name.Builtin.Pseudo */
|
||||
.fm { color: #A6E22E } /* Name.Function.Magic */
|
||||
.vc { color: #F8F8F2 } /* Name.Variable.Class */
|
||||
.vg { color: #F8F8F2 } /* Name.Variable.Global */
|
||||
.vi { color: #F8F8F2 } /* Name.Variable.Instance */
|
||||
.vm { color: #F8F8F2 } /* Name.Variable.Magic */
|
||||
.il { color: #AE81FF } /* Literal.Number.Integer.Long */
|
||||
pre { line-height: 100%; }
|
||||
td.linenos .normal { color: #3c4354; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: #3c4354; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #3c4354; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #3c4354; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
.hll { background-color: #6e7681 }
|
||||
.c { color: #7E8AA1 } /* Comment */
|
||||
.err { color: #F88F7F } /* Error */
|
||||
.esc { color: #D4D2C8 } /* Escape */
|
||||
.g { color: #D4D2C8 } /* Generic */
|
||||
.k { color: #FFAD66 } /* Keyword */
|
||||
.l { color: #D5FF80 } /* Literal */
|
||||
.n { color: #D4D2C8 } /* Name */
|
||||
.o { color: #FFAD66 } /* Operator */
|
||||
.x { color: #D4D2C8 } /* Other */
|
||||
.p { color: #D4D2C8 } /* Punctuation */
|
||||
.ch { color: #F88F7F; font-style: italic } /* Comment.Hashbang */
|
||||
.cm { color: #7E8AA1 } /* Comment.Multiline */
|
||||
.cp { color: #FFAD66; font-weight: bold } /* Comment.Preproc */
|
||||
.cpf { color: #7E8AA1 } /* Comment.PreprocFile */
|
||||
.c1 { color: #7E8AA1 } /* Comment.Single */
|
||||
.cs { color: #7E8AA1; font-style: italic } /* Comment.Special */
|
||||
.gd { color: #F88F7F; background-color: #3D1E20 } /* Generic.Deleted */
|
||||
.ge { color: #D4D2C8; font-style: italic } /* Generic.Emph */
|
||||
.ges { color: #D4D2C8 } /* Generic.EmphStrong */
|
||||
.gr { color: #F88F7F } /* Generic.Error */
|
||||
.gh { color: #D4D2C8 } /* Generic.Heading */
|
||||
.gi { color: #6AD4AF; background-color: #19362C } /* Generic.Inserted */
|
||||
.go { color: #7E8AA1 } /* Generic.Output */
|
||||
.gp { color: #D4D2C8 } /* Generic.Prompt */
|
||||
.gs { color: #D4D2C8; font-weight: bold } /* Generic.Strong */
|
||||
.gu { color: #D4D2C8 } /* Generic.Subheading */
|
||||
.gt { color: #F88F7F } /* Generic.Traceback */
|
||||
.kc { color: #FFAD66 } /* Keyword.Constant */
|
||||
.kd { color: #FFAD66 } /* Keyword.Declaration */
|
||||
.kn { color: #FFAD66 } /* Keyword.Namespace */
|
||||
.kp { color: #FFAD66 } /* Keyword.Pseudo */
|
||||
.kr { color: #FFAD66 } /* Keyword.Reserved */
|
||||
.kt { color: #73D0FF } /* Keyword.Type */
|
||||
.ld { color: #D5FF80 } /* Literal.Date */
|
||||
.m { color: #DFBFFF } /* Literal.Number */
|
||||
.s { color: #D5FF80 } /* Literal.String */
|
||||
.na { color: #FFD173 } /* Name.Attribute */
|
||||
.nb { color: #FFD173 } /* Name.Builtin */
|
||||
.nc { color: #73D0FF } /* Name.Class */
|
||||
.no { color: #FFD173 } /* Name.Constant */
|
||||
.nd { color: #7E8AA1; font-weight: bold; font-style: italic } /* Name.Decorator */
|
||||
.ni { color: #95E6CB } /* Name.Entity */
|
||||
.ne { color: #73D0FF } /* Name.Exception */
|
||||
.nf { color: #FFD173 } /* Name.Function */
|
||||
.nl { color: #D4D2C8 } /* Name.Label */
|
||||
.nn { color: #D4D2C8 } /* Name.Namespace */
|
||||
.nx { color: #D4D2C8 } /* Name.Other */
|
||||
.py { color: #FFD173 } /* Name.Property */
|
||||
.nt { color: #5CCFE6 } /* Name.Tag */
|
||||
.nv { color: #D4D2C8 } /* Name.Variable */
|
||||
.ow { color: #FFAD66 } /* Operator.Word */
|
||||
.pm { color: #D4D2C8 } /* Punctuation.Marker */
|
||||
.w { color: #D4D2C8 } /* Text.Whitespace */
|
||||
.mb { color: #DFBFFF } /* Literal.Number.Bin */
|
||||
.mf { color: #DFBFFF } /* Literal.Number.Float */
|
||||
.mh { color: #DFBFFF } /* Literal.Number.Hex */
|
||||
.mi { color: #DFBFFF } /* Literal.Number.Integer */
|
||||
.mo { color: #DFBFFF } /* Literal.Number.Oct */
|
||||
.sa { color: #F29E74 } /* Literal.String.Affix */
|
||||
.sb { color: #D5FF80 } /* Literal.String.Backtick */
|
||||
.sc { color: #D5FF80 } /* Literal.String.Char */
|
||||
.dl { color: #D5FF80 } /* Literal.String.Delimiter */
|
||||
.sd { color: #7E8AA1 } /* Literal.String.Doc */
|
||||
.s2 { color: #D5FF80 } /* Literal.String.Double */
|
||||
.se { color: #95E6CB } /* Literal.String.Escape */
|
||||
.sh { color: #D5FF80 } /* Literal.String.Heredoc */
|
||||
.si { color: #95E6CB } /* Literal.String.Interpol */
|
||||
.sx { color: #95E6CB } /* Literal.String.Other */
|
||||
.sr { color: #95E6CB } /* Literal.String.Regex */
|
||||
.s1 { color: #D5FF80 } /* Literal.String.Single */
|
||||
.ss { color: #DFBFFF } /* Literal.String.Symbol */
|
||||
.bp { color: #5CCFE6 } /* Name.Builtin.Pseudo */
|
||||
.fm { color: #FFD173 } /* Name.Function.Magic */
|
||||
.vc { color: #D4D2C8 } /* Name.Variable.Class */
|
||||
.vg { color: #D4D2C8 } /* Name.Variable.Global */
|
||||
.vi { color: #D4D2C8 } /* Name.Variable.Instance */
|
||||
.vm { color: #D4D2C8 } /* Name.Variable.Magic */
|
||||
.il { color: #DFBFFF } /* Literal.Number.Integer.Long */
|
||||
}
|
||||
}
|
||||
|
||||
7171
client/simple/package-lock.json
generated
7171
client/simple/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,49 +1,41 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@searxng/theme-simple",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"name": "simple",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "npm run build:icons && npm run build:vite",
|
||||
"build:icons": "node theme_icons.ts",
|
||||
"build:vite": "vite build",
|
||||
"clean": "rm -Rf node_modules",
|
||||
"fix": "npm run fix:stylelint && npm run fix:biome && npm run fix:package",
|
||||
"fix:biome": "biome check --write",
|
||||
"fix:package": "sort-package-json --quiet",
|
||||
"fix:stylelint": "stylelint --fix strict 'src/**/*.{scss,sass,less,styl}'",
|
||||
"lint": "npm run lint:biome && npm run lint:tsc",
|
||||
"lint:biome": "biome lint",
|
||||
"lint:tsc": "tsc --noEmit"
|
||||
},
|
||||
"browserslist": [
|
||||
"baseline 2022",
|
||||
"not dead"
|
||||
],
|
||||
"dependencies": {
|
||||
"ionicons": "^8.0.13",
|
||||
"normalize.css": "8.0.1",
|
||||
"ol": "^10.9.0",
|
||||
"swiped-events": "1.2.0"
|
||||
"build": "node theme_icons.js && vite build",
|
||||
"fix": "eslint --fix && stylelint --fix strict 'src/**/*.{css,scss,sass,less,styl,vue,svelte}'",
|
||||
"icons.html": "node theme_icons.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.3",
|
||||
"@types/node": "^26.1.1",
|
||||
"browserslist": "^4.28.6",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"edge.js": "^6.5.1",
|
||||
"less": "^4.6.7",
|
||||
"mathjs": "^15.2.0",
|
||||
"sharp": "~0.35.3",
|
||||
"sort-package-json": "^4.0.0",
|
||||
"stylelint": "^17.14.0",
|
||||
"stylelint-config-standard-less": "^4.1.0",
|
||||
"@eslint/js": "^9.22.0",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"edge.js": "^6.2.1",
|
||||
"eslint": "^9.24.0",
|
||||
"filemanager-webpack-plugin": "^8.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"ionicons": "^7.4.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"less": "^4.3.0",
|
||||
"less-loader": "^12.2.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"sharp": "^0.34.1",
|
||||
"style-loader": "^4.0.0",
|
||||
"stylelint": "^16.17.0",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"stylelint-config-standard-less": "^3.0.1",
|
||||
"stylelint-prettier": "^5.0.3",
|
||||
"svgo": "^4.0.2",
|
||||
"typescript": "~7.0.2",
|
||||
"vite": "^8.1.4",
|
||||
"vite-bundle-analyzer": "^1.3.8"
|
||||
"svgo": "^3.3.2",
|
||||
"swiped-events": "^1.2.0",
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-static-copy": "^2.3.0",
|
||||
"vite-plugin-stylelint": "^6.0.0",
|
||||
"webpack": "^5.99.5",
|
||||
"webpack-cli": "^6.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"autocomplete-js": "^2.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* Base class for client-side plugins.
|
||||
*
|
||||
* @remarks
|
||||
* Handle conditional loading of the plugin in:
|
||||
*
|
||||
* - client/simple/src/js/router.ts
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export abstract class Plugin {
|
||||
/**
|
||||
* Plugin name.
|
||||
*/
|
||||
protected readonly id: string;
|
||||
|
||||
/**
|
||||
* @remarks
|
||||
* Don't hold references of this instance outside the class.
|
||||
*/
|
||||
protected constructor(id: string) {
|
||||
this.id = id;
|
||||
|
||||
queueMicrotask(() => this.invoke());
|
||||
}
|
||||
|
||||
private async invoke(): Promise<void> {
|
||||
try {
|
||||
console.debug(`[PLUGIN] ${this.id}: Running...`);
|
||||
const result = await this.run();
|
||||
if (!result) return;
|
||||
|
||||
console.debug(`[PLUGIN] ${this.id}: Running post-exec...`);
|
||||
// @ts-expect-error
|
||||
void (await this.post(result as NonNullable<Awaited<ReturnType<this["run"]>>>));
|
||||
} catch (error) {
|
||||
console.error(`[PLUGIN] ${this.id}:`, error);
|
||||
} finally {
|
||||
console.debug(`[PLUGIN] ${this.id}: Done.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin goes here.
|
||||
*
|
||||
* @remarks
|
||||
* The plugin is already loaded at this point. If you wish to execute
|
||||
* conditions to exit early, consider moving the logic to:
|
||||
*
|
||||
* - client/simple/src/js/router.ts
|
||||
*
|
||||
* ...to avoid unnecessarily loading this plugin on the client.
|
||||
*/
|
||||
protected abstract run(): Promise<unknown>;
|
||||
|
||||
/**
|
||||
* Post-execution hook.
|
||||
*
|
||||
* @remarks
|
||||
* The hook is only executed if `#run()` returns a truthy value.
|
||||
*/
|
||||
// @ts-expect-error
|
||||
protected abstract post(result: NonNullable<Awaited<ReturnType<this["run"]>>>): Promise<void>;
|
||||
}
|
||||
20
client/simple/src/js/head/00_init.js
Normal file
20
client/simple/src/js/head/00_init.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
(function (w, d) {
|
||||
'use strict';
|
||||
|
||||
// add data- properties
|
||||
var script = d.currentScript || (function () {
|
||||
var scripts = d.getElementsByTagName('script');
|
||||
return scripts[scripts.length - 1];
|
||||
})();
|
||||
|
||||
w.searxng = {
|
||||
settings: JSON.parse(atob(script.getAttribute('client_settings')))
|
||||
};
|
||||
|
||||
// update the css
|
||||
var htmlElement = d.getElementsByTagName("html")[0];
|
||||
htmlElement.classList.remove('no-js');
|
||||
htmlElement.classList.add('js');
|
||||
|
||||
})(window, document);
|
||||
@@ -1,4 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
// core
|
||||
void import.meta.glob(["./*.ts", "./util/**/.ts"], { eager: true });
|
||||
@@ -1,36 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type { Plugin } from "./Plugin.ts";
|
||||
import { type EndpointsKeys, endpoint } from "./toolkit.ts";
|
||||
|
||||
type Options =
|
||||
| {
|
||||
on: "global";
|
||||
}
|
||||
| {
|
||||
on: "endpoint";
|
||||
where: EndpointsKeys[];
|
||||
};
|
||||
|
||||
export const load = <T extends Plugin>(instance: () => Promise<T>, options: Options): void => {
|
||||
if (!check(options)) return;
|
||||
|
||||
void instance();
|
||||
};
|
||||
|
||||
const check = (options: Options): boolean => {
|
||||
// biome-ignore lint/style/useDefaultSwitchClause: options is typed
|
||||
switch (options.on) {
|
||||
case "global": {
|
||||
return true;
|
||||
}
|
||||
case "endpoint": {
|
||||
if (!options.where.includes(endpoint)) {
|
||||
// not on the expected endpoint
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
165
client/simple/src/js/main/00_toolkit.js
Normal file
165
client/simple/src/js/main/00_toolkit.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* @license
|
||||
* (C) Copyright Contributors to the SearXNG project.
|
||||
* (C) Copyright Contributors to the searx project (2014 - 2021).
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
window.searxng = (function (w, d) {
|
||||
|
||||
'use strict';
|
||||
|
||||
// not invented here toolkit with bugs fixed elsewhere
|
||||
// purposes : be just good enough and as small as possible
|
||||
|
||||
// from https://plainjs.com/javascript/events/live-binding-event-handlers-14/
|
||||
if (w.Element) {
|
||||
(function (ElementPrototype) {
|
||||
ElementPrototype.matches = ElementPrototype.matches ||
|
||||
ElementPrototype.matchesSelector ||
|
||||
ElementPrototype.webkitMatchesSelector ||
|
||||
ElementPrototype.msMatchesSelector ||
|
||||
function (selector) {
|
||||
var node = this, nodes = (node.parentNode || node.document).querySelectorAll(selector), i = -1;
|
||||
while (nodes[++i] && nodes[i] != node);
|
||||
return !!nodes[i];
|
||||
};
|
||||
})(Element.prototype);
|
||||
}
|
||||
|
||||
function callbackSafe (callback, el, e) {
|
||||
try {
|
||||
callback.call(el, e);
|
||||
} catch (exception) {
|
||||
console.log(exception);
|
||||
}
|
||||
}
|
||||
|
||||
var searxng = window.searxng || {};
|
||||
|
||||
searxng.on = function (obj, eventType, callback, useCapture) {
|
||||
useCapture = useCapture || false;
|
||||
if (typeof obj !== 'string') {
|
||||
// obj HTMLElement, HTMLDocument
|
||||
obj.addEventListener(eventType, callback, useCapture);
|
||||
} else {
|
||||
// obj is a selector
|
||||
d.addEventListener(eventType, function (e) {
|
||||
var el = e.target || e.srcElement, found = false;
|
||||
while (el && el.matches && el !== d && !(found = el.matches(obj))) el = el.parentElement;
|
||||
if (found) callbackSafe(callback, el, e);
|
||||
}, useCapture);
|
||||
}
|
||||
};
|
||||
|
||||
searxng.ready = function (callback) {
|
||||
if (document.readyState != 'loading') {
|
||||
callback.call(w);
|
||||
} else {
|
||||
w.addEventListener('DOMContentLoaded', callback.bind(w));
|
||||
}
|
||||
};
|
||||
|
||||
searxng.http = function (method, url, data = null) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
try {
|
||||
var req = new XMLHttpRequest();
|
||||
req.open(method, url, true);
|
||||
req.timeout = 20000;
|
||||
|
||||
// On load
|
||||
req.onload = function () {
|
||||
if (req.status == 200) {
|
||||
resolve(req.response, req.responseType);
|
||||
} else {
|
||||
reject(Error(req.statusText));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle network errors
|
||||
req.onerror = function () {
|
||||
reject(Error("Network Error"));
|
||||
};
|
||||
|
||||
req.onabort = function () {
|
||||
reject(Error("Transaction is aborted"));
|
||||
};
|
||||
|
||||
req.ontimeout = function () {
|
||||
reject(Error("Timeout"));
|
||||
}
|
||||
|
||||
// Make the request
|
||||
if (data) {
|
||||
req.send(data)
|
||||
} else {
|
||||
req.send();
|
||||
}
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
searxng.loadStyle = function (src) {
|
||||
var path = searxng.settings.theme_static_path + "/" + src,
|
||||
id = "style_" + src.replace('.', '_'),
|
||||
s = d.getElementById(id);
|
||||
if (s === null) {
|
||||
s = d.createElement('link');
|
||||
s.setAttribute('id', id);
|
||||
s.setAttribute('rel', 'stylesheet');
|
||||
s.setAttribute('type', 'text/css');
|
||||
s.setAttribute('href', path);
|
||||
d.body.appendChild(s);
|
||||
}
|
||||
};
|
||||
|
||||
searxng.loadScript = function (src, callback) {
|
||||
var path = searxng.settings.theme_static_path + "/" + src,
|
||||
id = "script_" + src.replace('.', '_'),
|
||||
s = d.getElementById(id);
|
||||
if (s === null) {
|
||||
s = d.createElement('script');
|
||||
s.setAttribute('id', id);
|
||||
s.setAttribute('src', path);
|
||||
s.onload = callback;
|
||||
s.onerror = function () {
|
||||
s.setAttribute('error', '1');
|
||||
};
|
||||
d.body.appendChild(s);
|
||||
} else if (!s.hasAttribute('error')) {
|
||||
try {
|
||||
callback.apply(s, []);
|
||||
} catch (exception) {
|
||||
console.log(exception);
|
||||
}
|
||||
} else {
|
||||
console.log("callback not executed : script '" + path + "' not loaded.");
|
||||
}
|
||||
};
|
||||
|
||||
searxng.insertBefore = function (newNode, referenceNode) {
|
||||
referenceNode.parentNode.insertBefore(newNode, referenceNode);
|
||||
};
|
||||
|
||||
searxng.insertAfter = function (newNode, referenceNode) {
|
||||
referenceNode.parentNode.insertAfter(newNode, referenceNode.nextSibling);
|
||||
};
|
||||
|
||||
searxng.on('.close', 'click', function () {
|
||||
this.parentNode.classList.add('invisible');
|
||||
});
|
||||
|
||||
function getEndpoint () {
|
||||
for (var className of d.getElementsByTagName('body')[0].classList.values()) {
|
||||
if (className.endsWith('_endpoint')) {
|
||||
return className.split('_')[0];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
searxng.endpoint = getEndpoint();
|
||||
|
||||
return searxng;
|
||||
})(window, document);
|
||||
@@ -1,142 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { http, listen, settings } from "../toolkit.ts";
|
||||
import { assertElement } from "../util/assertElement.ts";
|
||||
|
||||
const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
|
||||
try {
|
||||
let res: Response;
|
||||
|
||||
if (settings.method === "GET") {
|
||||
res = await http("GET", `./autocompleter?q=${query}`);
|
||||
} else {
|
||||
res = await http("POST", "./autocompleter", { body: new URLSearchParams({ q: query }) });
|
||||
}
|
||||
|
||||
const results = await res.json();
|
||||
|
||||
const autocomplete = document.querySelector<HTMLElement>(".autocomplete");
|
||||
assertElement(autocomplete);
|
||||
|
||||
const autocompleteList = document.querySelector<HTMLUListElement>(".autocomplete ul");
|
||||
assertElement(autocompleteList);
|
||||
|
||||
autocomplete.classList.add("open");
|
||||
autocompleteList.replaceChildren();
|
||||
|
||||
// show an error message that no result was found
|
||||
if (results?.[1]?.length === 0) {
|
||||
const noItemFoundMessage = Object.assign(document.createElement("li"), {
|
||||
className: "no-item-found",
|
||||
textContent: settings.translations?.no_item_found ?? "No results found"
|
||||
});
|
||||
autocompleteList.append(noItemFoundMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const fragment = new DocumentFragment();
|
||||
|
||||
for (const result of results[1]) {
|
||||
const li = Object.assign(document.createElement("li"), { textContent: result });
|
||||
|
||||
listen("mousedown", li, () => {
|
||||
qInput.value = result;
|
||||
|
||||
const form = document.querySelector<HTMLFormElement>("#search");
|
||||
form?.submit();
|
||||
});
|
||||
|
||||
fragment.append(li);
|
||||
}
|
||||
|
||||
autocompleteList.append(fragment);
|
||||
} catch (error) {
|
||||
console.error("Error fetching autocomplete results:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const qInput = document.getElementById("q") as HTMLInputElement | null;
|
||||
assertElement(qInput);
|
||||
|
||||
let timeoutId: number;
|
||||
|
||||
listen("input", qInput, () => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const query = qInput.value;
|
||||
const minLength = settings.autocomplete_min ?? 2;
|
||||
|
||||
if (query.length < minLength) return;
|
||||
|
||||
timeoutId = window.setTimeout(async () => {
|
||||
if (query === qInput.value) {
|
||||
await fetchResults(qInput, query);
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
const autocomplete: HTMLElement | null = document.querySelector<HTMLElement>(".autocomplete");
|
||||
const autocompleteList: HTMLUListElement | null = document.querySelector<HTMLUListElement>(".autocomplete ul");
|
||||
if (autocompleteList) {
|
||||
listen("keydown", qInput, (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
autocomplete?.classList.remove("open");
|
||||
}
|
||||
});
|
||||
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 "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 ?? "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
listen("blur", qInput, () => {
|
||||
autocomplete?.classList.remove("open");
|
||||
});
|
||||
|
||||
listen("focus", qInput, () => {
|
||||
autocomplete?.classList.add("open");
|
||||
});
|
||||
}
|
||||
88
client/simple/src/js/main/infinite_scroll.js
Normal file
88
client/simple/src/js/main/infinite_scroll.js
Normal file
@@ -0,0 +1,88 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/* global searxng */
|
||||
|
||||
searxng.ready(function () {
|
||||
'use strict';
|
||||
|
||||
searxng.infinite_scroll_supported = (
|
||||
'IntersectionObserver' in window &&
|
||||
'IntersectionObserverEntry' in window &&
|
||||
'intersectionRatio' in window.IntersectionObserverEntry.prototype);
|
||||
|
||||
if (searxng.endpoint !== 'results') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!searxng.infinite_scroll_supported) {
|
||||
console.log('IntersectionObserver not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
let d = document;
|
||||
var onlyImages = d.getElementById('results').classList.contains('only_template_images');
|
||||
|
||||
function newLoadSpinner () {
|
||||
var loader = d.createElement('div');
|
||||
loader.classList.add('loader');
|
||||
return loader;
|
||||
}
|
||||
|
||||
function replaceChildrenWith (element, children) {
|
||||
element.textContent = '';
|
||||
children.forEach(child => element.appendChild(child));
|
||||
}
|
||||
|
||||
function loadNextPage (callback) {
|
||||
var form = d.querySelector('#pagination form.next_page');
|
||||
if (!form) {
|
||||
return
|
||||
}
|
||||
replaceChildrenWith(d.querySelector('#pagination'), [ newLoadSpinner() ]);
|
||||
var formData = new FormData(form);
|
||||
searxng.http('POST', d.querySelector('#search').getAttribute('action'), formData).then(
|
||||
function (response) {
|
||||
var nextPageDoc = new DOMParser().parseFromString(response, 'text/html');
|
||||
var articleList = nextPageDoc.querySelectorAll('#urls article');
|
||||
var paginationElement = nextPageDoc.querySelector('#pagination');
|
||||
d.querySelector('#pagination').remove();
|
||||
if (articleList.length > 0 && !onlyImages) {
|
||||
// do not add <hr> element when there are only images
|
||||
d.querySelector('#urls').appendChild(d.createElement('hr'));
|
||||
}
|
||||
articleList.forEach(articleElement => {
|
||||
d.querySelector('#urls').appendChild(articleElement);
|
||||
});
|
||||
if (paginationElement) {
|
||||
d.querySelector('#results').appendChild(paginationElement);
|
||||
callback();
|
||||
}
|
||||
}
|
||||
).catch(
|
||||
function (err) {
|
||||
console.log(err);
|
||||
var e = d.createElement('div');
|
||||
e.textContent = searxng.settings.translations.error_loading_next_page;
|
||||
e.classList.add('dialog-error');
|
||||
e.setAttribute('role', 'alert');
|
||||
replaceChildrenWith(d.querySelector('#pagination'), [ e ]);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (searxng.settings.infinite_scroll && searxng.infinite_scroll_supported) {
|
||||
const intersectionObserveOptions = {
|
||||
rootMargin: "20rem",
|
||||
};
|
||||
const observedSelector = 'article.result:last-child';
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
const paginationEntry = entries[0];
|
||||
if (paginationEntry.isIntersecting) {
|
||||
observer.unobserve(paginationEntry.target);
|
||||
loadNextPage(() => observer.observe(d.querySelector(observedSelector), intersectionObserveOptions));
|
||||
}
|
||||
});
|
||||
observer.observe(d.querySelector(observedSelector), intersectionObserveOptions);
|
||||
}
|
||||
|
||||
});
|
||||
461
client/simple/src/js/main/keyboard.js
Normal file
461
client/simple/src/js/main/keyboard.js
Normal file
@@ -0,0 +1,461 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
/* global searxng */
|
||||
|
||||
searxng.ready(function () {
|
||||
|
||||
function isElementInDetail (el) {
|
||||
while (el !== undefined) {
|
||||
if (el.classList.contains('detail')) {
|
||||
return true;
|
||||
}
|
||||
if (el.classList.contains('result')) {
|
||||
// we found a result, no need to go to the root of the document:
|
||||
// el is not inside a <div class="detail"> element
|
||||
return false;
|
||||
}
|
||||
el = el.parentNode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getResultElement (el) {
|
||||
while (el !== undefined) {
|
||||
if (el.classList.contains('result')) {
|
||||
return el;
|
||||
}
|
||||
el = el.parentNode;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isImageResult (resultElement) {
|
||||
return resultElement && resultElement.classList.contains('result-images');
|
||||
}
|
||||
|
||||
searxng.on('.result', 'click', function (e) {
|
||||
if (!isElementInDetail(e.target)) {
|
||||
highlightResult(this)(true, true);
|
||||
let resultElement = getResultElement(e.target);
|
||||
if (isImageResult(resultElement)) {
|
||||
e.preventDefault();
|
||||
searxng.selectImage(resultElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
searxng.on('.result a', 'focus', function (e) {
|
||||
if (!isElementInDetail(e.target)) {
|
||||
let resultElement = getResultElement(e.target);
|
||||
if (resultElement && resultElement.getAttribute("data-vim-selected") === null) {
|
||||
highlightResult(resultElement)(true);
|
||||
}
|
||||
if (isImageResult(resultElement)) {
|
||||
searxng.selectImage(resultElement);
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
/* common base for layouts */
|
||||
var baseKeyBinding = {
|
||||
'Escape': {
|
||||
key: 'ESC',
|
||||
fun: removeFocus,
|
||||
des: 'remove focus from the focused input',
|
||||
cat: 'Control'
|
||||
},
|
||||
'c': {
|
||||
key: 'c',
|
||||
fun: copyURLToClipboard,
|
||||
des: 'copy url of the selected result to the clipboard',
|
||||
cat: 'Results'
|
||||
},
|
||||
'h': {
|
||||
key: 'h',
|
||||
fun: toggleHelp,
|
||||
des: 'toggle help window',
|
||||
cat: 'Other'
|
||||
},
|
||||
'i': {
|
||||
key: 'i',
|
||||
fun: searchInputFocus,
|
||||
des: 'focus on the search input',
|
||||
cat: 'Control'
|
||||
},
|
||||
'n': {
|
||||
key: 'n',
|
||||
fun: GoToNextPage(),
|
||||
des: 'go to next page',
|
||||
cat: 'Results'
|
||||
},
|
||||
'o': {
|
||||
key: 'o',
|
||||
fun: openResult(false),
|
||||
des: 'open search result',
|
||||
cat: 'Results'
|
||||
},
|
||||
'p': {
|
||||
key: 'p',
|
||||
fun: GoToPreviousPage(),
|
||||
des: 'go to previous page',
|
||||
cat: 'Results'
|
||||
},
|
||||
'r': {
|
||||
key: 'r',
|
||||
fun: reloadPage,
|
||||
des: 'reload page from the server',
|
||||
cat: 'Control'
|
||||
},
|
||||
't': {
|
||||
key: 't',
|
||||
fun: openResult(true),
|
||||
des: 'open the result in a new tab',
|
||||
cat: 'Results'
|
||||
},
|
||||
};
|
||||
var keyBindingLayouts = {
|
||||
|
||||
"default": Object.assign(
|
||||
{ /* SearXNG layout */
|
||||
'ArrowLeft': {
|
||||
key: '←',
|
||||
fun: highlightResult('up'),
|
||||
des: 'select previous search result',
|
||||
cat: 'Results'
|
||||
},
|
||||
'ArrowRight': {
|
||||
key: '→',
|
||||
fun: highlightResult('down'),
|
||||
des: 'select next search result',
|
||||
cat: 'Results'
|
||||
},
|
||||
}, baseKeyBinding),
|
||||
|
||||
'vim': Object.assign(
|
||||
{ /* Vim-like Key Layout. */
|
||||
'b': {
|
||||
key: 'b',
|
||||
fun: scrollPage(-window.innerHeight),
|
||||
des: 'scroll one page up',
|
||||
cat: 'Navigation'
|
||||
},
|
||||
'f': {
|
||||
key: 'f',
|
||||
fun: scrollPage(window.innerHeight),
|
||||
des: 'scroll one page down',
|
||||
cat: 'Navigation'
|
||||
},
|
||||
'u': {
|
||||
key: 'u',
|
||||
fun: scrollPage(-window.innerHeight / 2),
|
||||
des: 'scroll half a page up',
|
||||
cat: 'Navigation'
|
||||
},
|
||||
'd': {
|
||||
key: 'd',
|
||||
fun: scrollPage(window.innerHeight / 2),
|
||||
des: 'scroll half a page down',
|
||||
cat: 'Navigation'
|
||||
},
|
||||
'g': {
|
||||
key: 'g',
|
||||
fun: scrollPageTo(-document.body.scrollHeight, 'top'),
|
||||
des: 'scroll to the top of the page',
|
||||
cat: 'Navigation'
|
||||
},
|
||||
'v': {
|
||||
key: 'v',
|
||||
fun: scrollPageTo(document.body.scrollHeight, 'bottom'),
|
||||
des: 'scroll to the bottom of the page',
|
||||
cat: 'Navigation'
|
||||
},
|
||||
'k': {
|
||||
key: 'k',
|
||||
fun: highlightResult('up'),
|
||||
des: 'select previous search result',
|
||||
cat: 'Results'
|
||||
},
|
||||
'j': {
|
||||
key: 'j',
|
||||
fun: highlightResult('down'),
|
||||
des: 'select next search result',
|
||||
cat: 'Results'
|
||||
},
|
||||
'y': {
|
||||
key: 'y',
|
||||
fun: copyURLToClipboard,
|
||||
des: 'copy url of the selected result to the clipboard',
|
||||
cat: 'Results'
|
||||
},
|
||||
}, baseKeyBinding)
|
||||
}
|
||||
|
||||
var keyBindings = keyBindingLayouts[searxng.settings.hotkeys] || keyBindingLayouts.default;
|
||||
|
||||
searxng.on(document, "keydown", function (e) {
|
||||
// check for modifiers so we don't break browser's hotkeys
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(keyBindings, e.key)
|
||||
&& !e.ctrlKey && !e.altKey
|
||||
&& !e.shiftKey && !e.metaKey
|
||||
) {
|
||||
var tagName = e.target.tagName.toLowerCase();
|
||||
if (e.key === 'Escape') {
|
||||
keyBindings[e.key].fun(e);
|
||||
} else {
|
||||
if (e.target === document.body || tagName === 'a' || tagName === 'button') {
|
||||
e.preventDefault();
|
||||
keyBindings[e.key].fun();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function highlightResult (which) {
|
||||
return function (noScroll, keepFocus) {
|
||||
var current = document.querySelector('.result[data-vim-selected]'),
|
||||
effectiveWhich = which;
|
||||
if (current === null) {
|
||||
// no selection : choose the first one
|
||||
current = document.querySelector('.result');
|
||||
if (current === null) {
|
||||
// no first one : there are no results
|
||||
return;
|
||||
}
|
||||
// replace up/down actions by selecting first one
|
||||
if (which === "down" || which === "up") {
|
||||
effectiveWhich = current;
|
||||
}
|
||||
}
|
||||
|
||||
var next, results = document.querySelectorAll('.result');
|
||||
results = Array.from(results); // convert NodeList to Array for further use
|
||||
|
||||
if (typeof effectiveWhich !== 'string') {
|
||||
next = effectiveWhich;
|
||||
} else {
|
||||
switch (effectiveWhich) {
|
||||
case 'visible':
|
||||
var top = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
var bot = top + document.documentElement.clientHeight;
|
||||
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
next = results[i];
|
||||
var etop = next.offsetTop;
|
||||
var ebot = etop + next.clientHeight;
|
||||
|
||||
if ((ebot <= bot) && (etop > top)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'down':
|
||||
next = results[results.indexOf(current) + 1] || current;
|
||||
break;
|
||||
case 'up':
|
||||
next = results[results.indexOf(current) - 1] || current;
|
||||
break;
|
||||
case 'bottom':
|
||||
next = results[results.length - 1];
|
||||
break;
|
||||
case 'top':
|
||||
/* falls through */
|
||||
default:
|
||||
next = results[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (next) {
|
||||
current.removeAttribute('data-vim-selected');
|
||||
next.setAttribute('data-vim-selected', 'true');
|
||||
if (!keepFocus) {
|
||||
var link = next.querySelector('h3 a') || next.querySelector('a');
|
||||
if (link !== null) {
|
||||
link.focus();
|
||||
}
|
||||
}
|
||||
if (!noScroll) {
|
||||
scrollPageToSelected();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function reloadPage () {
|
||||
document.location.reload(true);
|
||||
}
|
||||
|
||||
function removeFocus (e) {
|
||||
const tagName = e.target.tagName.toLowerCase();
|
||||
if (document.activeElement && (tagName === 'input' || tagName === 'select' || tagName === 'textarea')) {
|
||||
document.activeElement.blur();
|
||||
} else {
|
||||
searxng.closeDetail();
|
||||
}
|
||||
}
|
||||
|
||||
function pageButtonClick (css_selector) {
|
||||
return function () {
|
||||
var button = document.querySelector(css_selector);
|
||||
if (button) {
|
||||
button.click();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function GoToNextPage () {
|
||||
return pageButtonClick('nav#pagination .next_page button[type="submit"]');
|
||||
}
|
||||
|
||||
function GoToPreviousPage () {
|
||||
return pageButtonClick('nav#pagination .previous_page button[type="submit"]');
|
||||
}
|
||||
|
||||
function scrollPageToSelected () {
|
||||
var sel = document.querySelector('.result[data-vim-selected]');
|
||||
if (sel === null) {
|
||||
return;
|
||||
}
|
||||
var wtop = document.documentElement.scrollTop || document.body.scrollTop,
|
||||
wheight = document.documentElement.clientHeight,
|
||||
etop = sel.offsetTop,
|
||||
ebot = etop + sel.clientHeight,
|
||||
offset = 120;
|
||||
// first element ?
|
||||
if ((sel.previousElementSibling === null) && (ebot < wheight)) {
|
||||
// set to the top of page if the first element
|
||||
// is fully included in the viewport
|
||||
window.scroll(window.scrollX, 0);
|
||||
return;
|
||||
}
|
||||
if (wtop > (etop - offset)) {
|
||||
window.scroll(window.scrollX, etop - offset);
|
||||
} else {
|
||||
var wbot = wtop + wheight;
|
||||
if (wbot < (ebot + offset)) {
|
||||
window.scroll(window.scrollX, ebot - wheight + offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scrollPage (amount) {
|
||||
return function () {
|
||||
window.scrollBy(0, amount);
|
||||
highlightResult('visible')();
|
||||
};
|
||||
}
|
||||
|
||||
function scrollPageTo (position, nav) {
|
||||
return function () {
|
||||
window.scrollTo(0, position);
|
||||
highlightResult(nav)();
|
||||
};
|
||||
}
|
||||
|
||||
function searchInputFocus () {
|
||||
window.scrollTo(0, 0);
|
||||
var q = document.querySelector('#q');
|
||||
q.focus();
|
||||
if (q.setSelectionRange) {
|
||||
var len = q.value.length;
|
||||
q.setSelectionRange(len, len);
|
||||
}
|
||||
}
|
||||
|
||||
function openResult (newTab) {
|
||||
return function () {
|
||||
var link = document.querySelector('.result[data-vim-selected] h3 a');
|
||||
if (link === null) {
|
||||
link = document.querySelector('.result[data-vim-selected] > a');
|
||||
}
|
||||
if (link !== null) {
|
||||
var url = link.getAttribute('href');
|
||||
if (newTab) {
|
||||
window.open(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function initHelpContent (divElement) {
|
||||
var categories = {};
|
||||
|
||||
for (var k in keyBindings) {
|
||||
var key = keyBindings[k];
|
||||
categories[key.cat] = categories[key.cat] || [];
|
||||
categories[key.cat].push(key);
|
||||
}
|
||||
|
||||
var sorted = Object.keys(categories).sort(function (a, b) {
|
||||
return categories[b].length - categories[a].length;
|
||||
});
|
||||
|
||||
if (sorted.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<a href="#" class="close" aria-label="close" title="close">×</a>';
|
||||
html += '<h3>How to navigate SearXNG with hotkeys</h3>';
|
||||
html += '<table>';
|
||||
|
||||
for (var i = 0; i < sorted.length; i++) {
|
||||
var cat = categories[sorted[i]];
|
||||
|
||||
var lastCategory = i === (sorted.length - 1);
|
||||
var first = i % 2 === 0;
|
||||
|
||||
if (first) {
|
||||
html += '<tr>';
|
||||
}
|
||||
html += '<td>';
|
||||
|
||||
html += '<h4>' + cat[0].cat + '</h4>';
|
||||
html += '<ul class="list-unstyled">';
|
||||
|
||||
for (var cj in cat) {
|
||||
html += '<li><kbd>' + cat[cj].key + '</kbd> ' + cat[cj].des + '</li>';
|
||||
}
|
||||
|
||||
html += '</ul>';
|
||||
html += '</td>'; // col-sm-*
|
||||
|
||||
if (!first || lastCategory) {
|
||||
html += '</tr>'; // row
|
||||
}
|
||||
}
|
||||
|
||||
html += '</table>';
|
||||
|
||||
divElement.innerHTML = html;
|
||||
}
|
||||
|
||||
function toggleHelp () {
|
||||
var helpPanel = document.querySelector('#vim-hotkeys-help');
|
||||
if (helpPanel === undefined || helpPanel === null) {
|
||||
// first call
|
||||
helpPanel = document.createElement('div');
|
||||
helpPanel.id = 'vim-hotkeys-help';
|
||||
helpPanel.className = 'dialog-modal';
|
||||
initHelpContent(helpPanel);
|
||||
var body = document.getElementsByTagName('body')[0];
|
||||
body.appendChild(helpPanel);
|
||||
} else {
|
||||
// toggle hidden
|
||||
helpPanel.classList.toggle('invisible');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function copyURLToClipboard () {
|
||||
var currentUrlElement = document.querySelector('.result[data-vim-selected] h3 a');
|
||||
if (currentUrlElement === null) return;
|
||||
|
||||
const url = currentUrlElement.getAttribute('href');
|
||||
navigator.clipboard.writeText(url);
|
||||
}
|
||||
|
||||
searxng.scrollPageToSelected = scrollPageToSelected;
|
||||
searxng.selectNext = highlightResult('down');
|
||||
searxng.selectPrevious = highlightResult('up');
|
||||
});
|
||||
@@ -1,488 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { listen, mutable, settings } from "../toolkit.ts";
|
||||
import { assertElement } from "../util/assertElement.ts";
|
||||
|
||||
export type KeyBindingLayout = "default" | "vim";
|
||||
|
||||
type KeyBinding = {
|
||||
key: string;
|
||||
fun: (event: KeyboardEvent) => void;
|
||||
des: string;
|
||||
cat: string;
|
||||
};
|
||||
|
||||
type HighlightResultElement = "down" | "up" | "visible" | "bottom" | "top";
|
||||
|
||||
/* common base for layouts */
|
||||
const baseKeyBinding: Record<string, KeyBinding> = {
|
||||
Escape: {
|
||||
key: "ESC",
|
||||
fun: (event: KeyboardEvent) => removeFocus(event),
|
||||
des: "remove focus from the focused input",
|
||||
cat: "Control"
|
||||
},
|
||||
c: {
|
||||
key: "c",
|
||||
fun: () => copyURLToClipboard(),
|
||||
des: "copy url of the selected result to the clipboard",
|
||||
cat: "Results"
|
||||
},
|
||||
h: {
|
||||
key: "h",
|
||||
fun: () => toggleHelp(keyBindings),
|
||||
des: "toggle help window",
|
||||
cat: "Other"
|
||||
},
|
||||
i: {
|
||||
key: "i",
|
||||
fun: () => searchInputFocus(),
|
||||
des: "focus on the search input",
|
||||
cat: "Control"
|
||||
},
|
||||
n: {
|
||||
key: "n",
|
||||
fun: () => GoToNextPage(),
|
||||
des: "go to next page",
|
||||
cat: "Results"
|
||||
},
|
||||
o: {
|
||||
key: "o",
|
||||
fun: () => openResult(false),
|
||||
des: "open search result",
|
||||
cat: "Results"
|
||||
},
|
||||
p: {
|
||||
key: "p",
|
||||
fun: () => GoToPreviousPage(),
|
||||
des: "go to previous page",
|
||||
cat: "Results"
|
||||
},
|
||||
r: {
|
||||
key: "r",
|
||||
fun: () => reloadPage(),
|
||||
des: "reload page from the server",
|
||||
cat: "Control"
|
||||
},
|
||||
t: {
|
||||
key: "t",
|
||||
fun: () => openResult(true),
|
||||
des: "open the result in a new tab",
|
||||
cat: "Results"
|
||||
}
|
||||
};
|
||||
|
||||
const keyBindingLayouts: Record<KeyBindingLayout, Record<string, KeyBinding>> = {
|
||||
// SearXNG layout
|
||||
default: {
|
||||
ArrowLeft: {
|
||||
key: "←",
|
||||
fun: () => highlightResult("up")(),
|
||||
des: "select previous search result",
|
||||
cat: "Results"
|
||||
},
|
||||
ArrowRight: {
|
||||
key: "→",
|
||||
fun: () => highlightResult("down")(),
|
||||
des: "select next search result",
|
||||
cat: "Results"
|
||||
},
|
||||
...baseKeyBinding
|
||||
},
|
||||
|
||||
// Vim-like keyboard layout
|
||||
vim: {
|
||||
b: {
|
||||
key: "b",
|
||||
fun: () => scrollPage(-window.innerHeight),
|
||||
des: "scroll one page up",
|
||||
cat: "Navigation"
|
||||
},
|
||||
d: {
|
||||
key: "d",
|
||||
fun: () => scrollPage(window.innerHeight / 2),
|
||||
des: "scroll half a page down",
|
||||
cat: "Navigation"
|
||||
},
|
||||
f: {
|
||||
key: "f",
|
||||
fun: () => scrollPage(window.innerHeight),
|
||||
des: "scroll one page down",
|
||||
cat: "Navigation"
|
||||
},
|
||||
g: {
|
||||
key: "g",
|
||||
fun: () => scrollPageTo(-document.body.scrollHeight, "top"),
|
||||
des: "scroll to the top of the page",
|
||||
cat: "Navigation"
|
||||
},
|
||||
j: {
|
||||
key: "j",
|
||||
fun: () => highlightResult("down")(),
|
||||
des: "select next search result",
|
||||
cat: "Results"
|
||||
},
|
||||
k: {
|
||||
key: "k",
|
||||
fun: () => highlightResult("up")(),
|
||||
des: "select previous search result",
|
||||
cat: "Results"
|
||||
},
|
||||
u: {
|
||||
key: "u",
|
||||
fun: () => scrollPage(-window.innerHeight / 2),
|
||||
des: "scroll half a page up",
|
||||
cat: "Navigation"
|
||||
},
|
||||
v: {
|
||||
key: "v",
|
||||
fun: () => scrollPageTo(document.body.scrollHeight, "bottom"),
|
||||
des: "scroll to the bottom of the page",
|
||||
cat: "Navigation"
|
||||
},
|
||||
y: {
|
||||
key: "y",
|
||||
fun: () => copyURLToClipboard(),
|
||||
des: "copy url of the selected result to the clipboard",
|
||||
cat: "Results"
|
||||
},
|
||||
...baseKeyBinding
|
||||
}
|
||||
};
|
||||
|
||||
const keyBindings: Record<string, KeyBinding> =
|
||||
settings.hotkeys && settings.hotkeys in keyBindingLayouts
|
||||
? keyBindingLayouts[settings.hotkeys]
|
||||
: keyBindingLayouts.default;
|
||||
|
||||
const isElementInDetail = (element?: HTMLElement): boolean => {
|
||||
const ancestor = element?.closest(".detail, .result");
|
||||
return ancestor?.classList.contains("detail") ?? false;
|
||||
};
|
||||
|
||||
const getResultElement = (element?: HTMLElement): HTMLElement | undefined => {
|
||||
return element?.closest(".result") ?? undefined;
|
||||
};
|
||||
|
||||
const isImageResult = (resultElement?: HTMLElement): boolean => {
|
||||
return resultElement?.classList.contains("result-images") ?? false;
|
||||
};
|
||||
|
||||
const highlightResult =
|
||||
(which: HighlightResultElement | HTMLElement) =>
|
||||
(noScroll?: boolean, keepFocus?: boolean): void => {
|
||||
let effectiveWhich = which;
|
||||
let current = document.querySelector<HTMLElement>(".result[data-vim-selected]");
|
||||
if (!current) {
|
||||
// no selection : choose the first one
|
||||
current = document.querySelector<HTMLElement>(".result");
|
||||
if (!current) {
|
||||
// no first one : there are no results
|
||||
return;
|
||||
}
|
||||
// replace up/down actions by selecting first one
|
||||
if (which === "down" || which === "up") {
|
||||
effectiveWhich = current;
|
||||
}
|
||||
}
|
||||
|
||||
const results = Array.from(document.querySelectorAll<HTMLElement>(".result"));
|
||||
|
||||
let next: HTMLElement | undefined;
|
||||
|
||||
if (typeof effectiveWhich === "string") {
|
||||
switch (effectiveWhich) {
|
||||
case "visible": {
|
||||
const top = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const bot = top + document.documentElement.clientHeight;
|
||||
|
||||
for (const element of results) {
|
||||
const etop = element.offsetTop;
|
||||
const ebot = etop + element.clientHeight;
|
||||
if (ebot <= bot && etop > top) {
|
||||
next = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "down":
|
||||
next = results[results.indexOf(current) + 1] || current;
|
||||
break;
|
||||
case "up":
|
||||
next = results[results.indexOf(current) - 1] || current;
|
||||
break;
|
||||
case "bottom":
|
||||
next = results.at(-1);
|
||||
break;
|
||||
// biome-ignore lint/complexity/noUselessSwitchCase: fallthrough is intended
|
||||
case "top":
|
||||
default:
|
||||
[next] = results;
|
||||
}
|
||||
} else {
|
||||
next = effectiveWhich;
|
||||
}
|
||||
|
||||
if (next) {
|
||||
current.removeAttribute("data-vim-selected");
|
||||
next.setAttribute("data-vim-selected", "true");
|
||||
|
||||
if (!keepFocus) {
|
||||
const link = next.querySelector<HTMLAnchorElement>("h3 a") || next.querySelector<HTMLAnchorElement>("a");
|
||||
link?.focus();
|
||||
}
|
||||
|
||||
if (!noScroll) {
|
||||
mutable.scrollPageToSelected?.();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const reloadPage = (): void => {
|
||||
document.location.reload();
|
||||
};
|
||||
|
||||
const removeFocus = (event: KeyboardEvent): void => {
|
||||
const target = event.target as HTMLElement;
|
||||
const tagName = target?.tagName?.toLowerCase();
|
||||
|
||||
if (document.activeElement && (tagName === "input" || tagName === "select" || tagName === "textarea")) {
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
} else {
|
||||
mutable.closeDetail?.();
|
||||
}
|
||||
};
|
||||
|
||||
const pageButtonClick = (css_selector: string): void => {
|
||||
const button = document.querySelector<HTMLButtonElement>(css_selector);
|
||||
if (button) {
|
||||
button.click();
|
||||
}
|
||||
};
|
||||
|
||||
const GoToNextPage = (): void => {
|
||||
pageButtonClick('nav#pagination .next_page button[type="submit"]');
|
||||
};
|
||||
|
||||
const GoToPreviousPage = (): void => {
|
||||
pageButtonClick('nav#pagination .previous_page button[type="submit"]');
|
||||
};
|
||||
|
||||
mutable.scrollPageToSelected = (): void => {
|
||||
const sel = document.querySelector<HTMLElement>(".result[data-vim-selected]");
|
||||
if (!sel) return;
|
||||
|
||||
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) {
|
||||
// set to the top of page if the first element
|
||||
// is fully included in the viewport
|
||||
window.scroll(window.scrollX, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wtop > etop - offset) {
|
||||
window.scroll(window.scrollX, etop - offset);
|
||||
} else {
|
||||
const wbot = wtop + height;
|
||||
if (wbot < ebot + offset) {
|
||||
window.scroll(window.scrollX, ebot - height + offset);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const scrollPage = (amount: number): void => {
|
||||
window.scrollBy(0, amount);
|
||||
highlightResult("visible")();
|
||||
};
|
||||
|
||||
const scrollPageTo = (position: number, nav: HighlightResultElement): void => {
|
||||
window.scrollTo(0, position);
|
||||
highlightResult(nav)();
|
||||
};
|
||||
|
||||
const searchInputFocus = (): void => {
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
const q = document.querySelector<HTMLInputElement>("#q");
|
||||
if (q) {
|
||||
q.focus();
|
||||
|
||||
if (q.setSelectionRange) {
|
||||
const len = q.value.length;
|
||||
|
||||
q.setSelectionRange(len, len);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openResult = (newTab: boolean): void => {
|
||||
let link = document.querySelector<HTMLAnchorElement>(".result[data-vim-selected] h3 a");
|
||||
if (!link) {
|
||||
link = document.querySelector<HTMLAnchorElement>(".result[data-vim-selected] > a");
|
||||
}
|
||||
if (!link) return;
|
||||
|
||||
const url = link.getAttribute("href");
|
||||
if (url) {
|
||||
if (newTab) {
|
||||
window.open(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const initHelpContent = (divElement: HTMLElement, keyBindings: typeof baseKeyBinding): void => {
|
||||
const categories: Record<string, KeyBinding[]> = {};
|
||||
|
||||
for (const binding of Object.values(keyBindings)) {
|
||||
const { cat } = binding;
|
||||
categories[cat] ??= [];
|
||||
categories[cat].push(binding);
|
||||
}
|
||||
|
||||
const sortedCategoryKeys = Object.keys(categories).sort(
|
||||
(a, b) => (categories[b]?.length ?? 0) - (categories[a]?.length ?? 0)
|
||||
);
|
||||
|
||||
let html = '<a href="#" class="close" aria-label="close" title="close">×</a>';
|
||||
html += "<h3>How to navigate SearXNG with hotkeys</h3>";
|
||||
html += "<table>";
|
||||
|
||||
for (const [i, categoryKey] of sortedCategoryKeys.entries()) {
|
||||
const bindings = categories[categoryKey];
|
||||
if (!bindings || bindings.length === 0) continue;
|
||||
|
||||
const isFirst = i % 2 === 0;
|
||||
const isLast = i === sortedCategoryKeys.length - 1;
|
||||
|
||||
if (isFirst) {
|
||||
html += "<tr>";
|
||||
}
|
||||
|
||||
html += "<td>";
|
||||
html += `<h4>${categoryKey}</h4>`;
|
||||
html += '<ul class="list-unstyled">';
|
||||
|
||||
for (const binding of bindings) {
|
||||
html += `<li><kbd>${binding.key}</kbd> ${binding.des}</li>`;
|
||||
}
|
||||
|
||||
html += "</ul>";
|
||||
html += "</td>";
|
||||
|
||||
if (!isFirst || isLast) {
|
||||
html += "</tr>";
|
||||
}
|
||||
}
|
||||
|
||||
html += "</table>";
|
||||
|
||||
divElement.innerHTML = html;
|
||||
};
|
||||
|
||||
const toggleHelp = (keyBindings: typeof baseKeyBinding): void => {
|
||||
let helpPanel = document.querySelector<HTMLElement>("#vim-hotkeys-help");
|
||||
if (helpPanel) {
|
||||
// toggle hidden
|
||||
helpPanel.classList.toggle("invisible");
|
||||
} else {
|
||||
// first call
|
||||
helpPanel = Object.assign(document.createElement("div"), {
|
||||
id: "vim-hotkeys-help",
|
||||
className: "dialog-modal"
|
||||
});
|
||||
initHelpContent(helpPanel, keyBindings);
|
||||
const [body] = document.getElementsByTagName("body");
|
||||
if (body) {
|
||||
body.appendChild(helpPanel);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const copyURLToClipboard = async (): Promise<void> => {
|
||||
const selectedResult = document.querySelector<HTMLElement>(".result[data-vim-selected]");
|
||||
if (!selectedResult) return;
|
||||
|
||||
const resultAnchor = selectedResult.querySelector<HTMLAnchorElement>("a");
|
||||
assertElement(resultAnchor);
|
||||
|
||||
const url = resultAnchor.getAttribute("href");
|
||||
if (url) {
|
||||
if (window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(url);
|
||||
} else {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
const node = document.createElement("span");
|
||||
node.textContent = url;
|
||||
resultAnchor.appendChild(node);
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand("copy");
|
||||
node.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
mutable.selectImage?.(resultElement);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
|
||||
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 (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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mutable.selectNext = highlightResult("down");
|
||||
mutable.selectPrevious = highlightResult("up");
|
||||
74
client/simple/src/js/main/mapresult.js
Normal file
74
client/simple/src/js/main/mapresult.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
/* global L */
|
||||
(function (w, d, searxng) {
|
||||
'use strict';
|
||||
|
||||
searxng.ready(function () {
|
||||
searxng.on('.searxng_init_map', 'click', function (event) {
|
||||
// no more request
|
||||
this.classList.remove("searxng_init_map");
|
||||
|
||||
//
|
||||
var leaflet_target = this.dataset.leafletTarget;
|
||||
var map_lon = parseFloat(this.dataset.mapLon);
|
||||
var map_lat = parseFloat(this.dataset.mapLat);
|
||||
var map_zoom = parseFloat(this.dataset.mapZoom);
|
||||
var map_boundingbox = JSON.parse(this.dataset.mapBoundingbox);
|
||||
var map_geojson = JSON.parse(this.dataset.mapGeojson);
|
||||
|
||||
searxng.loadStyle('css/leaflet.css');
|
||||
searxng.loadScript('js/leaflet.js', function () {
|
||||
var map_bounds = null;
|
||||
if (map_boundingbox) {
|
||||
var southWest = L.latLng(map_boundingbox[0], map_boundingbox[2]);
|
||||
var northEast = L.latLng(map_boundingbox[1], map_boundingbox[3]);
|
||||
map_bounds = L.latLngBounds(southWest, northEast);
|
||||
}
|
||||
|
||||
// init map
|
||||
var map = L.map(leaflet_target);
|
||||
// create the tile layer with correct attribution
|
||||
var osmMapnikUrl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
var osmMapnikAttrib = 'Map data © <a href="https://openstreetmap.org">OpenStreetMap</a> contributors';
|
||||
var osmMapnik = new L.TileLayer(osmMapnikUrl, {minZoom: 1, maxZoom: 19, attribution: osmMapnikAttrib});
|
||||
var osmWikimediaUrl = 'https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png';
|
||||
var osmWikimediaAttrib = 'Wikimedia maps | Maps data © <a href="https://openstreetmap.org">OpenStreetMap contributors</a>';
|
||||
var osmWikimedia = new L.TileLayer(osmWikimediaUrl, {minZoom: 1, maxZoom: 19, attribution: osmWikimediaAttrib});
|
||||
// init map view
|
||||
if (map_bounds) {
|
||||
// TODO hack: https://github.com/Leaflet/Leaflet/issues/2021
|
||||
// Still useful ?
|
||||
setTimeout(function () {
|
||||
map.fitBounds(map_bounds, {
|
||||
maxZoom: 17
|
||||
});
|
||||
}, 0);
|
||||
} else if (map_lon && map_lat) {
|
||||
if (map_zoom) {
|
||||
map.setView(new L.latLng(map_lat, map_lon), map_zoom);
|
||||
} else {
|
||||
map.setView(new L.latLng(map_lat, map_lon), 8);
|
||||
}
|
||||
}
|
||||
|
||||
map.addLayer(osmMapnik);
|
||||
|
||||
var baseLayers = {
|
||||
"OSM Mapnik": osmMapnik,
|
||||
"OSM Wikimedia": osmWikimedia,
|
||||
};
|
||||
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
|
||||
if (map_geojson) {
|
||||
L.geoJson(map_geojson).addTo(map);
|
||||
} /* else if(map_bounds) {
|
||||
L.rectangle(map_bounds, {color: "#ff7800", weight: 3, fill:false}).addTo(map);
|
||||
} */
|
||||
});
|
||||
|
||||
// this event occur only once per element
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
})(window, document, window.searxng);
|
||||
53
client/simple/src/js/main/preferences.js
Normal file
53
client/simple/src/js/main/preferences.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
(function (w, d, searxng) {
|
||||
'use strict';
|
||||
|
||||
if (searxng.endpoint !== 'preferences') {
|
||||
return;
|
||||
}
|
||||
|
||||
searxng.ready(function () {
|
||||
let engine_descriptions = null;
|
||||
function load_engine_descriptions () {
|
||||
if (engine_descriptions == null) {
|
||||
searxng.http("GET", "engine_descriptions.json").then(function (content) {
|
||||
engine_descriptions = JSON.parse(content);
|
||||
for (const [engine_name, description] of Object.entries(engine_descriptions)) {
|
||||
let elements = d.querySelectorAll('[data-engine-name="' + engine_name + '"] .engine-description');
|
||||
for (const element of elements) {
|
||||
let source = ' (<i>' + searxng.settings.translations.Source + ': ' + description[1] + '</i>)';
|
||||
element.innerHTML = description[0] + source;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const el of d.querySelectorAll('[data-engine-name]')) {
|
||||
searxng.on(el, 'mouseenter', load_engine_descriptions);
|
||||
}
|
||||
|
||||
const enableAllEngines = d.querySelectorAll(".enable-all-engines");
|
||||
const disableAllEngines = d.querySelectorAll(".disable-all-engines");
|
||||
const engineToggles = d.querySelectorAll('tbody input[type=checkbox][class~=checkbox-onoff]');
|
||||
const toggleEngines = (enable) => {
|
||||
for (const el of engineToggles) {
|
||||
// check if element visible, so that only engines of the current category are modified
|
||||
if (el.offsetParent !== null) el.checked = !enable;
|
||||
}
|
||||
};
|
||||
for (const el of enableAllEngines) {
|
||||
searxng.on(el, 'click', () => toggleEngines(true));
|
||||
}
|
||||
for (const el of disableAllEngines) {
|
||||
searxng.on(el, 'click', () => toggleEngines(false));
|
||||
}
|
||||
|
||||
const copyHashButton = d.querySelector("#copy-hash");
|
||||
searxng.on(copyHashButton, 'click', (e) => {
|
||||
e.preventDefault();
|
||||
navigator.clipboard.writeText(copyHashButton.dataset.hash);
|
||||
copyHashButton.innerText = copyHashButton.dataset.copiedText;
|
||||
});
|
||||
});
|
||||
})(window, document, window.searxng);
|
||||
@@ -1,76 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { http, listen, settings } from "../toolkit.ts";
|
||||
import { assertElement } from "../util/assertElement.ts";
|
||||
|
||||
let engineDescriptions: Record<string, [string, string]> | undefined;
|
||||
|
||||
const loadEngineDescriptions = async (): Promise<void> => {
|
||||
if (engineDescriptions) return;
|
||||
try {
|
||||
const res = await http("GET", "engine_descriptions.json");
|
||||
engineDescriptions = await res.json();
|
||||
} catch (error) {
|
||||
console.error("Error fetching engineDescriptions:", error);
|
||||
}
|
||||
if (!engineDescriptions) return;
|
||||
|
||||
for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) {
|
||||
const elements = document.querySelectorAll<HTMLElement>(`[data-engine-name="${engine_name}"] .engine-description`);
|
||||
const sourceText = ` (<i>${settings.translations?.Source}: ${source}</i>)`;
|
||||
|
||||
for (const element of elements) {
|
||||
element.innerHTML = description + sourceText;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleEngines = (enable: boolean, engineToggles: NodeListOf<HTMLInputElement>): void => {
|
||||
for (const engineToggle of engineToggles) {
|
||||
// check if element visible, so that only engines of the current category are modified
|
||||
if (engineToggle.offsetParent) {
|
||||
engineToggle.checked = !enable;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engineElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>("[data-engine-name]");
|
||||
for (const engineElement of engineElements) {
|
||||
listen("mouseenter", engineElement, loadEngineDescriptions);
|
||||
}
|
||||
|
||||
const engineToggles: NodeListOf<HTMLInputElement> = document.querySelectorAll<HTMLInputElement>(
|
||||
"tbody input[type=checkbox][class~=checkbox-onoff]"
|
||||
);
|
||||
|
||||
const enableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".enable-all-engines");
|
||||
for (const engine of enableAllEngines) {
|
||||
listen("click", engine, () => toggleEngines(true, engineToggles));
|
||||
}
|
||||
|
||||
const disableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".disable-all-engines");
|
||||
for (const engine of disableAllEngines) {
|
||||
listen("click", engine, () => toggleEngines(false, engineToggles));
|
||||
}
|
||||
|
||||
listen("click", "#copy-hash", async function (this: HTMLElement) {
|
||||
const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
|
||||
assertElement(target);
|
||||
|
||||
if (window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(target.innerText);
|
||||
} else {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand("copy");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.dataset.copiedText) {
|
||||
this.innerText = this.dataset.copiedText;
|
||||
}
|
||||
});
|
||||
184
client/simple/src/js/main/results.js
Normal file
184
client/simple/src/js/main/results.js
Normal file
@@ -0,0 +1,184 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
import "../../../node_modules/swiped-events/src/swiped-events.js";
|
||||
|
||||
(function (w, d, searxng) {
|
||||
'use strict';
|
||||
|
||||
if (searxng.endpoint !== 'results') {
|
||||
return;
|
||||
}
|
||||
|
||||
searxng.ready(function () {
|
||||
d.querySelectorAll('#urls img').forEach(
|
||||
img =>
|
||||
img.addEventListener(
|
||||
'error', () => {
|
||||
// console.log("ERROR can't load: " + img.src);
|
||||
img.src = window.searxng.settings.theme_static_path + "/img/img_load_error.svg";
|
||||
},
|
||||
{once: true}
|
||||
));
|
||||
|
||||
if (d.querySelector('#search_url button#copy_url')) {
|
||||
d.querySelector('#search_url button#copy_url').style.display = "block";
|
||||
}
|
||||
|
||||
searxng.on('.btn-collapse', 'click', function () {
|
||||
var btnLabelCollapsed = this.getAttribute('data-btn-text-collapsed');
|
||||
var btnLabelNotCollapsed = this.getAttribute('data-btn-text-not-collapsed');
|
||||
var target = this.getAttribute('data-target');
|
||||
var targetElement = d.querySelector(target);
|
||||
var html = this.innerHTML;
|
||||
if (this.classList.contains('collapsed')) {
|
||||
html = html.replace(btnLabelCollapsed, btnLabelNotCollapsed);
|
||||
} else {
|
||||
html = html.replace(btnLabelNotCollapsed, btnLabelCollapsed);
|
||||
}
|
||||
this.innerHTML = html;
|
||||
this.classList.toggle('collapsed');
|
||||
targetElement.classList.toggle('invisible');
|
||||
});
|
||||
|
||||
searxng.on('.media-loader', 'click', function () {
|
||||
var target = this.getAttribute('data-target');
|
||||
var iframe_load = d.querySelector(target + ' > iframe');
|
||||
var srctest = iframe_load.getAttribute('src');
|
||||
if (srctest === null || srctest === undefined || srctest === false) {
|
||||
iframe_load.setAttribute('src', iframe_load.getAttribute('data-src'));
|
||||
}
|
||||
});
|
||||
|
||||
searxng.on('#copy_url', 'click', function () {
|
||||
var target = this.parentElement.querySelector('pre');
|
||||
navigator.clipboard.writeText(target.innerText);
|
||||
this.innerText = this.dataset.copiedText;
|
||||
});
|
||||
|
||||
// searxng.selectImage (gallery)
|
||||
// -----------------------------
|
||||
|
||||
// setTimeout() ID, needed to cancel *last* loadImage
|
||||
let imgTimeoutID;
|
||||
|
||||
// progress spinner, while an image is loading
|
||||
const imgLoaderSpinner = d.createElement('div');
|
||||
imgLoaderSpinner.classList.add('loader');
|
||||
|
||||
// singleton image object, which is used for all loading processes of a
|
||||
// detailed image
|
||||
const imgLoader = new Image();
|
||||
|
||||
const loadImage = (imgSrc, onSuccess) => {
|
||||
// if defered image load exists, stop defered task.
|
||||
if (imgTimeoutID) clearTimeout(imgTimeoutID);
|
||||
|
||||
// defer load of the detail image for 1 sec
|
||||
imgTimeoutID = setTimeout(() => {
|
||||
imgLoader.src = imgSrc;
|
||||
}, 1000);
|
||||
|
||||
// set handlers in the on-properties
|
||||
imgLoader.onload = () => {
|
||||
onSuccess();
|
||||
imgLoaderSpinner.remove();
|
||||
};
|
||||
imgLoader.onerror = () => {
|
||||
imgLoaderSpinner.remove();
|
||||
};
|
||||
};
|
||||
|
||||
searxng.selectImage = (resultElement) => {
|
||||
|
||||
// add a class that can be evaluated in the CSS and indicates that the
|
||||
// detail view is open
|
||||
d.getElementById('results').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 none element given by the caller, stop here
|
||||
if (!resultElement) return;
|
||||
|
||||
// find <img> object in the element, if there is none, stop here.
|
||||
const img = resultElement.querySelector('.result-images-source img');
|
||||
if (!img) return;
|
||||
|
||||
// <img src="" data-src="http://example.org/image.jpg">
|
||||
const src = img.getAttribute('data-src');
|
||||
|
||||
// already loaded high-res image or no high-res image available
|
||||
if (!src) return;
|
||||
|
||||
// use the image thumbnail until the image is fully loaded
|
||||
const thumbnail = resultElement.querySelector('.image_thumbnail');
|
||||
img.src = thumbnail.src;
|
||||
|
||||
// show a progress spinner
|
||||
const detailElement = resultElement.querySelector('.detail');
|
||||
detailElement.appendChild(imgLoaderSpinner);
|
||||
|
||||
// load full size image in background
|
||||
loadImage(src, () => {
|
||||
// after the singelton loadImage has loaded the detail image into the
|
||||
// cache, it can be used in the origin <img> as src property.
|
||||
img.src = src;
|
||||
img.removeAttribute('data-src');
|
||||
});
|
||||
};
|
||||
|
||||
searxng.closeDetail = function () {
|
||||
d.getElementById('results').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.on('.result-detail-close', 'click', e => {
|
||||
e.preventDefault();
|
||||
searxng.closeDetail();
|
||||
});
|
||||
searxng.on('.result-detail-previous', 'click', e => {
|
||||
e.preventDefault();
|
||||
searxng.selectPrevious(false);
|
||||
});
|
||||
searxng.on('.result-detail-next', 'click', e => {
|
||||
e.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();
|
||||
});
|
||||
|
||||
d.querySelectorAll('.swipe-horizontal').forEach(
|
||||
obj => {
|
||||
obj.addEventListener('swiped-left', function () {
|
||||
searxng.selectNext(false);
|
||||
});
|
||||
obj.addEventListener('swiped-right', function () {
|
||||
searxng.selectPrevious(false);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
w.addEventListener('scroll', function () {
|
||||
var e = d.getElementById('backToTop'),
|
||||
scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
|
||||
results = d.getElementById('results');
|
||||
if (e !== null) {
|
||||
if (scrollTop >= 100) {
|
||||
results.classList.add('scrolling');
|
||||
} else {
|
||||
results.classList.remove('scrolling');
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
});
|
||||
|
||||
})(window, document, window.searxng);
|
||||
@@ -1,190 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import "../../../node_modules/swiped-events/src/swiped-events.js";
|
||||
import { listen, mutable, settings } from "../toolkit.ts";
|
||||
import { assertElement } from "../util/assertElement.ts";
|
||||
|
||||
let imgTimeoutID: number;
|
||||
|
||||
const imageLoader = (resultElement: HTMLElement): void => {
|
||||
if (imgTimeoutID) clearTimeout(imgTimeoutID);
|
||||
|
||||
const imgElement = resultElement.querySelector<HTMLImageElement>(".result-images-source img");
|
||||
if (!imgElement) return;
|
||||
|
||||
// use thumbnail until full image loads
|
||||
const thumbnail = resultElement.querySelector<HTMLImageElement>(".image_thumbnail");
|
||||
if (thumbnail) {
|
||||
if (thumbnail.src === `${settings.theme_static_path}/img/img_load_error.svg`) return;
|
||||
|
||||
imgElement.onerror = (): void => {
|
||||
imgElement.src = thumbnail.src;
|
||||
};
|
||||
|
||||
imgElement.src = thumbnail.src;
|
||||
}
|
||||
|
||||
const imgSource = imgElement.getAttribute("data-src");
|
||||
if (!imgSource) return;
|
||||
|
||||
// unsafe nodejs specific, cast to https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#return_value
|
||||
// https://github.com/searxng/searxng/pull/5073#discussion_r2265767231
|
||||
imgTimeoutID = setTimeout(() => {
|
||||
imgElement.src = imgSource;
|
||||
imgElement.removeAttribute("data-src");
|
||||
}, 1000) as unknown as number;
|
||||
};
|
||||
|
||||
const imageThumbnails: NodeListOf<HTMLImageElement> = document.querySelectorAll<HTMLImageElement>(
|
||||
"#urls img.image_thumbnail, img.thumbnail"
|
||||
);
|
||||
for (const thumbnail of imageThumbnails) {
|
||||
if (thumbnail.complete && thumbnail.naturalWidth === 0) {
|
||||
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
|
||||
}
|
||||
|
||||
thumbnail.onerror = (): void => {
|
||||
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
|
||||
};
|
||||
}
|
||||
|
||||
const copyUrlButton: HTMLButtonElement | null =
|
||||
document.querySelector<HTMLButtonElement>("#search_url button#copy_url");
|
||||
copyUrlButton?.style.setProperty("display", "block");
|
||||
|
||||
mutable.selectImage = (resultElement: HTMLElement): void => {
|
||||
// add a class that can be evaluated in the CSS and indicates that the
|
||||
// detail view is open
|
||||
const resultsElement = document.getElementById("results");
|
||||
resultsElement?.classList.add("image-detail-open");
|
||||
|
||||
// add a hash to the browser history so that pressing back doesn't return
|
||||
// to the previous page this allows us to dismiss the image details on
|
||||
// pressing the back button on mobile devices
|
||||
window.location.hash = "#image-viewer";
|
||||
|
||||
mutable.scrollPageToSelected?.();
|
||||
|
||||
// if there is no element given by the caller, stop here
|
||||
if (!resultElement) return;
|
||||
|
||||
imageLoader(resultElement);
|
||||
};
|
||||
|
||||
mutable.closeDetail = (): void => {
|
||||
const resultsElement = document.getElementById("results");
|
||||
resultsElement?.classList.remove("image-detail-open");
|
||||
|
||||
// remove #image-viewer hash from url by navigating back
|
||||
if (window.location.hash === "#image-viewer") {
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
mutable.scrollPageToSelected?.();
|
||||
};
|
||||
|
||||
listen("click", ".btn-collapse", function (this: HTMLElement) {
|
||||
const btnLabelCollapsed = this.getAttribute("data-btn-text-collapsed");
|
||||
const btnLabelNotCollapsed = this.getAttribute("data-btn-text-not-collapsed");
|
||||
const target = this.getAttribute("data-target");
|
||||
|
||||
if (!(target && btnLabelCollapsed && btnLabelNotCollapsed)) return;
|
||||
|
||||
const targetElement = document.querySelector<HTMLElement>(target);
|
||||
assertElement(targetElement);
|
||||
|
||||
const isCollapsed = this.classList.contains("collapsed");
|
||||
const newLabel = isCollapsed ? btnLabelNotCollapsed : btnLabelCollapsed;
|
||||
const oldLabel = isCollapsed ? btnLabelCollapsed : btnLabelNotCollapsed;
|
||||
|
||||
this.innerHTML = this.innerHTML.replace(oldLabel, newLabel);
|
||||
this.classList.toggle("collapsed");
|
||||
|
||||
targetElement.classList.toggle("invisible");
|
||||
});
|
||||
|
||||
listen("click", ".media-loader", function (this: HTMLElement) {
|
||||
const target = this.getAttribute("data-target");
|
||||
if (!target) return;
|
||||
|
||||
const iframeLoad = document.querySelector<HTMLIFrameElement>(`${target} > iframe`);
|
||||
assertElement(iframeLoad);
|
||||
|
||||
const srctest = iframeLoad.getAttribute("src");
|
||||
if (!srctest) {
|
||||
const dataSrc = iframeLoad.getAttribute("data-src");
|
||||
if (dataSrc) {
|
||||
iframeLoad.setAttribute("src", dataSrc);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
listen("click", "#copy_url", async function (this: HTMLElement) {
|
||||
const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
|
||||
assertElement(target);
|
||||
|
||||
if (window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(target.innerText);
|
||||
} else {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand("copy");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.dataset.copiedText) {
|
||||
this.innerText = this.dataset.copiedText;
|
||||
}
|
||||
});
|
||||
|
||||
listen("click", ".result-detail-close", (event: Event) => {
|
||||
event.preventDefault();
|
||||
mutable.closeDetail?.();
|
||||
});
|
||||
|
||||
listen("click", ".result-detail-previous", (event: Event) => {
|
||||
event.preventDefault();
|
||||
mutable.selectPrevious?.(false);
|
||||
});
|
||||
|
||||
listen("click", ".result-detail-next", (event: Event) => {
|
||||
event.preventDefault();
|
||||
mutable.selectNext?.(false);
|
||||
});
|
||||
|
||||
// listen for the back button to be pressed and dismiss the image details when called
|
||||
window.addEventListener("hashchange", () => {
|
||||
if (window.location.hash !== "#image-viewer") {
|
||||
mutable.closeDetail?.();
|
||||
}
|
||||
});
|
||||
|
||||
const swipeHorizontal: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".swipe-horizontal");
|
||||
for (const element of swipeHorizontal) {
|
||||
listen("swiped-left", element, () => {
|
||||
mutable.selectNext?.(false);
|
||||
});
|
||||
|
||||
listen("swiped-right", element, () => {
|
||||
mutable.selectPrevious?.(false);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener(
|
||||
"scroll",
|
||||
() => {
|
||||
const 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
|
||||
);
|
||||
210
client/simple/src/js/main/search.js
Normal file
210
client/simple/src/js/main/search.js
Normal file
@@ -0,0 +1,210 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
/* exported AutoComplete */
|
||||
|
||||
import AutoComplete from "../../../node_modules/autocomplete-js/dist/autocomplete.js";
|
||||
|
||||
(function (w, d, searxng) {
|
||||
'use strict';
|
||||
|
||||
var qinput_id = "q", qinput;
|
||||
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 50em)").matches;
|
||||
|
||||
function submitIfQuery () {
|
||||
if (qinput.value.length > 0) {
|
||||
var search = document.getElementById('search');
|
||||
setTimeout(search.submit.bind(search), 0);
|
||||
}
|
||||
}
|
||||
|
||||
function createClearButton (qinput) {
|
||||
var cs = document.getElementById('clear_search');
|
||||
var updateClearButton = function () {
|
||||
if (qinput.value.length === 0) {
|
||||
cs.classList.add("empty");
|
||||
} else {
|
||||
cs.classList.remove("empty");
|
||||
}
|
||||
};
|
||||
|
||||
// update status, event listener
|
||||
updateClearButton();
|
||||
cs.addEventListener('click', function (ev) {
|
||||
qinput.value = '';
|
||||
qinput.focus();
|
||||
updateClearButton();
|
||||
ev.preventDefault();
|
||||
});
|
||||
qinput.addEventListener('input', updateClearButton, false);
|
||||
}
|
||||
|
||||
searxng.ready(function () {
|
||||
qinput = d.getElementById(qinput_id);
|
||||
|
||||
if (qinput !== null) {
|
||||
// clear button
|
||||
createClearButton(qinput);
|
||||
|
||||
// autocompleter
|
||||
if (searxng.settings.autocomplete) {
|
||||
searxng.autocomplete = AutoComplete.call(w, {
|
||||
Url: "./autocompleter",
|
||||
EmptyMessage: searxng.settings.translations.no_item_found,
|
||||
HttpMethod: searxng.settings.method,
|
||||
HttpHeaders: {
|
||||
"Content-type": "application/x-www-form-urlencoded",
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
},
|
||||
MinChars: searxng.settings.autocomplete_min,
|
||||
Delay: 300,
|
||||
_Position: function () {},
|
||||
_Open: function () {
|
||||
var params = this;
|
||||
Array.prototype.forEach.call(this.DOMResults.getElementsByTagName("li"), function (li) {
|
||||
if (li.getAttribute("class") != "locked") {
|
||||
li.onmousedown = function () {
|
||||
params._Select(li);
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
_Select: function (item) {
|
||||
AutoComplete.defaults._Select.call(this, item);
|
||||
var form = item.closest('form');
|
||||
if (form) {
|
||||
form.submit();
|
||||
}
|
||||
},
|
||||
_MinChars: function () {
|
||||
if (this.Input.value.indexOf('!') > -1) {
|
||||
return 0;
|
||||
} else {
|
||||
return AutoComplete.defaults._MinChars.call(this);
|
||||
}
|
||||
},
|
||||
KeyboardMappings: Object.assign({}, AutoComplete.defaults.KeyboardMappings, {
|
||||
"KeyUpAndDown_up": Object.assign({}, AutoComplete.defaults.KeyboardMappings.KeyUpAndDown_up, {
|
||||
Callback: function (event) {
|
||||
AutoComplete.defaults.KeyboardMappings.KeyUpAndDown_up.Callback.call(this, event);
|
||||
var liActive = this.DOMResults.querySelector("li.active");
|
||||
if (liActive) {
|
||||
AutoComplete.defaults._Select.call(this, liActive);
|
||||
}
|
||||
},
|
||||
}),
|
||||
"Tab": Object.assign({}, AutoComplete.defaults.KeyboardMappings.Enter, {
|
||||
Conditions: [{
|
||||
Is: 9,
|
||||
Not: false
|
||||
}],
|
||||
Callback: function (event) {
|
||||
if (this.DOMResults.getAttribute("class").indexOf("open") != -1) {
|
||||
var liActive = this.DOMResults.querySelector("li.active");
|
||||
if (liActive !== null) {
|
||||
AutoComplete.defaults._Select.call(this, liActive);
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}),
|
||||
}, "#" + qinput_id);
|
||||
}
|
||||
|
||||
/*
|
||||
Monkey patch autocomplete.js to fix a bug
|
||||
With the POST method, the values are not URL encoded: query like "1 + 1" are sent as "1 1" since space are URL encoded as plus.
|
||||
See HTML specifications:
|
||||
* HTML5: https://url.spec.whatwg.org/#concept-urlencoded-serializer
|
||||
* HTML4: https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1
|
||||
|
||||
autocomplete.js does not URL encode the name and values:
|
||||
https://github.com/autocompletejs/autocomplete.js/blob/87069524f3b95e68f1b54d8976868e0eac1b2c83/src/autocomplete.ts#L665
|
||||
|
||||
The monkey patch overrides the compiled version of the ajax function.
|
||||
See https://github.com/autocompletejs/autocomplete.js/blob/87069524f3b95e68f1b54d8976868e0eac1b2c83/dist/autocomplete.js#L143-L158
|
||||
The patch changes only the line 156 from
|
||||
params.Request.send(params._QueryArg() + "=" + params._Pre());
|
||||
to
|
||||
params.Request.send(encodeURIComponent(params._QueryArg()) + "=" + encodeURIComponent(params._Pre()));
|
||||
|
||||
Related to:
|
||||
* https://github.com/autocompletejs/autocomplete.js/issues/78
|
||||
* https://github.com/searxng/searxng/issues/1695
|
||||
*/
|
||||
AutoComplete.prototype.ajax = function (params, request, timeout) {
|
||||
if (timeout === void 0) { timeout = true; }
|
||||
if (params.$AjaxTimer) {
|
||||
window.clearTimeout(params.$AjaxTimer);
|
||||
}
|
||||
if (timeout === true) {
|
||||
params.$AjaxTimer = window.setTimeout(AutoComplete.prototype.ajax.bind(null, params, request, false), params.Delay);
|
||||
} else {
|
||||
if (params.Request) {
|
||||
params.Request.abort();
|
||||
}
|
||||
params.Request = request;
|
||||
params.Request.send(encodeURIComponent(params._QueryArg()) + "=" + encodeURIComponent(params._Pre()));
|
||||
}
|
||||
};
|
||||
|
||||
if (!isMobile && document.querySelector('.index_endpoint')) {
|
||||
qinput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (
|
||||
qinput !== null
|
||||
&& 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
|
||||
&& d.querySelector(".search_filters") != null
|
||||
) {
|
||||
searxng.on(d.getElementById('safesearch'), 'change', submitIfQuery);
|
||||
searxng.on(d.getElementById('time_range'), 'change', submitIfQuery);
|
||||
searxng.on(d.getElementById('language'), 'change', submitIfQuery);
|
||||
}
|
||||
|
||||
const categoryButtons = d.querySelectorAll("button.category_button");
|
||||
for (let button of categoryButtons) {
|
||||
searxng.on(button, 'click', (event) => {
|
||||
if (event.shiftKey) {
|
||||
event.preventDefault();
|
||||
button.classList.toggle("selected");
|
||||
return;
|
||||
}
|
||||
|
||||
// manually deselect the old selection when a new category is selected
|
||||
const selectedCategories = d.querySelectorAll("button.category_button.selected");
|
||||
for (let categoryButton of selectedCategories) {
|
||||
categoryButton.classList.remove("selected");
|
||||
}
|
||||
button.classList.add("selected");
|
||||
})
|
||||
}
|
||||
|
||||
// override form submit action to update the actually selected categories
|
||||
const form = d.querySelector("#search");
|
||||
if (form != null) {
|
||||
searxng.on(form, 'submit', (event) => {
|
||||
event.preventDefault();
|
||||
const categoryValuesInput = d.querySelector("#selected-categories");
|
||||
if (categoryValuesInput) {
|
||||
let categoryValues = [];
|
||||
for (let categoryButton of categoryButtons) {
|
||||
if (categoryButton.classList.contains("selected")) {
|
||||
categoryValues.push(categoryButton.name.replace("category_", ""));
|
||||
}
|
||||
}
|
||||
categoryValuesInput.value = categoryValues.join(",");
|
||||
}
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})(window, document, window.searxng);
|
||||
@@ -1,94 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { listen } from "../toolkit.ts";
|
||||
import { getElement } from "../util/getElement.ts";
|
||||
|
||||
const searchForm: HTMLFormElement = getElement<HTMLFormElement>("search");
|
||||
const searchInput: HTMLInputElement = getElement<HTMLInputElement>("q");
|
||||
const searchReset: HTMLButtonElement = getElement<HTMLButtonElement>("clear_search");
|
||||
|
||||
const isMobile: boolean = window.matchMedia("(max-width: 50em)").matches;
|
||||
const isResultsPage: boolean = document.querySelector("main")?.id === "main_results";
|
||||
|
||||
const categoryButtons: HTMLButtonElement[] = Array.from(
|
||||
document.querySelectorAll<HTMLButtonElement>("#categories_container button.category")
|
||||
);
|
||||
|
||||
if (searchInput.value.length === 0) {
|
||||
searchReset.classList.add("empty");
|
||||
}
|
||||
|
||||
// focus search input on large screens
|
||||
if (!(isMobile || isResultsPage)) {
|
||||
searchInput.focus();
|
||||
}
|
||||
|
||||
// On mobile, move cursor to the end of the input on focus
|
||||
if (isMobile) {
|
||||
listen("focus", searchInput, () => {
|
||||
// Defer cursor move until the next frame to prevent a visual jump
|
||||
requestAnimationFrame(() => {
|
||||
const end = searchInput.value.length;
|
||||
searchInput.setSelectionRange(end, end);
|
||||
searchInput.scrollLeft = searchInput.scrollWidth;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
listen("input", searchInput, () => {
|
||||
searchReset.classList.toggle("empty", searchInput.value.length === 0);
|
||||
});
|
||||
|
||||
listen("click", searchReset, (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
searchInput.value = "";
|
||||
searchInput.focus();
|
||||
searchReset.classList.add("empty");
|
||||
});
|
||||
|
||||
for (const button of categoryButtons) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.querySelector("div.search_filters")) {
|
||||
const safesearchElement = document.getElementById("safesearch");
|
||||
if (safesearchElement) {
|
||||
listen("change", safesearchElement, () => searchForm.submit());
|
||||
}
|
||||
|
||||
const timeRangeElement = document.getElementById("time_range");
|
||||
if (timeRangeElement) {
|
||||
listen("change", timeRangeElement, () => searchForm.submit());
|
||||
}
|
||||
|
||||
const languageElement = document.getElementById("language");
|
||||
if (languageElement) {
|
||||
listen("change", languageElement, () => searchForm.submit());
|
||||
}
|
||||
}
|
||||
|
||||
// override searchForm submit event
|
||||
listen("submit", searchForm, (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (categoryButtons.length > 0) {
|
||||
const searchCategories = getElement<HTMLInputElement>("selected-categories");
|
||||
searchCategories.value = categoryButtons
|
||||
.filter((button) => button.classList.contains("selected"))
|
||||
.map((button) => button.name.replace("category_", ""))
|
||||
.join(",");
|
||||
}
|
||||
|
||||
searchForm.submit();
|
||||
});
|
||||
@@ -1,93 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
absDependencies,
|
||||
addDependencies,
|
||||
create,
|
||||
divideDependencies,
|
||||
eDependencies,
|
||||
evaluateDependencies,
|
||||
expDependencies,
|
||||
factorialDependencies,
|
||||
gcdDependencies,
|
||||
lcmDependencies,
|
||||
log1pDependencies,
|
||||
log2Dependencies,
|
||||
log10Dependencies,
|
||||
logDependencies,
|
||||
modDependencies,
|
||||
multiplyDependencies,
|
||||
nthRootDependencies,
|
||||
piDependencies,
|
||||
powDependencies,
|
||||
roundDependencies,
|
||||
signDependencies,
|
||||
sqrtDependencies,
|
||||
subtractDependencies
|
||||
} from "mathjs/number";
|
||||
import { Plugin } from "../Plugin.ts";
|
||||
import { appendAnswerElement } from "../util/appendAnswerElement.ts";
|
||||
import { getElement } from "../util/getElement.ts";
|
||||
|
||||
/**
|
||||
* Parses and solves mathematical expressions. Can do basic arithmetic and
|
||||
* evaluate some functions.
|
||||
*
|
||||
* @example
|
||||
* "(3 + 5) / 2" = "4"
|
||||
* "e ^ 2 + pi" = "10.530648752520442"
|
||||
* "gcd(48, 18) + lcm(4, 5)" = "26"
|
||||
*
|
||||
* @remarks
|
||||
* Depends on `mathjs` library.
|
||||
*/
|
||||
export default class Calculator extends Plugin {
|
||||
public constructor() {
|
||||
super("calculator");
|
||||
}
|
||||
|
||||
/**
|
||||
* @remarks
|
||||
* Compare bundle size after adding or removing features.
|
||||
*/
|
||||
private static readonly math = create({
|
||||
...absDependencies,
|
||||
...addDependencies,
|
||||
...divideDependencies,
|
||||
...eDependencies,
|
||||
...evaluateDependencies,
|
||||
...expDependencies,
|
||||
...factorialDependencies,
|
||||
...gcdDependencies,
|
||||
...lcmDependencies,
|
||||
...log10Dependencies,
|
||||
...log1pDependencies,
|
||||
...log2Dependencies,
|
||||
...logDependencies,
|
||||
...modDependencies,
|
||||
...multiplyDependencies,
|
||||
...nthRootDependencies,
|
||||
...piDependencies,
|
||||
...powDependencies,
|
||||
...roundDependencies,
|
||||
...signDependencies,
|
||||
...sqrtDependencies,
|
||||
...subtractDependencies
|
||||
});
|
||||
|
||||
protected async run(): Promise<string | undefined> {
|
||||
const searchInput = getElement<HTMLInputElement>("q");
|
||||
|
||||
try {
|
||||
const node = Calculator.math.parse(searchInput.value);
|
||||
return `${node.toString()} = ${node.evaluate()}`;
|
||||
} catch {
|
||||
// not a compatible math expression
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected async post(result: string): Promise<void> {
|
||||
appendAnswerElement(result);
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { Plugin } from "../Plugin.ts";
|
||||
import { http, settings } from "../toolkit.ts";
|
||||
import { assertElement } from "../util/assertElement.ts";
|
||||
import { getElement } from "../util/getElement.ts";
|
||||
|
||||
/**
|
||||
* Automatically loads the next page when scrolling to bottom of the current page.
|
||||
*/
|
||||
export default class InfiniteScroll extends Plugin {
|
||||
public constructor() {
|
||||
super("infiniteScroll");
|
||||
}
|
||||
|
||||
protected async run(): Promise<void> {
|
||||
const resultsElement = getElement<HTMLElement>("results");
|
||||
|
||||
const onlyImages: boolean = resultsElement.classList.contains("only_template_images");
|
||||
const observedSelector = "article.result:last-child";
|
||||
|
||||
const spinnerElement = document.createElement("div");
|
||||
spinnerElement.className = "loader";
|
||||
|
||||
const loadNextPage = async (callback: () => void): Promise<void> => {
|
||||
const searchForm = document.querySelector<HTMLFormElement>("#search");
|
||||
assertElement(searchForm);
|
||||
|
||||
const form = document.querySelector<HTMLFormElement>("#pagination form.next_page");
|
||||
assertElement(form);
|
||||
|
||||
const action = searchForm.getAttribute("action");
|
||||
if (!action) {
|
||||
throw new Error("Form action not defined");
|
||||
}
|
||||
|
||||
const paginationElement = document.querySelector<HTMLElement>("#pagination");
|
||||
assertElement(paginationElement);
|
||||
|
||||
paginationElement.replaceChildren(spinnerElement);
|
||||
|
||||
try {
|
||||
const res = await http("POST", action, { body: new FormData(form) });
|
||||
const nextPage = await res.text();
|
||||
if (!nextPage) return;
|
||||
|
||||
const nextPageDoc = new DOMParser().parseFromString(nextPage, "text/html");
|
||||
const articleList = nextPageDoc.querySelectorAll<HTMLElement>("#urls article");
|
||||
const nextPaginationElement = nextPageDoc.querySelector<HTMLElement>("#pagination");
|
||||
|
||||
document.querySelector("#pagination")?.remove();
|
||||
|
||||
const urlsElement = document.querySelector<HTMLElement>("#urls");
|
||||
if (!urlsElement) {
|
||||
throw new Error("URLs element not found");
|
||||
}
|
||||
|
||||
if (articleList.length > 0 && !onlyImages) {
|
||||
// do not add <hr> element when there are only images
|
||||
urlsElement.appendChild(document.createElement("hr"));
|
||||
}
|
||||
|
||||
urlsElement.append(...articleList);
|
||||
|
||||
if (nextPaginationElement) {
|
||||
const results = document.querySelector<HTMLElement>("#results");
|
||||
results?.appendChild(nextPaginationElement);
|
||||
callback();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading next page:", error);
|
||||
|
||||
const errorElement = Object.assign(document.createElement("div"), {
|
||||
textContent: settings.translations?.error_loading_next_page ?? "Error loading next page",
|
||||
className: "dialog-error"
|
||||
});
|
||||
errorElement.setAttribute("role", "alert");
|
||||
document.querySelector("#pagination")?.replaceChildren(errorElement);
|
||||
}
|
||||
};
|
||||
|
||||
const intersectionObserveOptions: IntersectionObserverInit = {
|
||||
rootMargin: "320px"
|
||||
};
|
||||
|
||||
const observer: IntersectionObserver = new IntersectionObserver(async (entries: IntersectionObserverEntry[]) => {
|
||||
const [paginationEntry] = entries;
|
||||
|
||||
if (paginationEntry?.isIntersecting) {
|
||||
observer.unobserve(paginationEntry.target);
|
||||
|
||||
await loadNextPage(() => {
|
||||
const nextObservedElement = document.querySelector<HTMLElement>(observedSelector);
|
||||
if (nextObservedElement) {
|
||||
observer.observe(nextObservedElement);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, intersectionObserveOptions);
|
||||
|
||||
const initialObservedElement: HTMLElement | null = document.querySelector<HTMLElement>(observedSelector);
|
||||
if (initialObservedElement) {
|
||||
observer.observe(initialObservedElement);
|
||||
}
|
||||
}
|
||||
|
||||
protected async post(): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import "ol/ol.css";
|
||||
import { Feature, Map as OlMap, View } from "ol";
|
||||
import { GeoJSON } from "ol/format";
|
||||
import { Point } from "ol/geom";
|
||||
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
|
||||
import { fromLonLat } from "ol/proj";
|
||||
import { OSM, Vector as VectorSource } from "ol/source";
|
||||
import { Circle, Fill, Stroke, Style } from "ol/style";
|
||||
import { Plugin } from "../Plugin.ts";
|
||||
|
||||
/**
|
||||
* MapView
|
||||
*/
|
||||
export default class MapView extends Plugin {
|
||||
private readonly map: HTMLElement;
|
||||
|
||||
public constructor(map: HTMLElement) {
|
||||
super("mapView");
|
||||
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
protected async run(): Promise<void> {
|
||||
const { leafletTarget: target, mapLon = "0", mapLat = "0", mapGeojson } = this.map.dataset;
|
||||
|
||||
const lon = Number.parseFloat(mapLon);
|
||||
const lat = Number.parseFloat(mapLat);
|
||||
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"
|
||||
})
|
||||
});
|
||||
|
||||
const geoLayer = new VectorLayer({
|
||||
source: geoSource,
|
||||
style: new Style({
|
||||
stroke: new Stroke({ color: "#3050ff", width: 2 }),
|
||||
fill: new Fill({ color: "#3050ff33" })
|
||||
})
|
||||
});
|
||||
|
||||
map.addLayer(geoLayer);
|
||||
|
||||
const geoSourceExtent = geoSource.getExtent();
|
||||
if (geoSourceExtent) {
|
||||
view.fit(geoSourceExtent, { padding: [20, 20, 20, 20] });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create GeoJSON layer:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async post(): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { load } from "./loader.ts";
|
||||
import { Endpoints, endpoint, listen, ready, settings } from "./toolkit.ts";
|
||||
|
||||
ready(() => {
|
||||
document.documentElement.classList.remove("no-js");
|
||||
document.documentElement.classList.add("js");
|
||||
|
||||
listen("click", ".close", function (this: HTMLElement) {
|
||||
(this.parentNode as HTMLElement)?.classList.add("invisible");
|
||||
});
|
||||
|
||||
listen("click", ".searxng_init_map", async function (this: HTMLElement, event: Event) {
|
||||
event.preventDefault();
|
||||
this.classList.remove("searxng_init_map");
|
||||
|
||||
load(() => import("./plugin/MapView.ts").then(({ default: Plugin }) => new Plugin(this)), {
|
||||
on: "endpoint",
|
||||
where: [Endpoints.results]
|
||||
});
|
||||
});
|
||||
|
||||
if (settings.plugins?.includes("infiniteScroll")) {
|
||||
load(() => import("./plugin/InfiniteScroll.ts").then(({ default: Plugin }) => new Plugin()), {
|
||||
on: "endpoint",
|
||||
where: [Endpoints.results]
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.plugins?.includes("calculator")) {
|
||||
load(() => import("./plugin/Calculator.ts").then(({ default: Plugin }) => new Plugin()), {
|
||||
on: "endpoint",
|
||||
where: [Endpoints.results]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ready(
|
||||
() => {
|
||||
void import("./main/keyboard.ts");
|
||||
void import("./main/search.ts");
|
||||
|
||||
if (settings.autocomplete) {
|
||||
void import("./main/autocomplete.ts");
|
||||
}
|
||||
},
|
||||
{ on: [endpoint === Endpoints.index] }
|
||||
);
|
||||
|
||||
ready(
|
||||
() => {
|
||||
void import("./main/keyboard.ts");
|
||||
void import("./main/results.ts");
|
||||
void import("./main/search.ts");
|
||||
|
||||
if (settings.autocomplete) {
|
||||
void import("./main/autocomplete.ts");
|
||||
}
|
||||
},
|
||||
{ on: [endpoint === Endpoints.results] }
|
||||
);
|
||||
|
||||
ready(
|
||||
() => {
|
||||
void import("./main/preferences.ts");
|
||||
},
|
||||
{ on: [endpoint === Endpoints.preferences] }
|
||||
);
|
||||
1
client/simple/src/js/searxng.head.js
Normal file
1
client/simple/src/js/searxng.head.js
Normal file
@@ -0,0 +1 @@
|
||||
import "./head/00_init.js";
|
||||
7
client/simple/src/js/searxng.js
Normal file
7
client/simple/src/js/searxng.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import "./main/00_toolkit.js";
|
||||
import "./main/infinite_scroll.js";
|
||||
import "./main/keyboard.js";
|
||||
import "./main/mapresult.js";
|
||||
import "./main/preferences.js";
|
||||
import "./main/results.js";
|
||||
import "./main/search.js";
|
||||
@@ -1,133 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type { KeyBindingLayout } from "./main/keyboard.ts";
|
||||
|
||||
// synced with searx/webapp.py get_client_settings
|
||||
type Settings = {
|
||||
plugins?: string[];
|
||||
autocomplete?: string;
|
||||
autocomplete_min?: number;
|
||||
doi_resolver?: string;
|
||||
favicon_resolver?: string;
|
||||
hotkeys?: KeyBindingLayout;
|
||||
method?: "GET" | "POST";
|
||||
query_in_title?: boolean;
|
||||
results_on_new_tab?: boolean;
|
||||
safesearch?: 0 | 1 | 2;
|
||||
search_on_category_select?: boolean;
|
||||
theme?: string;
|
||||
theme_static_path?: string;
|
||||
translations?: Record<string, string>;
|
||||
url_formatting?: "pretty" | "full" | "host";
|
||||
};
|
||||
|
||||
type HTTPOptions = {
|
||||
body?: BodyInit;
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
type ReadyOptions = {
|
||||
// all values must be truthy for the callback to be executed
|
||||
on?: (boolean | undefined)[];
|
||||
};
|
||||
|
||||
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 http = async (method: string, url: string | URL, options?: HTTPOptions): Promise<Response> => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), options?.timeout ?? 30_000);
|
||||
|
||||
const res = await fetch(url, {
|
||||
body: options?.body,
|
||||
method: method,
|
||||
signal: controller.signal
|
||||
}).finally(() => clearTimeout(timeoutId));
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const listen = <K extends keyof DocumentEventMap, E extends HTMLElement>(
|
||||
type: string | K,
|
||||
target: string | Document | E,
|
||||
listener: (this: E, event: DocumentEventMap[K]) => void | Promise<void>,
|
||||
options?: AddEventListenerOptions
|
||||
): void => {
|
||||
if (typeof target !== "string") {
|
||||
target.addEventListener(type, listener as EventListener, options);
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener(
|
||||
type,
|
||||
(event: Event) => {
|
||||
for (const node of event.composedPath()) {
|
||||
if (node instanceof HTMLElement && node.matches(target)) {
|
||||
try {
|
||||
listener.call(node as E, event as DocumentEventMap[K]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
export const ready = (callback: () => void, options?: ReadyOptions): void => {
|
||||
for (const condition of options?.on ?? []) {
|
||||
if (!condition) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
listen("DOMContentLoaded", document, callback, { once: true });
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
export const endpoint: EndpointsKeys = getEndpoint();
|
||||
export const settings: Settings = getSettings();
|
||||
@@ -1,34 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { getElement } from "./getElement.ts";
|
||||
|
||||
export const appendAnswerElement = (element: HTMLElement | string | number): void => {
|
||||
const results = getElement<HTMLDivElement>("results");
|
||||
|
||||
// ./searx/templates/elements/answers.html
|
||||
let answers = getElement<HTMLDivElement>("answers", { assert: false });
|
||||
if (!answers) {
|
||||
// what is this?
|
||||
const answersTitle = document.createElement("h4");
|
||||
answersTitle.setAttribute("class", "title");
|
||||
answersTitle.setAttribute("id", "answers-title");
|
||||
answersTitle.textContent = "Answers : ";
|
||||
|
||||
answers = document.createElement("div");
|
||||
answers.setAttribute("id", "answers");
|
||||
answers.setAttribute("role", "complementary");
|
||||
answers.setAttribute("aria-labelledby", "answers-title");
|
||||
answers.appendChild(answersTitle);
|
||||
}
|
||||
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
const span = document.createElement("span");
|
||||
span.innerHTML = element.toString();
|
||||
// biome-ignore lint/style/noParameterAssign: TODO
|
||||
element = span;
|
||||
}
|
||||
|
||||
answers.appendChild(element);
|
||||
|
||||
results.insertAdjacentElement("afterbegin", answers);
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
type AssertElement = <T>(element?: T | null) => asserts element is T;
|
||||
export const assertElement: AssertElement = <T>(element?: T | null): asserts element is T => {
|
||||
if (!element) {
|
||||
throw new Error("DOM element not found");
|
||||
}
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { assertElement } from "./assertElement.ts";
|
||||
|
||||
type Options = {
|
||||
assert?: boolean;
|
||||
};
|
||||
|
||||
export function getElement<T>(id: string, options?: { assert: true }): T;
|
||||
export function getElement<T>(id: string, options?: { assert: false }): T | null;
|
||||
export function getElement<T>(id: string, options: Options = {}): T | null {
|
||||
options.assert ??= true;
|
||||
|
||||
const element = document.getElementById(id) as T | null;
|
||||
|
||||
if (options.assert) {
|
||||
assertElement(element);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.dialog-modal {
|
||||
animation-name: dialogmodal;
|
||||
animation-duration: 0.13s;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
/*! Autocomplete.js v2.6.3 | license MIT | (c) 2017, Baptiste Donaux | http://autocomplete-js.com */
|
||||
|
||||
.autocomplete {
|
||||
position: absolute;
|
||||
width: @search-width;
|
||||
max-width: calc(100% - 2 * @search-padding-horizontal);
|
||||
max-height: 0;
|
||||
overflow-y: hidden;
|
||||
.ltr-text-align-left();
|
||||
@@ -66,6 +65,8 @@
|
||||
|
||||
@media screen and (max-width: @phone) {
|
||||
.autocomplete {
|
||||
width: 100%;
|
||||
|
||||
> ul > li {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
@import "../../../generated/pygments.less";
|
||||
@import "../../generated/pygments.less";
|
||||
|
||||
.codelines {
|
||||
margin: @results-margin 0 0 0;
|
||||
@@ -20,9 +18,12 @@
|
||||
cursor: default;
|
||||
|
||||
&::selection {
|
||||
background: transparent;
|
||||
background: transparent; /* WebKit/Blink Browsers */
|
||||
}
|
||||
|
||||
&::-moz-selection {
|
||||
background: transparent; /* Gecko Browsers */
|
||||
}
|
||||
margin-right: 8px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -30,12 +31,6 @@
|
||||
span.linenos {
|
||||
color: var(--color-line-number);
|
||||
}
|
||||
|
||||
.err {
|
||||
// The code view in the result list should not act as a code-checker.
|
||||
border: none;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
* SearXNG, A privacy-respecting, hackable metasearch engine
|
||||
*
|
||||
@@ -289,9 +287,8 @@
|
||||
@results-image-row-height: 12rem;
|
||||
@results-image-row-height-phone: 10rem;
|
||||
@search-width: 44rem;
|
||||
// height of #search, see detail.less
|
||||
// heigh of #search, see detail.less
|
||||
@search-height: 13rem;
|
||||
@search-padding-horizontal: 0.5rem;
|
||||
|
||||
/// Device Size
|
||||
/// @desktop > @tablet
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#main_results #results.image-detail-open.only_template_images {
|
||||
width: min(98%, 59.25rem) !important;
|
||||
}
|
||||
@@ -246,7 +244,7 @@ article.result-images .detail {
|
||||
|
||||
a.result-images-source img {
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 2rem);
|
||||
max-height: calc(100vh - 20rem);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
iframe[src^="https://w.soundcloud.com"] {
|
||||
iframe[src^="https://w.soundcloud.com"]
|
||||
{
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
iframe[src^="https://www.deezer.com"] {
|
||||
iframe[src^="https://www.deezer.com"]
|
||||
{
|
||||
// The real size is 92px, but 94px are needed to avoid an inner scrollbar of
|
||||
// the embedded HTML.
|
||||
height: 94px;
|
||||
}
|
||||
|
||||
iframe[src^="https://www.mixcloud.com"] {
|
||||
iframe[src^="https://www.mixcloud.com"]
|
||||
{
|
||||
// the embedded player from mixcloud has some quirks: initial there is an
|
||||
// issue with an image URL that is blocked since it is an a Cross-Origin
|
||||
// request. The alternative text (<img alt='Mixcloud Logo'> then cause an
|
||||
@@ -20,16 +21,19 @@ iframe[src^="https://www.mixcloud.com"] {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
iframe[src^="https://bandcamp.com/EmbeddedPlayer"] {
|
||||
iframe[src^="https://bandcamp.com/EmbeddedPlayer"]
|
||||
{
|
||||
// show playlist
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
iframe[src^="https://bandcamp.com/EmbeddedPlayer/track"] {
|
||||
iframe[src^="https://bandcamp.com/EmbeddedPlayer/track"]
|
||||
{
|
||||
// hide playlist
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
iframe[src^="https://genius.com/songs"] {
|
||||
iframe[src^="https://genius.com/songs"]
|
||||
{
|
||||
height: 65px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#main_index {
|
||||
margin-top: 26vh;
|
||||
}
|
||||
@@ -8,7 +6,7 @@
|
||||
text-align: center;
|
||||
|
||||
.title {
|
||||
background: url("./img/searxng.png") no-repeat;
|
||||
background: url("../img/searxng.png") no-repeat;
|
||||
min-height: 4rem;
|
||||
margin: 4rem auto;
|
||||
background-position: center;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.info-page {
|
||||
code {
|
||||
font-family: monospace;
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -19,6 +22,7 @@
|
||||
|
||||
// disable user selection
|
||||
.disable-user-select () {
|
||||
-webkit-touch-callout: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
37
client/simple/src/less/new_issue.less
Normal file
37
client/simple/src/less/new_issue.less
Normal file
@@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
.stats_endpoint {
|
||||
.github-issue-button {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.issue-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="checked"] {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 1rem 1rem 1rem 0;
|
||||
}
|
||||
|
||||
.step_content {
|
||||
margin: 1rem 1rem 1rem 2rem;
|
||||
}
|
||||
|
||||
.step1,
|
||||
.step2 {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.step1_delay {
|
||||
transition: visibility 0s linear 4s;
|
||||
}
|
||||
|
||||
#step1:checked ~ .step1,
|
||||
#step2:checked ~ .step2 {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
Layout of the Files result class
|
||||
*/
|
||||
|
||||
#main_results .result-file {
|
||||
border: 1px solid var(--color-result-border);
|
||||
margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important;
|
||||
.rounded-corners;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
|
||||
audio {
|
||||
width: 100%;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
Layout of the KeyValue result class
|
||||
*/
|
||||
|
||||
#main_results .result-keyvalue {
|
||||
caption {
|
||||
padding: 0.8rem 0.5rem;
|
||||
@@ -17,7 +14,7 @@
|
||||
}
|
||||
|
||||
table {
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
background-color: var(--color-result-keyvalue-table);
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
Layout of the Paper result class
|
||||
*/
|
||||
|
||||
.result-paper {
|
||||
.attributes {
|
||||
display: table;
|
||||
border-spacing: 0.125rem;
|
||||
|
||||
div {
|
||||
display: table-row;
|
||||
|
||||
span {
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.25rem;
|
||||
display: table-cell;
|
||||
|
||||
time {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
span:first-child {
|
||||
color: var(--color-base-font);
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
color: var(--color-result-publishdate-font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.comments {
|
||||
font-size: 0.9rem;
|
||||
margin: 0.25rem 0 0 0;
|
||||
padding: 0;
|
||||
overflow-wrap: break-word;
|
||||
line-height: 1.24;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @phone) {
|
||||
.result-paper {
|
||||
.attributes {
|
||||
display: block;
|
||||
|
||||
div {
|
||||
display: block;
|
||||
|
||||
span {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
span:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
.ltr-margin-left(0.5rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
@import (inline) "../../node_modules/normalize.css/normalize.css";
|
||||
@import "definitions.less";
|
||||
@import "mixins.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;
|
||||
}
|
||||
|
||||
// Reset padding and margin
|
||||
html,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
/*
|
||||
* SearXNG, A privacy-respecting, hackable metasearch engine
|
||||
*/
|
||||
|
||||
#search {
|
||||
padding: 0;
|
||||
@@ -129,7 +131,7 @@ button.category_button {
|
||||
}
|
||||
|
||||
#search_view {
|
||||
padding: 0.5rem @search-padding-horizontal 0 @search-padding-horizontal;
|
||||
padding: 0.5rem 0.3rem 0 0.5rem;
|
||||
grid-area: search;
|
||||
|
||||
body.results_endpoint & {
|
||||
@@ -139,8 +141,7 @@ button.category_button {
|
||||
|
||||
.search_box {
|
||||
border-radius: 0.8rem;
|
||||
width: 100%;
|
||||
max-width: @search-width;
|
||||
width: @search-width;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
white-space: nowrap;
|
||||
@@ -178,6 +179,7 @@ html.no-js #clear_search.hide_if_nojs {
|
||||
#send_search {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0.8rem;
|
||||
background: none repeat scroll 0 0 var(--color-search-background);
|
||||
border: none;
|
||||
outline: none;
|
||||
@@ -193,9 +195,13 @@ 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);
|
||||
padding: 0.8rem;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
@@ -264,6 +270,7 @@ html.no-js #clear_search.hide_if_nojs {
|
||||
width: 100%;
|
||||
.ltr-text-align-left();
|
||||
overflow: scroll hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,7 +291,8 @@ html.no-js #clear_search.hide_if_nojs {
|
||||
}
|
||||
|
||||
.search_box {
|
||||
width: 100%;
|
||||
width: 98%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#q {
|
||||
@@ -366,6 +374,11 @@ html.no-js #clear_search.hide_if_nojs {
|
||||
|
||||
#categories {
|
||||
.disable-user-select;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#categories_container {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
--center-page-width overrides the less variable @results-width when the results are centered
|
||||
see the CSS rules for #results in style.less ( grid-template-columns and gap).
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.ltr-left(@offset) {
|
||||
left: @offset;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.ltr-left(@offset) {
|
||||
right: @offset;
|
||||
}
|
||||
@@ -131,7 +129,13 @@
|
||||
}
|
||||
|
||||
// select HTML element
|
||||
@supports ((background-position-x: 100%) and ((appearance: none))) {
|
||||
@supports (
|
||||
(background-position-x: 100%) and
|
||||
(
|
||||
(appearance: none) or (-webkit-appearance: none) or
|
||||
(-moz-appearance: none)
|
||||
)
|
||||
) {
|
||||
select {
|
||||
border-width: 0 0 0 2rem;
|
||||
background-position-x: -2rem;
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
/*
|
||||
* SearXNG, A privacy-respecting, hackable metasearch engine
|
||||
*
|
||||
* To convert "style.less" to "style.css" run: $make styles
|
||||
*/
|
||||
|
||||
// stylelint-disable no-descending-specificity
|
||||
|
||||
@import (inline) "../../node_modules/normalize.css/normalize.css";
|
||||
@import "definitions.less";
|
||||
@import "mixins.less";
|
||||
@import "code.less";
|
||||
@import "toolkit.less";
|
||||
@import "autocomplete.less";
|
||||
@import "detail.less";
|
||||
@import "animations.less";
|
||||
@import "embedded.less";
|
||||
@import "info.less";
|
||||
@import "new_issue.less";
|
||||
@import "stats.less";
|
||||
@import "result_templates.less";
|
||||
@import "weather.less";
|
||||
|
||||
// for index.html template
|
||||
@import "index.less";
|
||||
@@ -161,22 +167,12 @@ article[data-vim-selected].category-videos,
|
||||
article[data-vim-selected].category-news,
|
||||
article[data-vim-selected].category-map,
|
||||
article[data-vim-selected].category-music,
|
||||
article[data-vim-selected].category-files,
|
||||
article[data-vim-selected].category-social {
|
||||
border: 1px solid var(--color-result-vim-arrow);
|
||||
.rounded-corners;
|
||||
}
|
||||
|
||||
.image-label-bottom-right() {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--color-image-resolution-background);
|
||||
padding: 0.3rem 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-image-resolution-font);
|
||||
border-top-left-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin: @results-margin 0;
|
||||
padding: @result-padding;
|
||||
@@ -186,7 +182,7 @@ article[data-vim-selected].category-social {
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
margin: 0.4rem 0 0.4rem 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -220,7 +216,7 @@ article[data-vim-selected].category-social {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: 54em;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
line-height: 1.24;
|
||||
|
||||
.highlight {
|
||||
@@ -303,22 +299,12 @@ article[data-vim-selected].category-social {
|
||||
color: var(--color-result-description-highlight-font);
|
||||
}
|
||||
|
||||
a.thumbnail_link {
|
||||
position: relative;
|
||||
margin-top: 0.6rem;
|
||||
.ltr-margin-right(1rem);
|
||||
img.thumbnail {
|
||||
.ltr-float-left();
|
||||
|
||||
img.thumbnail {
|
||||
width: 7rem;
|
||||
height: unset; // remove height value that was needed for lazy loading
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumbnail_length {
|
||||
.image-label-bottom-right();
|
||||
right: 6px;
|
||||
}
|
||||
padding-top: 0.6rem;
|
||||
.ltr-padding-right(1rem);
|
||||
width: 7rem;
|
||||
height: unset; // remove height value that was needed for lazy loading
|
||||
}
|
||||
|
||||
.break {
|
||||
@@ -326,11 +312,11 @@ article[data-vim-selected].category-social {
|
||||
}
|
||||
}
|
||||
|
||||
.result-paper,
|
||||
.result-packages {
|
||||
.attributes {
|
||||
display: table;
|
||||
border-spacing: 0.125rem;
|
||||
margin-top: 0.3rem;
|
||||
|
||||
div {
|
||||
display: table-row;
|
||||
@@ -364,12 +350,18 @@ article[data-vim-selected].category-social {
|
||||
font-size: 0.9rem;
|
||||
margin: 0.25rem 0 0 0;
|
||||
padding: 0;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
line-height: 1.24;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.result-packages {
|
||||
.attributes {
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.template_group_images {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -384,6 +376,7 @@ article[data-vim-selected].category-social {
|
||||
.category-news,
|
||||
.category-map,
|
||||
.category-music,
|
||||
.category-files,
|
||||
.category-social {
|
||||
border: 1px solid var(--color-result-border);
|
||||
margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important;
|
||||
@@ -408,19 +401,23 @@ article[data-vim-selected].category-social {
|
||||
}
|
||||
|
||||
.result-videos {
|
||||
a.thumbnail_link img.thumbnail {
|
||||
img.thumbnail {
|
||||
.ltr-float-left();
|
||||
padding-top: 0.6rem;
|
||||
.ltr-padding-right(1rem);
|
||||
width: 20rem;
|
||||
height: unset; // remove height value that was needed for lazy loading
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: hidden;
|
||||
}
|
||||
.result-videos .content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.embedded-video iframe {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
.result-videos .embedded-video iframe {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
|
||||
@supports not (aspect-ratio: 1 / 1) {
|
||||
@@ -485,7 +482,14 @@ article[data-vim-selected].category-social {
|
||||
}
|
||||
|
||||
.image_resolution {
|
||||
.image-label-bottom-right();
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--color-image-resolution-background);
|
||||
padding: 0.3rem 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-image-resolution-font);
|
||||
border-top-left-radius: 0.3rem;
|
||||
}
|
||||
|
||||
span.title,
|
||||
@@ -569,16 +573,13 @@ article[data-vim-selected].category-social {
|
||||
|
||||
#suggestions {
|
||||
.wrapper {
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
list-style-position: inside;
|
||||
|
||||
li::marker {
|
||||
color: var(--color-result-link-font);
|
||||
}
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
form {
|
||||
display: inline-block;
|
||||
flex: 1 1 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -586,8 +587,8 @@ article[data-vim-selected].category-social {
|
||||
#suggestions,
|
||||
#infoboxes {
|
||||
input {
|
||||
padding: 3px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin: 3px;
|
||||
font-size: 0.9em;
|
||||
display: inline-block;
|
||||
background: transparent;
|
||||
@@ -689,7 +690,7 @@ summary.title {
|
||||
|
||||
#sidebar {
|
||||
grid-area: sidebar;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
color: var(--color-sidebar-font);
|
||||
|
||||
.infobox {
|
||||
@@ -1030,6 +1031,10 @@ summary.title {
|
||||
/ 100%;
|
||||
gap: 0;
|
||||
|
||||
#sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#urls {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
@@ -1116,6 +1121,7 @@ summary.title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.result-paper,
|
||||
.result-packages {
|
||||
.attributes {
|
||||
display: block;
|
||||
@@ -1161,7 +1167,3 @@ pre code {
|
||||
|
||||
// import layouts of the Result types
|
||||
@import "result_types/keyvalue.less";
|
||||
@import "result_types/code.less";
|
||||
@import "result_types/paper.less";
|
||||
@import "result_types/file.less";
|
||||
@import "result_types/image.less";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
// other solution : http://stackoverflow.com/questions/1577598/how-to-hide-parts-of-html-when-javascript-is-disabled/13857783#13857783
|
||||
|
||||
// stylelint-disable no-descending-specificity
|
||||
@@ -158,7 +156,9 @@ div.selectable_url {
|
||||
|
||||
td {
|
||||
padding: 0 1em 0 0;
|
||||
padding-top: 0;
|
||||
.ltr-padding-right(1rem);
|
||||
padding-bottom: 0;
|
||||
.ltr-padding-left(0);
|
||||
}
|
||||
|
||||
@@ -193,15 +193,6 @@ div.selectable_url {
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.dialog-warning-block {
|
||||
.dialog();
|
||||
|
||||
display: block;
|
||||
color: var(--color-warning);
|
||||
background: var(--color-warning-background);
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.dialog-modal {
|
||||
.dialog();
|
||||
|
||||
@@ -316,7 +307,7 @@ html body .tabs > input:checked {
|
||||
}
|
||||
|
||||
~ label {
|
||||
position: inherit;
|
||||
position: inherited;
|
||||
background: inherit;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-weight: normal;
|
||||
@@ -356,9 +347,17 @@ select {
|
||||
}
|
||||
}
|
||||
|
||||
@supports ((background-position-x: 100%) and ((appearance: none))) {
|
||||
@supports (
|
||||
(background-position-x: 100%) and
|
||||
(
|
||||
(appearance: none) or (-webkit-appearance: none) or
|
||||
(-moz-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)
|
||||
@@ -401,6 +400,8 @@ select {
|
||||
|
||||
/* -- checkbox-onoff -- */
|
||||
input.checkbox-onoff[type="checkbox"] {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@@ -474,6 +475,8 @@ 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;
|
||||
@@ -546,16 +549,33 @@ 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;
|
||||
}
|
||||
|
||||
@keyframes load8 {
|
||||
@-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);
|
||||
}
|
||||
}
|
||||
@@ -586,6 +606,9 @@ 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;
|
||||
@@ -610,7 +633,7 @@ td:hover .engine-tooltip,
|
||||
.stacked-bar-chart-base();
|
||||
|
||||
background: var(--color-base-font);
|
||||
border: 1px solid rgb(var(--color-base-font-rgb), 0.9);
|
||||
border: 1px solid rgba(var(--color-base-font-rgb), 0.9);
|
||||
padding: 0.3rem 0;
|
||||
}
|
||||
|
||||
@@ -618,7 +641,7 @@ td:hover .engine-tooltip,
|
||||
.stacked-bar-chart-base();
|
||||
|
||||
background: transparent;
|
||||
border: 1px solid rgb(var(--color-base-font-rgb), 0.3);
|
||||
border: 1px solid rgba(var(--color-base-font-rgb), 0.3);
|
||||
padding: 0.3rem 0;
|
||||
}
|
||||
|
||||
@@ -626,7 +649,7 @@ td:hover .engine-tooltip,
|
||||
.stacked-bar-chart-base();
|
||||
|
||||
background: transparent;
|
||||
border-bottom: 1px dotted rgb(var(--color-base-font-rgb), 0.5);
|
||||
border-bottom: 1px dotted rgba(var(--color-base-font-rgb), 0.5);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -634,7 +657,7 @@ td:hover .engine-tooltip,
|
||||
.stacked-bar-chart-base();
|
||||
|
||||
background: transparent;
|
||||
border-left: 1px solid rgb(var(--color-base-font-rgb), 0.9);
|
||||
border-left: 1px solid rgba(var(--color-base-font-rgb), 0.9);
|
||||
padding: 0.4rem 0;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.loader,
|
||||
.loader::after {
|
||||
border-radius: 50%;
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#answers .weather {
|
||||
summary {
|
||||
display: block;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
div.summary {
|
||||
margin: 0;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: var(--color-header-background);
|
||||
.rounded-corners-tiny;
|
||||
}
|
||||
|
||||
table {
|
||||
font-size: 0.9rem;
|
||||
table-layout: fixed;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0.1em 0;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
img.symbol {
|
||||
width: 5rem;
|
||||
margin: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.title {
|
||||
// background-color: var(--color-result-keyvalue-even);
|
||||
}
|
||||
|
||||
.measured {
|
||||
// background-color: var(--color-result-keyvalue-odd);
|
||||
}
|
||||
}
|
||||
2
client/simple/static/.gitattributes
vendored
Normal file
2
client/simple/static/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
leaflet.css -diff
|
||||
leaflet.js -diff
|
||||
84
client/simple/theme_icons.js
Normal file
84
client/simple/theme_icons.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Generate icons.html for the jinja templates of the simple theme.
|
||||
*/
|
||||
|
||||
import { argv } from "node:process";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { jinja_svg_sets } from "./tools/jinja_svg_catalog.js";
|
||||
|
||||
const HERE = dirname(argv[1]) + "/";
|
||||
const dest = resolve(HERE, "../../searx/templates/simple/icons.html");
|
||||
|
||||
/** @type import("./tools/jinja_svg_catalog.js").JinjaMacro[] */
|
||||
const searxng_jinja_macros = [
|
||||
{ name: "icon", class: "sxng-icon-set" },
|
||||
{ name: "icon_small", class: "sxng-icon-set-small" },
|
||||
{ name: "icon_big", class: "sxng-icon-set-big" },
|
||||
];
|
||||
|
||||
|
||||
const sxng_icon_opts ={
|
||||
multipass: true,
|
||||
plugins: [
|
||||
{ name: "removeTitle" },
|
||||
{ name: "removeXMLNS" },
|
||||
{ name: "addAttributesToSVGElement",
|
||||
params: {
|
||||
attributes: [
|
||||
{
|
||||
"aria-hidden": "true",
|
||||
}]}}]
|
||||
};
|
||||
|
||||
|
||||
/** @type import("./tools/jinja_svg_catalog.js").IconSet */
|
||||
const simple_icons = [
|
||||
{
|
||||
base: resolve(HERE, "node_modules/ionicons/dist/svg"),
|
||||
set: {
|
||||
"alert": "alert-outline.svg",
|
||||
"appstore": "apps-outline.svg",
|
||||
"book": "book-outline.svg",
|
||||
"close": "close-outline.svg",
|
||||
"download": "download-outline.svg",
|
||||
"ellipsis-vertical": "ellipsis-vertical-outline.svg",
|
||||
"file-tray-full": "file-tray-full-outline.svg",
|
||||
"film": "film-outline.svg",
|
||||
"globe": "globe-outline.svg",
|
||||
"heart": "heart-outline.svg",
|
||||
"image": "image-outline.svg",
|
||||
"layers": "layers-outline.svg",
|
||||
"leecher": "arrow-down.svg",
|
||||
"location": "location-outline.svg",
|
||||
"magnet": "magnet-outline.svg",
|
||||
"musical-notes": "musical-notes-outline.svg",
|
||||
"navigate-down": "chevron-down-outline.svg",
|
||||
"navigate-left": "chevron-back-outline.svg",
|
||||
"navigate-right": "chevron-forward-outline.svg",
|
||||
"navigate-up": "chevron-up-outline.svg",
|
||||
"people": "people-outline.svg",
|
||||
"play": "play-outline.svg",
|
||||
"radio": "radio-outline.svg",
|
||||
"save": "save-outline.svg",
|
||||
"school": "school-outline.svg",
|
||||
"search": "search-outline.svg",
|
||||
"seeder": "swap-vertical.svg",
|
||||
"settings": "settings-outline.svg",
|
||||
"tv": "tv-outline.svg",
|
||||
},
|
||||
svgo_opts: sxng_icon_opts,
|
||||
},
|
||||
// some of the ionicons are not suitable for a dark theme, we fixed the svg
|
||||
// manually in src/svg/ionicons
|
||||
// - https://github.com/searxng/searxng/pull/4284#issuecomment-2680550342
|
||||
{
|
||||
base: resolve(HERE, "src/svg/ionicons"),
|
||||
set: {
|
||||
"information-circle": "information-circle-outline.svg",
|
||||
"newspaper": "newspaper-outline.svg",
|
||||
},
|
||||
svgo_opts: sxng_icon_opts,
|
||||
}
|
||||
];
|
||||
|
||||
jinja_svg_sets(dest, searxng_jinja_macros, simple_icons);
|
||||
@@ -1,84 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* Generate icons.html for the jinja templates of the simple theme.
|
||||
*/
|
||||
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { argv } from "node:process";
|
||||
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: string = resolve(HERE, "../../searx/templates/simple/icons.html");
|
||||
|
||||
const searxng_jinja_macros: JinjaMacro[] = [
|
||||
{ name: "icon", class: "sxng-icon-set" },
|
||||
{ name: "icon_small", class: "sxng-icon-set-small" },
|
||||
{ name: "icon_big", class: "sxng-icon-set-big" }
|
||||
];
|
||||
|
||||
const sxng_icon_opts: SvgoConfig = {
|
||||
multipass: true,
|
||||
plugins: [
|
||||
"removeTitle",
|
||||
"removeXMLNS",
|
||||
{
|
||||
name: "addAttributesToSVGElement",
|
||||
params: {
|
||||
attributes: [{ "aria-hidden": "true" }]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const simple_icons: IconSet[] = [
|
||||
{
|
||||
base: resolve(HERE, "node_modules/ionicons/dist/svg"),
|
||||
set: {
|
||||
alert: "alert-outline.svg",
|
||||
appstore: "apps-outline.svg",
|
||||
book: "book-outline.svg",
|
||||
close: "close-outline.svg",
|
||||
download: "download-outline.svg",
|
||||
"ellipsis-vertical": "ellipsis-vertical-outline.svg",
|
||||
"file-tray-full": "file-tray-full-outline.svg",
|
||||
film: "film-outline.svg",
|
||||
globe: "globe-outline.svg",
|
||||
heart: "heart-outline.svg",
|
||||
image: "image-outline.svg",
|
||||
layers: "layers-outline.svg",
|
||||
leecher: "arrow-down.svg",
|
||||
location: "location-outline.svg",
|
||||
magnet: "magnet-outline.svg",
|
||||
"musical-notes": "musical-notes-outline.svg",
|
||||
"navigate-down": "chevron-down-outline.svg",
|
||||
"navigate-left": "chevron-back-outline.svg",
|
||||
"navigate-right": "chevron-forward-outline.svg",
|
||||
"navigate-up": "chevron-up-outline.svg",
|
||||
people: "people-outline.svg",
|
||||
play: "play-outline.svg",
|
||||
radio: "radio-outline.svg",
|
||||
save: "save-outline.svg",
|
||||
school: "school-outline.svg",
|
||||
search: "search-outline.svg",
|
||||
seeder: "swap-vertical.svg",
|
||||
settings: "settings-outline.svg",
|
||||
tv: "tv-outline.svg"
|
||||
},
|
||||
svgo_opts: sxng_icon_opts
|
||||
},
|
||||
// some of the ionicons are not suitable for a dark theme, we fixed the svg
|
||||
// manually in src/svg/ionicons
|
||||
// - https://github.com/searxng/searxng/pull/4284#issuecomment-2680550342
|
||||
{
|
||||
base: resolve(HERE, "src/svg/ionicons"),
|
||||
set: {
|
||||
"information-circle": "information-circle-outline.svg",
|
||||
newspaper: "newspaper-outline.svg"
|
||||
},
|
||||
svgo_opts: sxng_icon_opts
|
||||
}
|
||||
];
|
||||
|
||||
jinja_svg_sets(dest, searxng_jinja_macros, simple_icons);
|
||||
78
client/simple/tools/img.js
Normal file
78
client/simple/tools/img.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import sharp from "sharp";
|
||||
import { optimize as svgo } from "svgo";
|
||||
|
||||
/**
|
||||
* @typedef {object} Src2Dest - Mapping of src to dest
|
||||
* @property {string} src - Name of the source file.
|
||||
* @property {string} dest - Name of the destination file.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Convert a list of SVG files to PNG.
|
||||
*
|
||||
* @param {Src2Dest[]} items - Array of SVG files (src: SVG, dest:PNG) to convert.
|
||||
*/
|
||||
|
||||
async function svg2png (items) {
|
||||
items.forEach(
|
||||
async (item) => {
|
||||
try {
|
||||
fs.mkdir(path.dirname(item.dest), { recursive: true }, (err) => {
|
||||
if (err)
|
||||
throw err;
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Optimize SVG images for WEB.
|
||||
*
|
||||
* @param {import('svgo').Config} svgo_opts - Options passed to svgo.
|
||||
* @param {Src2Dest[]} items - Array of SVG files (src:SVG, dest:SVG) to optimize.
|
||||
*/
|
||||
|
||||
async function svg2svg(svgo_opts, items) {
|
||||
items.forEach(
|
||||
async (item) => {
|
||||
try {
|
||||
fs.mkdir(path.dirname(item.dest), { recursive: true }, (err) => {
|
||||
if (err)
|
||||
throw err;
|
||||
});
|
||||
|
||||
const raw = fs.readFileSync(item.src, "utf8");
|
||||
const opt = svgo(raw, svgo_opts);
|
||||
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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export { svg2png, svg2svg };
|
||||
@@ -1,69 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import sharp from "sharp";
|
||||
import type { Config } from "svgo";
|
||||
import { optimize as svgo } from "svgo";
|
||||
|
||||
// Mapping of src to dest
|
||||
export type Src2Dest = {
|
||||
// Name of the source file.
|
||||
src: string;
|
||||
// Name of the destination file.
|
||||
dest: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a list of SVG files to PNG.
|
||||
*
|
||||
* @param items - Array of SVG files (src: SVG, dest:PNG) to convert.
|
||||
* @param width - (optional) width of the PNG pictures
|
||||
* @param height - (optional) height of the PNG pictures.
|
||||
*/
|
||||
export const svg2png = (items: Src2Dest[], width?: number, height?: number): void => {
|
||||
for (const item of items) {
|
||||
fs.mkdirSync(path.dirname(item.dest), { recursive: true });
|
||||
|
||||
sharp(item.src)
|
||||
.png({
|
||||
force: true,
|
||||
compressionLevel: 9,
|
||||
palette: true
|
||||
})
|
||||
.resize(width, height, {
|
||||
fit: "contain"
|
||||
})
|
||||
.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;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Optimize SVG images for WEB.
|
||||
*
|
||||
* @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): void => {
|
||||
for (const item of items) {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(item.dest), { recursive: true });
|
||||
|
||||
const raw = fs.readFileSync(item.src, "utf8");
|
||||
const opt = svgo(raw, svgo_opts);
|
||||
|
||||
fs.writeFileSync(item.dest, opt.data);
|
||||
console.log(`[svg2svg] optimized: ${item.dest} -- src: ${item.src}`);
|
||||
} catch (error) {
|
||||
console.error(`ERROR: optimize src: ${item.src} -- ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,4 @@
|
||||
{{--
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
This is a EDGE https://edgejs.dev/ template to generate a HTML Jinja template
|
||||
for the backend. Example output of this EDGE template:
|
||||
- https://github.com/searxng/searxng/blob/master/searx/templates/simple/icons.html
|
||||
|
||||
130
client/simple/tools/jinja_svg_catalog.js
Normal file
130
client/simple/tools/jinja_svg_catalog.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import fs from "fs";
|
||||
import { resolve, dirname } from "path";
|
||||
import { Edge } from 'edge.js';
|
||||
import { optimize as svgo } from "svgo";
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const __jinja_class_placeholder__ = "__jinja_class_placeholder__";
|
||||
|
||||
// -- types
|
||||
|
||||
/**
|
||||
* @typedef {object} IconSet - A set of icons
|
||||
* @property {object[]} set - Array of SVG icons, where property name is the
|
||||
* name of the icon and value is the src of the SVG (relative to base).
|
||||
* @property {string} base - Folder in which the SVG src files are located.
|
||||
* @property {import("svgo").Config} svgo_opts - svgo options for this set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} IconSVG - Mapping of icon name to SVG source file.
|
||||
* @property {string} name - Name of the icon isource file.
|
||||
* @property {string} src - Name of the destination file.
|
||||
* @property {import("svgo").Config} svgo_opts - Options passed to svgo.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} JinjaMacro - Arguments to create a jinja macro
|
||||
* @property {string} name - Name of the jinja macro.
|
||||
* @property {string} class - SVG's class name (value of XML class attribute)
|
||||
*/
|
||||
|
||||
|
||||
// -- functions
|
||||
|
||||
/**
|
||||
* Generate a jinja template with a catalog of SVG icons that can be
|
||||
* used in in other HTML jinja templates.
|
||||
*
|
||||
* @param {string} dest - filename of the generate jinja template.
|
||||
* @param {JinjaMacro} macros - Jinja macros to create.
|
||||
* @param {IconSVG[]} items - Array of SVG items.
|
||||
*/
|
||||
|
||||
function jinja_svg_catalog(dest, macros, items) {
|
||||
|
||||
const svg_catalog = {};
|
||||
const edge_template = resolve(__dirname, "jinja_svg_catalog.html.edge");
|
||||
|
||||
items.forEach(
|
||||
(item) => {
|
||||
|
||||
/** @type {import("svgo").Config} */
|
||||
const svgo_opts = JSON.parse(JSON.stringify(item.svgo_opts));
|
||||
svgo_opts.plugins.push({
|
||||
name: "addAttributesToSVGElement",
|
||||
params: {
|
||||
attributes: [{ "class": __jinja_class_placeholder__, }]
|
||||
}}
|
||||
);
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(item.src, "utf8");
|
||||
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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
fs.mkdir(dirname(dest), { recursive: true }, (err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
|
||||
const ctx = {
|
||||
svg_catalog: svg_catalog,
|
||||
macros: macros,
|
||||
edge_template: edge_template,
|
||||
__jinja_class_placeholder__: __jinja_class_placeholder__,
|
||||
// see https://github.com/edge-js/edge/issues/162
|
||||
open_curly_brace : "{{",
|
||||
close_curly_brace : "}}"
|
||||
};
|
||||
|
||||
const jinjatmpl = Edge.create().renderRawSync(
|
||||
fs.readFileSync(edge_template, "utf-8"),
|
||||
ctx
|
||||
);
|
||||
|
||||
fs.writeFileSync(dest, jinjatmpl);
|
||||
console.log(`[jinja_svg_catalog] created: ${dest}`);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calls jinja_svg_catalog for a collection of icon sets where each set has its
|
||||
* own parameters.
|
||||
*
|
||||
* @param {string} dest - filename of the generate jinja template.
|
||||
* @param {JinjaMacro} macros - Jinja macros to create.
|
||||
* @param {IconSet[]} sets - Array of SVG sets.
|
||||
*/
|
||||
function jinja_svg_sets(dest, macros, sets) {
|
||||
/** @type IconSVG[] */
|
||||
const items = [];
|
||||
const all = [];
|
||||
for (const obj of sets) {
|
||||
|
||||
for (const [name, file] of Object.entries(obj.set)) {
|
||||
if (all.includes(name)) {
|
||||
throw new Error(`ERROR: ${name} has already been defined`);
|
||||
}
|
||||
items.push({
|
||||
name: name,
|
||||
src: resolve(obj.base, file),
|
||||
svgo_opts: obj.svgo_opts,
|
||||
});
|
||||
}
|
||||
jinja_svg_catalog(dest, macros, items);
|
||||
}
|
||||
}
|
||||
|
||||
// -- exports
|
||||
|
||||
export {
|
||||
jinja_svg_sets,
|
||||
jinja_svg_catalog,
|
||||
};
|
||||
@@ -1,118 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import fs from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { Edge } from "edge.js";
|
||||
import { type Config as SvgoConfig, optimize as svgo } from "svgo";
|
||||
|
||||
const __jinja_class_placeholder__ = "__jinja_class_placeholder__";
|
||||
|
||||
// A set of icons
|
||||
export type IconSet = {
|
||||
// Object of SVG icons, where property name is the name of the icon and value is the src of the SVG (relative to base)
|
||||
set: Record<string, string>;
|
||||
// Folder in which the SVG src files are located
|
||||
base: string;
|
||||
// svgo options for this set
|
||||
svgo_opts: SvgoConfig;
|
||||
};
|
||||
|
||||
// Mapping of icon name to SVG source file
|
||||
type IconSVG = {
|
||||
// Name of the icon isource file
|
||||
name: string;
|
||||
// Name of the destination file
|
||||
src: string;
|
||||
// Options passed to svgo
|
||||
svgo_opts: SvgoConfig;
|
||||
};
|
||||
|
||||
// Arguments to create a jinja macro
|
||||
export type JinjaMacro = {
|
||||
// Name of the jinja macro
|
||||
name: string;
|
||||
// SVG's class name (value of XML class attribute)
|
||||
class: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a jinja template with a catalog of SVG icons that can be
|
||||
* used in other HTML jinja templates.
|
||||
*
|
||||
* @param dest - filename of the generate jinja template.
|
||||
* @param macros - Jinja macros to create.
|
||||
* @param items - Array of SVG items.
|
||||
*/
|
||||
export const jinja_svg_catalog = (dest: string, macros: JinjaMacro[], items: IconSVG[]): void => {
|
||||
const svg_catalog: Record<string, string> = {};
|
||||
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
|
||||
const svgo_opts: SvgoConfig = JSON.parse(JSON.stringify(item.svgo_opts));
|
||||
|
||||
svgo_opts.plugins?.push({
|
||||
name: "addClassesToSVGElement",
|
||||
params: {
|
||||
classNames: [__jinja_class_placeholder__]
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(item.src, "utf8");
|
||||
const opt = svgo(raw, svgo_opts);
|
||||
|
||||
svg_catalog[item.name] = opt.data;
|
||||
} catch (error) {
|
||||
console.error(`ERROR: jinja_svg_catalog processing ${item.name} src: ${item.src} -- ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(dirname(dest), { recursive: true });
|
||||
|
||||
const ctx = {
|
||||
svg_catalog: svg_catalog,
|
||||
macros: macros,
|
||||
edge_template: edge_template,
|
||||
__jinja_class_placeholder__: __jinja_class_placeholder__,
|
||||
// see https://github.com/edge-js/edge/issues/162
|
||||
open_curly_brace: "{{",
|
||||
close_curly_brace: "}}"
|
||||
};
|
||||
|
||||
const jinjatmpl = Edge.create().renderRawSync(fs.readFileSync(edge_template, "utf-8"), ctx);
|
||||
|
||||
fs.writeFileSync(dest, jinjatmpl);
|
||||
console.log(`[jinja_svg_catalog] created: ${dest}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calls jinja_svg_catalog for a collection of icon sets where each set has its
|
||||
* own parameters.
|
||||
*
|
||||
* @param dest - filename of the generate jinja template.
|
||||
* @param macros - Jinja macros to create.
|
||||
* @param sets - Array of SVG sets.
|
||||
*/
|
||||
export const jinja_svg_sets = (dest: string, macros: JinjaMacro[], sets: IconSet[]): void => {
|
||||
const items: IconSVG[] = [];
|
||||
const all: string[] = [];
|
||||
|
||||
for (const obj of sets) {
|
||||
for (const [name, file] of Object.entries(obj.set)) {
|
||||
if (all.includes(name)) {
|
||||
throw new Error(`ERROR: ${name} has already been defined`);
|
||||
}
|
||||
|
||||
all.push(name);
|
||||
items.push({
|
||||
name: name,
|
||||
src: resolve(obj.base, file),
|
||||
svgo_opts: obj.svgo_opts
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
jinja_svg_catalog(dest, macros, items);
|
||||
};
|
||||
41
client/simple/tools/plg.js
Normal file
41
client/simple/tools/plg.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Custom vite plugins to build the web-client components of the simple theme.
|
||||
*
|
||||
* HINT:
|
||||
*
|
||||
* This is an inital implementation for the migration of the build process
|
||||
* from grunt to vite. For fully support (vite: build & serve) more work is
|
||||
* needed.
|
||||
*/
|
||||
|
||||
import { svg2png } from "./img.js";
|
||||
import { svg2svg } from "./img.js";
|
||||
|
||||
/**
|
||||
* Vite plugin to convert a list of SVG files to PNG.
|
||||
*
|
||||
* @param {import('./img.js').Src2Dest} items - Array of SVG files (src: SVG, dest:PNG) to convert.
|
||||
*/
|
||||
function plg_svg2png(items) {
|
||||
return {
|
||||
name: 'searxng-simple-svg2png',
|
||||
apply: 'build', // or 'serve'
|
||||
async writeBundle() { svg2png(items); },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite plugin to optimize SVG images for WEB.
|
||||
*
|
||||
* @param {import('svgo').Config} svgo_opts - Options passed to svgo.
|
||||
* @param {import('./img.js').Src2Dest} items - Array of SVG files (src:SVG, dest:SVG) to optimize.
|
||||
*/
|
||||
function plg_svg2svg(svgo_opts, items) {
|
||||
return {
|
||||
name: 'searxng-simple-svg2png',
|
||||
apply: 'build', // or 'serve'
|
||||
async writeBundle() { svg2svg(items, svgo_opts); },
|
||||
};
|
||||
}
|
||||
|
||||
export { plg_svg2png, plg_svg2svg };
|
||||
@@ -1,47 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* Custom vite plugins to build the web-client components of the simple theme.
|
||||
*
|
||||
* HINT:
|
||||
* This is an initial implementation for the migration of the build process
|
||||
* from grunt to vite. For fully support (vite: build & serve) more work is
|
||||
* needed.
|
||||
*/
|
||||
|
||||
import type { Config } from "svgo";
|
||||
import type { Plugin } from "vite";
|
||||
import { type Src2Dest, svg2png, svg2svg } from "./img.ts";
|
||||
|
||||
/**
|
||||
* Vite plugin to convert a list of SVG files to PNG.
|
||||
*
|
||||
* @param items - Array of SVG files (src: SVG, dest:PNG) to convert.
|
||||
* @param width - (optional) width of the PNG picture
|
||||
* @param height - (optional) height of the PNG picture
|
||||
*/
|
||||
export const plg_svg2png = (items: Src2Dest[], width?: number, height?: number): Plugin => {
|
||||
return {
|
||||
name: "searxng-simple-svg2png",
|
||||
apply: "build",
|
||||
writeBundle: () => {
|
||||
svg2png(items, width, height);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Vite plugin to optimize SVG images for WEB.
|
||||
*
|
||||
* @param items - Array of SVG files (src:SVG, dest:SVG) to optimize.
|
||||
* @param svgo_opts - Options passed to svgo.
|
||||
*/
|
||||
export const plg_svg2svg = (items: Src2Dest[], svgo_opts: Config): Plugin => {
|
||||
return {
|
||||
name: "searxng-simple-svg2svg",
|
||||
apply: "build",
|
||||
writeBundle: () => {
|
||||
svg2svg(items, svgo_opts);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"target": "ES2022",
|
||||
|
||||
"allowImportingTsExtensions": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"incremental": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
"strict": true,
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["./"],
|
||||
"exclude": ["./node_modules/"]
|
||||
}
|
||||
185
client/simple/vite.config.js
Normal file
185
client/simple/vite.config.js
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* CONFIG: https://vite.dev/config/
|
||||
*/
|
||||
|
||||
import { resolve } from "node:path";
|
||||
import { defineConfig } from "vite";
|
||||
import stylelint from "vite-plugin-stylelint";
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
import { plg_svg2png } from "./tools/plg.js";
|
||||
import { plg_svg2svg } from "./tools/plg.js";
|
||||
|
||||
|
||||
const ROOT = "../.."; // root of the git reposetory
|
||||
|
||||
const PATH = {
|
||||
|
||||
dist: resolve(ROOT, "searx/static/themes/simple"),
|
||||
// dist: resolve(ROOT, "client/simple/dist"),
|
||||
|
||||
src: "src",
|
||||
modules: "node_modules",
|
||||
brand: "src/brand",
|
||||
static: resolve(ROOT, "client/simple/static"),
|
||||
leaflet: resolve(ROOT, "client/simple/node_modules/leaflet/dist"),
|
||||
templates: resolve(ROOT, "searx/templates/simple"),
|
||||
};
|
||||
|
||||
const svg2svg_opts = {
|
||||
plugins: [
|
||||
{ name: "preset-default" },
|
||||
"sortAttrs",
|
||||
"convertStyleToAttrs",
|
||||
]
|
||||
};
|
||||
|
||||
const svg2svg_favicon_opts = {
|
||||
plugins: [
|
||||
{ name: "preset-default" },
|
||||
"sortAttrs",
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
root: PATH.src,
|
||||
mode: "production",
|
||||
// mode: "development",
|
||||
|
||||
// FIXME: missing CCS sourcemaps!!
|
||||
// see: https://github.com/vitejs/vite/discussions/13845#discussioncomment-11992084
|
||||
//
|
||||
// what I have tried so far (see config below):
|
||||
//
|
||||
// - build.sourcemap
|
||||
// - esbuild.sourcemap
|
||||
// - css.preprocessorOptions.less.sourceMap
|
||||
|
||||
css: {
|
||||
devSourcemap: true,
|
||||
preprocessorOptions: {
|
||||
less: {
|
||||
// FIXME: missing CCS sourcemaps!!
|
||||
sourceMap: {
|
||||
outputSourceFiles: true,
|
||||
sourceMapURL: (name) => { const s = name.split('/'); return s[s.length - 1] + '.map'; },
|
||||
},
|
||||
// env: 'development',
|
||||
// relativeUrls: true,
|
||||
// javascriptEnabled: true,
|
||||
},
|
||||
},
|
||||
}, // end: css
|
||||
|
||||
esbuild : {
|
||||
// FIXME: missing CCS sourcemaps!!
|
||||
sourcemap: true
|
||||
},
|
||||
|
||||
build: {
|
||||
manifest: "manifest.json",
|
||||
emptyOutDir: true,
|
||||
assetsDir: "",
|
||||
outDir: PATH.dist,
|
||||
|
||||
// FIXME: missing CCS sourcemaps!!
|
||||
sourcemap: true,
|
||||
|
||||
// https://vite.dev/config/build-options.html#build-cssminify
|
||||
cssMinify: true,
|
||||
// cssMinify: "esbuild",
|
||||
minify: "esbuild",
|
||||
|
||||
rollupOptions: {
|
||||
input: {
|
||||
|
||||
// build CSS files
|
||||
"css/searxng.min.css": PATH.src + "/less/style-ltr.less",
|
||||
"css/searxng-rtl.min.css": PATH.src + "/less/style-rtl.less",
|
||||
"css/rss.min.css": PATH.src + "/less/rss.less",
|
||||
|
||||
// build JS files
|
||||
"js/searxng.head.min": PATH.src + "/js/searxng.head.js",
|
||||
"js/searxng.min": PATH.src + "/js/searxng.js",
|
||||
|
||||
},
|
||||
|
||||
// file naming conventions / pathnames are relative to outDir (PATH.dist)
|
||||
output: {
|
||||
entryFileNames: "[name].js",
|
||||
chunkFileNames: "[name].js",
|
||||
assetFileNames: "[name].[ext]",
|
||||
// Vite does not support "rollupOptions.output.sourcemap".
|
||||
// Please use "build.sourcemap" instead.
|
||||
// sourcemap: true,
|
||||
},
|
||||
|
||||
},
|
||||
}, // end: build
|
||||
|
||||
plugins: [
|
||||
|
||||
stylelint({
|
||||
build: true,
|
||||
emitWarningAsError: true,
|
||||
fix: true,
|
||||
}),
|
||||
|
||||
// Leaflet
|
||||
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{ src: PATH.leaflet + "/leaflet.{js,js.map}", dest: PATH.dist + "/js" },
|
||||
{ src: PATH.leaflet + "/images/*.png", dest: PATH.dist + "/css/images/" },
|
||||
{ src: PATH.leaflet + "/*.{css,css.map}", dest: PATH.dist + "/css" },
|
||||
{ src: PATH.static + "/**/*", dest: PATH.dist },
|
||||
]
|
||||
}),
|
||||
|
||||
// -- svg images
|
||||
|
||||
plg_svg2svg(
|
||||
[
|
||||
{ src: PATH.src + "/svg/empty_favicon.svg", dest: PATH.dist + "/img/empty_favicon.svg" },
|
||||
{ src: PATH.src + "/svg/select-dark.svg", dest: PATH.dist + "/img/select-dark.svg" },
|
||||
{ src: PATH.src + "/svg/select-light.svg", dest: PATH.dist + "/img/select-light.svg" },
|
||||
],
|
||||
svg2svg_opts,
|
||||
),
|
||||
|
||||
// SearXNG brand (static)
|
||||
|
||||
plg_svg2png(
|
||||
[
|
||||
{ src: PATH.brand + "/searxng-wordmark.svg", dest: PATH.dist + "/img/favicon.png" },
|
||||
{ src: PATH.brand + "/searxng.svg", dest: PATH.dist + "/img/searxng.png" },
|
||||
],
|
||||
),
|
||||
|
||||
// -- svg
|
||||
plg_svg2svg(
|
||||
[
|
||||
{ src: PATH.brand + "/searxng.svg", dest: PATH.dist + "/img/searxng.svg" },
|
||||
{ src: PATH.brand + "/img_load_error.svg", dest: PATH.dist + "/img/img_load_error.svg" },
|
||||
],
|
||||
svg2svg_opts,
|
||||
),
|
||||
|
||||
// -- favicon
|
||||
plg_svg2svg(
|
||||
[ { src: PATH.brand + "/searxng-wordmark.svg", dest: PATH.dist + "/img/favicon.svg" } ],
|
||||
svg2svg_favicon_opts,
|
||||
),
|
||||
|
||||
// -- simple templates
|
||||
plg_svg2svg(
|
||||
[
|
||||
{ src: PATH.brand + "/searxng-wordmark.svg", dest: PATH.templates + "/searxng-wordmark.min.svg" },
|
||||
],
|
||||
svg2svg_opts
|
||||
),
|
||||
|
||||
] // end: plugins
|
||||
|
||||
});
|
||||
@@ -1,210 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* CONFIG: https://vite.dev/config/
|
||||
*/
|
||||
|
||||
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" with { type: "json" };
|
||||
import { plg_svg2png, plg_svg2svg } from "./tools/plg.ts";
|
||||
|
||||
const ROOT = "../../"; // root of the git repository
|
||||
|
||||
const PATH = {
|
||||
brand: "src/brand/",
|
||||
dist: resolve(ROOT, "searx/static/themes/simple/"),
|
||||
modules: "node_modules/",
|
||||
src: "src/",
|
||||
templates: resolve(ROOT, "searx/templates/simple/")
|
||||
} as const;
|
||||
|
||||
const svg2svg_opts: Config = {
|
||||
plugins: [{ name: "preset-default" }, "sortAttrs", "convertStyleToAttrs"]
|
||||
};
|
||||
|
||||
const svg2svg_favicon_opts: Config = {
|
||||
plugins: [{ name: "preset-default" }, "sortAttrs"]
|
||||
};
|
||||
|
||||
export default {
|
||||
base: "./",
|
||||
publicDir: "static/",
|
||||
|
||||
build: {
|
||||
target: browserslistToEsbuild(manifest.browserslist),
|
||||
assetsDir: "",
|
||||
outDir: PATH.dist,
|
||||
manifest: "manifest.json",
|
||||
emptyOutDir: true,
|
||||
sourcemap: true,
|
||||
rolldownOptions: {
|
||||
input: {
|
||||
// entrypoint
|
||||
core: `${PATH.src}/js/index.ts`,
|
||||
|
||||
// stylesheets
|
||||
ltr: `${PATH.src}/less/style-ltr.less`,
|
||||
rtl: `${PATH.src}/less/style-rtl.less`,
|
||||
rss: `${PATH.src}/less/rss.less`
|
||||
},
|
||||
|
||||
// file naming conventions / pathnames are relative to outDir (PATH.dist)
|
||||
output: {
|
||||
entryFileNames: "sxng-[name].min.js",
|
||||
chunkFileNames: "chunk/[hash].min.js",
|
||||
assetFileNames: ({ names }: PreRenderedAsset): string => {
|
||||
const [name] = names;
|
||||
|
||||
switch (name?.split(".").pop()) {
|
||||
case "css":
|
||||
return "sxng-[name].min[extname]";
|
||||
default:
|
||||
return "sxng-[name][extname]";
|
||||
}
|
||||
},
|
||||
sanitizeFileName: (name: string): string => {
|
||||
return name
|
||||
.normalize("NFD")
|
||||
.replace(/[^a-zA-Z0-9.-]/g, "_")
|
||||
.toLowerCase();
|
||||
},
|
||||
comments: {
|
||||
legal: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}, // end: build
|
||||
|
||||
plugins: [
|
||||
// -- bundle analyzer
|
||||
analyzer({
|
||||
enabled: process.env.VITE_BUNDLE_ANALYZE === "true",
|
||||
analyzerPort: "auto",
|
||||
summary: true,
|
||||
reportTitle: manifest.name,
|
||||
|
||||
// sidecars with max compression
|
||||
gzipOptions: {
|
||||
level: zlibConstants.Z_BEST_COMPRESSION
|
||||
},
|
||||
brotliOptions: {
|
||||
params: {
|
||||
[zlibConstants.BROTLI_PARAM_QUALITY]: zlibConstants.BROTLI_MAX_QUALITY
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
// -- svg images
|
||||
plg_svg2svg(
|
||||
[
|
||||
{
|
||||
src: `${PATH.src}/svg/empty_favicon.svg`,
|
||||
dest: `${PATH.dist}/img/empty_favicon.svg`
|
||||
},
|
||||
{
|
||||
src: `${PATH.src}/svg/select-dark.svg`,
|
||||
dest: `${PATH.dist}/img/select-dark.svg`
|
||||
},
|
||||
{
|
||||
src: `${PATH.src}/svg/select-light.svg`,
|
||||
dest: `${PATH.dist}/img/select-light.svg`
|
||||
}
|
||||
],
|
||||
svg2svg_opts
|
||||
),
|
||||
|
||||
// SearXNG brand (static)
|
||||
plg_svg2png([
|
||||
{
|
||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
||||
dest: `${PATH.dist}/img/favicon.png`
|
||||
},
|
||||
{
|
||||
src: `${PATH.brand}/searxng.svg`,
|
||||
dest: `${PATH.dist}/img/searxng.png`
|
||||
}
|
||||
]),
|
||||
|
||||
// SearXNG PWA Icons (static)
|
||||
plg_svg2png(
|
||||
[
|
||||
{
|
||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
||||
dest: `${PATH.dist}/img/512.png`
|
||||
}
|
||||
],
|
||||
512,
|
||||
512
|
||||
),
|
||||
plg_svg2png(
|
||||
[
|
||||
{
|
||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
||||
dest: `${PATH.dist}/img/192.png`
|
||||
}
|
||||
],
|
||||
192,
|
||||
192
|
||||
),
|
||||
|
||||
// -- svg
|
||||
plg_svg2svg(
|
||||
[
|
||||
{
|
||||
src: `${PATH.brand}/searxng.svg`,
|
||||
dest: `${PATH.dist}/img/searxng.svg`
|
||||
},
|
||||
{
|
||||
src: `${PATH.brand}/img_load_error.svg`,
|
||||
dest: `${PATH.dist}/img/img_load_error.svg`
|
||||
}
|
||||
],
|
||||
svg2svg_opts
|
||||
),
|
||||
|
||||
// -- favicon
|
||||
plg_svg2svg(
|
||||
[
|
||||
{
|
||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
||||
dest: `${PATH.dist}/img/favicon.svg`
|
||||
}
|
||||
],
|
||||
svg2svg_favicon_opts
|
||||
),
|
||||
|
||||
// -- simple templates
|
||||
plg_svg2svg(
|
||||
[
|
||||
{
|
||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
||||
dest: `${PATH.templates}/searxng-wordmark.min.svg`
|
||||
}
|
||||
],
|
||||
svg2svg_opts
|
||||
)
|
||||
], // end: plugins
|
||||
|
||||
// FIXME: missing CCS sourcemaps!!
|
||||
// see: https://github.com/vitejs/vite/discussions/13845#discussioncomment-11992084
|
||||
//
|
||||
// what I have tried so far (see config below):
|
||||
//
|
||||
// - build.sourcemap
|
||||
// - esbuild.sourcemap
|
||||
// - css.preprocessorOptions.less.sourceMap
|
||||
css: {
|
||||
transformer: "lightningcss",
|
||||
lightningcss: {
|
||||
targets: browserslistToTargets(manifest.browserslist)
|
||||
},
|
||||
devSourcemap: true
|
||||
} // end: css
|
||||
} satisfies UserConfig;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user