1 Commits

Author SHA1 Message Date
searxng-bot
308659964a [l10n] update translations from Weblate
a4922b114 - 2025-08-21 - KOUSTAV <koustav@noreply.codeberg.org>
3f61bddd1 - 2025-08-18 - reis2724 <reis2724@noreply.codeberg.org>
7313a683d - 2025-08-19 - Kemystra <kemystra@noreply.codeberg.org>
92e1bce48 - 2025-08-19 - Artiman <artiman@noreply.codeberg.org>
0f27c1bc2 - 2025-08-18 - youtherthyf <youtherthyf@noreply.codeberg.org>
60496bc76 - 2025-08-16 - alexgabi <alexgabi@noreply.codeberg.org>
742982d00 - 2025-08-15 - lucasmz.dev <lucasmz.dev@noreply.codeberg.org>
2025-08-22 07:10:08 +00:00
738 changed files with 38953 additions and 63289 deletions

163
.dir-locals-template.el Normal file
View 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"
;; ))
))))
)

View File

@@ -1,6 +1,5 @@
* *
!container/*.template.*
!container/entrypoint.sh !container/entrypoint.sh
!searx/** !searx/**
!requirements*.txt !requirements*.txt

View File

@@ -10,7 +10,7 @@ trim_trailing_whitespace = true
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
[{*.py,*.pyi}] [*.py]
# code formatter accepts length of 120, but editor should prefer 80 # code formatter accepts length of 120, but editor should prefer 80
max_line_length = 80 max_line_length = 80

View File

@@ -1,50 +1,39 @@
--- ---
name: "Bug report" name: Bug report
about: Report a bug in SearXNG" about: Report a bug in SearXNG
labels: ["bug"] title: ''
type: "bug" 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. Please also stipulate if you are using a forked version of SearXNG and
include a link to the fork source code.
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
--> -->
**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 To Reproduce**
<!-- How can we reproduce this issue? (as minimally and as precisely as possible) -->
<!-- How can we reproduce this issue? (as minimally and as precisely as
possible) -->
### Expected behavior
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. --> <!-- 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. --> <!-- If applicable, add screenshots, logs to help explain your problem. -->
### Version of SearXNG **Additional context**
<!-- 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
<!-- Add any other context about the problem here. --> <!-- 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.

View File

@@ -1,4 +1,4 @@
blank_issues_enabled: false blank_issues_enabled: true
contact_links: contact_links:
- name: Questions & Answers (Q&A) - name: Questions & Answers (Q&A)
url: https://github.com/searxng/searxng/discussions/categories/q-a url: https://github.com/searxng/searxng/discussions/categories/q-a

View File

@@ -1,46 +1,31 @@
--- ---
name: Engine request" name: Engine request
about: Request a new engine in SearXNG" about: Request a new engine in SearXNG
labels: ["engine request"] title: ''
type: "feature" 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? --> **Additional context**
<!-- Add any other context about this engine here. -->
### 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.

View File

@@ -1,32 +1,21 @@
--- ---
name: "Feature request" name: Feature request
about: "Request a new feature in SearXNG" about: Request a new feature in SearXNG
labels: ["new feature"] title: ''
type: "feature" labels: enhancement
assignees: ''
--- ---
<!-- PLEASE FILL THESE FIELDS, IT REALLY HELPS THE MAINTAINERS OF SearXNG -->
_Replace this placeholder with a concise description of the feature._ **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 [...] -->
<!-- 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
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. --> <!-- 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. --> <!-- A clear and concise description of any alternative solutions or features you've considered. -->
### Code of Conduct **Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
[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.

47
.github/workflows/checker.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
---
name: Checker
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
schedule:
- cron: "0 4 * * 5"
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
permissions:
contents: read
env:
PYTHON_VERSION: "3.13"
jobs:
search:
if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch'
name: Search
runs-on: ubuntu-24.04-arm
steps:
- name: Setup Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: "false"
- name: Setup cache Python
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
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: Search checker
run: make search.checker

37
.github/workflows/cleanup.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
---
name: Cleanup
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
schedule:
- cron: "4 4 * * *"
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
permissions:
contents: read
jobs:
registry:
# FIXME: On forks it fails with "Failed to fetch packages: missing field `id` at line 1 column 141"
if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch'
name: Registry
runs-on: ubuntu-24.04
permissions:
# Organization GHCR
packages: write
steps:
- name: Prune
uses: snok/container-retention-policy@4f22ef80902ad409ed55a99dc5133cc1250a0d03 # v3.0.0
with:
account: "${{ github.repository_owner }}"
token: "${{ secrets.GITHUB_TOKEN }}"
# Remove only cache images https://github.com/snok/container-retention-policy/issues/97
image-names: "cache"
image-tags: "!searxng*"
cut-off: "1d"
keep-n-most-recent: "30"

View File

@@ -18,98 +18,158 @@ concurrency:
permissions: permissions:
contents: read contents: read
# Organization GHCR
packages: read packages: read
env: env:
PYTHON_VERSION: "3.14" PYTHON_VERSION: "3.13"
jobs: jobs:
build-base:
if: |
(github.repository_owner == 'searxng' && github.event.workflow_run.conclusion == 'success')
|| github.event_name == 'workflow_dispatch'
name: Build base
runs-on: ubuntu-24.04
permissions:
# Organization GHCR
packages: write
steps:
- if: github.repository_owner == 'searxng'
name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: "false"
- if: github.repository_owner == 'searxng'
name: Get date
id: date
run: echo "date=$(date +'%Y%m%d')" >>$GITHUB_OUTPUT
- if: github.repository_owner == 'searxng'
name: Check cache apko
id: cache-apko
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
# yamllint disable-line rule:line-length
key: "apko-${{ steps.date.outputs.date }}-${{ hashFiles('./container/base.yml', './container/base-builder.yml') }}"
path: "/tmp/.apko/"
lookup-only: true
- if: github.repository_owner == 'searxng' && steps.cache-apko.outputs.cache-hit != 'true'
name: Setup cache apko
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
# yamllint disable-line rule:line-length
key: "apko-${{ steps.date.outputs.date }}-${{ hashFiles('./container/base.yml', './container/base-builder.yml') }}"
restore-keys: "apko-${{ steps.date.outputs.date }}-"
path: "/tmp/.apko/"
- if: github.repository_owner == 'searxng' && steps.cache-apko.outputs.cache-hit != 'true'
name: Setup apko
run: |
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
brew install apko
- if: github.repository_owner == 'searxng' && steps.cache-apko.outputs.cache-hit != 'true'
name: Login to GHCR
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: "ghcr.io"
username: "${{ github.repository_owner }}"
password: "${{ secrets.GITHUB_TOKEN }}"
- if: github.repository_owner == 'searxng' && steps.cache-apko.outputs.cache-hit != 'true'
name: Build
run: |
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
apko publish ./container/base.yml ghcr.io/${{ github.repository_owner }}/base:searxng \
--cache-dir=/tmp/.apko/ \
--sbom=false \
--vcs=false \
--log-level=debug
apko publish ./container/base-builder.yml ghcr.io/${{ github.repository_owner }}/base:searxng-builder \
--cache-dir=/tmp/.apko/ \
--sbom=false \
--vcs=false \
--log-level=debug
build: build:
if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch' if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch'
name: Build (${{ matrix.arch }}) name: Build (${{ matrix.arch }})
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: build-base
strategy: strategy:
fail-fast: false fail-fast: false
# Faster runners first to cache arch independent wheels
max-parallel: 1
matrix: matrix:
include: include:
- arch: amd64 - arch: amd64
march: amd64
os: ubuntu-24.04 os: ubuntu-24.04
emulation: false emulation: false
- arch: arm64 - arch: arm64
march: arm64
os: ubuntu-24.04-arm os: ubuntu-24.04-arm
emulation: false emulation: false
- arch: armv7 - arch: armv7
march: arm64
os: ubuntu-24.04-arm os: ubuntu-24.04-arm
emulation: true emulation: true
permissions: permissions:
# Organization GHCR
packages: write packages: write
# Clean key cache step
actions: write
outputs: outputs:
docker_tag: ${{ steps.build.outputs.docker_tag }} docker_tag: ${{ steps.build.outputs.docker_tag }}
git_url: ${{ steps.build.outputs.git_url }} git_url: ${{ steps.build.outputs.git_url }}
steps: steps:
# yamllint disable rule:line-length
- name: Setup podman
env:
PODMAN_VERSION: "v5.7.1"
run: |
sudo apt-get purge -y podman runc crun conmon
curl -fsSLO "https://github.com/mgoltzsche/podman-static/releases/download/${{ env.PODMAN_VERSION }}/podman-linux-${{ matrix.march }}.tar.gz"
curl -fsSLO "https://github.com/mgoltzsche/podman-static/releases/download/${{ env.PODMAN_VERSION }}/podman-linux-${{ matrix.march }}.tar.gz.asc"
gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 0CCF102C4F95D89E583FF1D4F8B5AF50344BB503
gpg --batch --verify "podman-linux-${{ matrix.march }}.tar.gz.asc" "podman-linux-${{ matrix.march }}.tar.gz"
tar -xzf "podman-linux-${{ matrix.march }}.tar.gz"
sudo cp -rfv ./podman-linux-${{ matrix.march }}/etc/. /etc/
sudo cp -rfv ./podman-linux-${{ matrix.march }}/usr/. /usr/
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
# yamllint enable rule:line-length
- name: Setup Python - name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
path: "./local/" path: "./local/"
- name: Get date - name: Restore cache container mounts
id: date id: cache-container-mounts
run: echo "date=$(date +'%Y%m%d')" >>$GITHUB_OUTPUT uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
- name: Setup cache container
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with: with:
key: "container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-${{ hashFiles('./requirements*.txt') }}" key: "container-mounts-${{ hashFiles('./container/*.dockerfile') }}"
restore-keys: | restore-keys: "container-mounts-"
container-${{ matrix.arch }}-${{ steps.date.outputs.date }}- path: |
container-${{ matrix.arch }}- /var/tmp/buildah-cache/
path: "/var/tmp/buildah-cache-*/*" /var/tmp/buildah-cache-*/
# https://github.com/actions/cache/pull/1308
- if: steps.cache-container-mounts.outputs.cache-hit == 'true'
name: Clean key cache container mounts
continue-on-error: true
env:
GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
run: gh cache delete container-mounts-${{ hashFiles('./container/*.dockerfile') }}
- if: ${{ matrix.emulation }} - if: ${{ matrix.emulation }}
name: Setup QEMU name: Setup QEMU
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with: with:
registry: "ghcr.io" registry: "ghcr.io"
username: "${{ github.repository_owner }}" username: "${{ github.repository_owner }}"
@@ -121,6 +181,15 @@ jobs:
OVERRIDE_ARCH: "${{ matrix.arch }}" OVERRIDE_ARCH: "${{ matrix.arch }}"
run: make podman.build run: make podman.build
- if: always()
name: Save cache container mounts
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
key: "container-mounts-${{ hashFiles('./container/*.dockerfile') }}"
path: |
/var/tmp/buildah-cache/
/var/tmp/buildah-cache-*/
test: test:
name: Test (${{ matrix.arch }}) name: Test (${{ matrix.arch }})
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@@ -141,16 +210,16 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
- if: ${{ matrix.emulation }} - if: ${{ matrix.emulation }}
name: Setup QEMU name: Setup QEMU
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with: with:
registry: "ghcr.io" registry: "ghcr.io"
username: "${{ github.repository_owner }}" username: "${{ github.repository_owner }}"
@@ -171,27 +240,28 @@ jobs:
- test - test
permissions: permissions:
# Organization GHCR
packages: write packages: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with: with:
registry: "ghcr.io" registry: "ghcr.io"
username: "${{ github.repository_owner }}" username: "${{ github.repository_owner }}"
password: "${{ secrets.GITHUB_TOKEN }}" password: "${{ secrets.GITHUB_TOKEN }}"
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with: with:
registry: "docker.io" registry: "docker.io"
username: "${{ secrets.DOCKER_USER }}" username: "${{ secrets.DOCKERHUB_USERNAME }}"
password: "${{ secrets.DOCKER_TOKEN }}" password: "${{ secrets.DOCKERHUB_TOKEN }}"
- name: Release - name: Release
env: env:

View File

@@ -15,7 +15,7 @@ permissions:
contents: read contents: read
env: env:
PYTHON_VERSION: "3.14" PYTHON_VERSION: "3.13"
jobs: jobs:
data: data:
@@ -33,7 +33,6 @@ jobs:
- update_engine_traits.py - update_engine_traits.py
- update_wikidata_units.py - update_wikidata_units.py
- update_engine_descriptions.py - update_engine_descriptions.py
- update_gsa_useragents.py
permissions: permissions:
contents: write contents: write
@@ -41,21 +40,20 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
path: "./local/" path: "./local/"
- name: Setup venv - name: Setup venv
@@ -66,7 +64,7 @@ jobs:
- name: Create PR - name: Create PR
id: cpr id: cpr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with: with:
author: "searxng-bot <searxng-bot@users.noreply.github.com>" author: "searxng-bot <searxng-bot@users.noreply.github.com>"
committer: "searxng-bot <searxng-bot@users.noreply.github.com>" committer: "searxng-bot <searxng-bot@users.noreply.github.com>"

View File

@@ -19,7 +19,7 @@ permissions:
contents: read contents: read
env: env:
PYTHON_VERSION: "3.14" PYTHON_VERSION: "3.13"
jobs: jobs:
release: release:
@@ -32,36 +32,32 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
path: "./local/" path: "./local/"
- name: Setup dependencies
run: sudo ./utils/searxng.sh install buildhost
- name: Setup venv - name: Setup venv
run: make V=1 install run: make V=1 install
- name: Build documentation - name: Build documentation
run: make V=1 docs.html run: make V=1 docs.clean docs.html
- if: github.ref_name == 'master' - if: github.ref_name == 'master'
name: Release name: Release
uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4.8.0 uses: JamesIves/github-pages-deploy-action@6c2d9db40f9296374acc17b90404b6e8864128c8 # v4.7.3
with: with:
folder: "dist/docs" folder: "dist/docs"
branch: "gh-pages" branch: "gh-pages"

View File

@@ -18,7 +18,7 @@ permissions:
contents: read contents: read
env: env:
PYTHON_VERSION: "3.14" PYTHON_VERSION: "3.13"
jobs: jobs:
test: test:
@@ -27,28 +27,28 @@ jobs:
strategy: strategy:
matrix: matrix:
python-version: python-version:
- "3.9"
- "3.10"
- "3.11" - "3.11"
- "3.12" - "3.12"
- "3.13" - "3.13"
- "3.14"
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with: with:
python-version: "${{ matrix.python-version }}" python-version: "${{ matrix.python-version }}"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: "python-${{ matrix.python-version }}-${{ runner.arch }}-"
python-${{ matrix.python-version }}-${{ runner.arch }}-
path: "./local/" path: "./local/"
- name: Setup venv - name: Setup venv
@@ -62,32 +62,31 @@ jobs:
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: "./.nvmrc" node-version-file: "./.nvmrc"
- name: Setup cache Node.js - name: Setup cache Node.js
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
key: "nodejs-${{ runner.arch }}-${{ hashFiles('./.nvmrc', './package.json') }}" key: "nodejs-${{ runner.arch }}-${{ hashFiles('./.nvmrc', './package.json') }}"
path: "./client/simple/node_modules/" path: "./client/simple/node_modules/"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
path: "./local/" path: "./local/"
- name: Setup venv - name: Setup venv

View File

@@ -22,7 +22,7 @@ permissions:
contents: read contents: read
env: env:
PYTHON_VERSION: "3.14" PYTHON_VERSION: "3.13"
jobs: jobs:
update: update:
@@ -35,22 +35,21 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}" token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
path: "./local/" path: "./local/"
- name: Setup venv - name: Setup venv
@@ -83,22 +82,21 @@ jobs:
steps: steps:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with: with:
python-version: "${{ env.PYTHON_VERSION }}" python-version: "${{ env.PYTHON_VERSION }}"
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}" token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
fetch-depth: "0" fetch-depth: "0"
- name: Setup cache Python - name: Setup cache Python
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}" key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
restore-keys: | restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
path: "./local/" path: "./local/"
- name: Setup venv - name: Setup venv
@@ -119,7 +117,7 @@ jobs:
- name: Create PR - name: Create PR
id: cpr id: cpr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with: with:
author: "searxng-bot <searxng-bot@users.noreply.github.com>" author: "searxng-bot <searxng-bot@users.noreply.github.com>"
committer: "searxng-bot <searxng-bot@users.noreply.github.com>" committer: "searxng-bot <searxng-bot@users.noreply.github.com>"
@@ -132,7 +130,7 @@ jobs:
body: | body: |
[l10n] update translations from Weblate [l10n] update translations from Weblate
labels: | labels: |
area:i18n translation
- name: Display information - name: Display information
run: | run: |

View File

@@ -24,16 +24,16 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: "false" persist-credentials: "false"
- name: Sync GHCS from Docker Scout - name: Sync GHCS from Docker Scout
uses: docker/scout-action@7520205ff60037fdc436b40b6a1d1e55a839ec2d # v1.22.0 uses: docker/scout-action@f8c776824083494ab0d56b8105ba2ca85c86e4de # v1.18.2
with: with:
organization: "searxng" organization: "searxng"
dockerhub-user: "${{ secrets.DOCKER_USER }}" dockerhub-user: "${{ secrets.DOCKERHUB_USERNAME }}"
dockerhub-password: "${{ secrets.DOCKER_TOKEN }}" dockerhub-password: "${{ secrets.DOCKERHUB_TOKEN }}"
image: "registry://ghcr.io/searxng/searxng:latest" image: "registry://ghcr.io/searxng/searxng:latest"
command: "cves" command: "cves"
sarif-file: "./scout.sarif" sarif-file: "./scout.sarif"
@@ -41,6 +41,6 @@ jobs:
write-comment: "false" write-comment: "false"
- name: Upload SARIFs - name: Upload SARIFs
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 uses: github/codeql-action/upload-sarif@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
with: with:
sarif_file: "./scout.sarif" sarif_file: "./scout.sarif"

View File

@@ -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

2
.nvmrc
View File

@@ -1 +1 @@
25 24

View File

@@ -162,7 +162,7 @@ no-docstring-rgx=^_
property-classes=abc.abstractproperty property-classes=abc.abstractproperty
# Regular expression matching correct variable names # 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] [FORMAT]
@@ -311,7 +311,7 @@ dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
ignored-argument-names=_.*|^ignored_|^unused_ ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files. # 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 # List of qualified module names which can have objects that can redefine
# builtins. # builtins.

4
.tool-versions Normal file
View File

@@ -0,0 +1,4 @@
nodejs 24.3.0
python 3.13.1
shellcheck 0.10.0
sqlite 3.47.2

View File

@@ -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.

49
CONTRIBUTING.md Normal file
View 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.

View File

@@ -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`_.

View File

@@ -17,6 +17,7 @@ help:
@echo 'install - developer install of SearxNG into virtualenv' @echo 'install - developer install of SearxNG into virtualenv'
@echo 'uninstall - uninstall developer installation' @echo 'uninstall - uninstall developer installation'
@echo 'clean - clean up working tree' @echo 'clean - clean up working tree'
@echo 'search.checker - check search engines'
@echo 'test - run shell & CI tests' @echo 'test - run shell & CI tests'
@echo 'test.shell - test shell scripts' @echo 'test.shell - test shell scripts'
@echo 'ci.test - run CI tests' @echo 'ci.test - run CI tests'
@@ -38,6 +39,12 @@ clean: py.clean docs.clean node.clean nvm.clean go.clean test.clean
$(Q)find . -name '*~' -exec rm -f {} + $(Q)find . -name '*~' -exec rm -f {} +
$(Q)find . -name '*.bak' -exec rm -f {} + $(Q)find . -name '*.bak' -exec rm -f {} +
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 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 test: test.yamllint test.black test.pyright_modified test.pylint test.unit test.robot test.rst test.shell test.shfmt
@@ -63,7 +70,7 @@ format: format.python format.shell
# wrap ./manage script # wrap ./manage script
MANAGE += weblate.translations.commit weblate.push.translations MANAGE += weblate.translations.commit weblate.push.translations
MANAGE += data.all data.traits data.useragents data.gsa_useragents data.locales data.currencies MANAGE += data.all data.traits data.useragents data.locales data.currencies
MANAGE += docs.html docs.live docs.gh-pages docs.prebuild docs.clean MANAGE += docs.html docs.live docs.gh-pages docs.prebuild docs.clean
MANAGE += podman.build MANAGE += podman.build
MANAGE += docker.build docker.buildx MANAGE += docker.build docker.buildx

View File

@@ -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 <!-- explain the motivation behind your PR -->
any edge cases (environment, language, or other contexts) to take into
account? -->
### 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.

View File

@@ -1,62 +1,130 @@
.. SPDX-License-Identifier: AGPL-3.0-or-later .. 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 .. 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 :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 Searx.space_ lists ready-to-use running instances.
:target: https://github.com/searxng
:alt: Organization
.. image:: https://img.shields.io/badge/documentation-3050ff?style=flat-square&logo=readthedocs&logoColor=fff&cacheSeconds=86400 A user_, admin_ and developer_ handbook is available on the homepage_.
:target: https://docs.searxng.org
:alt: Documentation
.. 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 :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 .. |Issues| image:: https://img.shields.io/github/issues/searxng/searxng?color=yellow&label=issues
:target: https://github.com/searxng/searxng/commits/master/ :target: https://github.com/searxng/searxng/issues
:alt: Commits
.. 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/ :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 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 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

View File

@@ -4,9 +4,7 @@
"extends": ["stylelint-config-standard-less"], "extends": ["stylelint-config-standard-less"],
"rules": { "rules": {
"at-rule-no-vendor-prefix": null, "at-rule-no-vendor-prefix": null,
"at-rule-prelude-no-invalid": null,
"declaration-empty-line-before": null, "declaration-empty-line-before": null,
"declaration-property-value-no-unknown": null,
"no-invalid-position-at-import-rule": null, "no-invalid-position-at-import-rule": null,
"prettier/prettier": true, "prettier/prettier": true,
"property-no-vendor-prefix": null, "property-no-vendor-prefix": null,

View File

@@ -1,23 +1,24 @@
{ {
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
"files": { "files": {
"ignoreUnknown": true, "includes": ["**", "!dist", "!node_modules"],
"includes": ["**", "!node_modules", "!src/brand", "!src/svg"] "ignoreUnknown": true
},
"vcs": {
"clientKind": "git",
"enabled": false,
"useIgnoreFile": true
}, },
"assist": { "assist": {
"enabled": true, "enabled": true,
"actions": { "actions": {
"preset": "recommended", "recommended": true
"source": {
"useSortedAttributes": "on",
"useSortedProperties": "on"
}
} }
}, },
"formatter": { "formatter": {
"enabled": true,
"bracketSameLine": false, "bracketSameLine": false,
"bracketSpacing": true, "bracketSpacing": true,
"enabled": true,
"formatWithErrors": false, "formatWithErrors": false,
"indentStyle": "space", "indentStyle": "space",
"indentWidth": 2, "indentWidth": 2,
@@ -27,29 +28,29 @@
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"preset": "recommended", "recommended": true,
"complexity": { "complexity": {
"noForEach": "error", "noForEach": "error",
"noImplicitCoercions": "error", "noImplicitCoercions": "error",
"noRedundantDefaultExport": "error",
"noUselessCatchBinding": "error",
"noUselessUndefined": "error",
"useArrayFind": "error",
"useSimplifiedLogicExpression": "error" "useSimplifiedLogicExpression": "error"
}, },
"correctness": { "correctness": {
"noGlobalDirnameFilename": "error", "noGlobalDirnameFilename": "error",
"noUndeclaredVariables": {
"level": "error",
"options": {
"checkTypes": true
}
},
"useImportExtensions": "error", "useImportExtensions": "error",
"useJsonImportAttributes": "error", "useJsonImportAttributes": "error",
"useSingleJsDocAsterisk": "error" "useSingleJsDocAsterisk": "error"
}, },
"nursery": { "nursery": {
"noFloatingPromises": "warn",
"noMisusedPromises": "warn", "noMisusedPromises": "warn",
"useAwaitThenable": "off", "noUselessUndefined": "warn",
"useExhaustiveSwitchCases": "warn", "useExhaustiveSwitchCases": "warn",
"useExplicitType": "off", "useExplicitType": "warn"
"useRegexpExec": "warn"
}, },
"performance": { "performance": {
"noAwaitInLoops": "error", "noAwaitInLoops": "error",
@@ -62,16 +63,23 @@
"style": { "style": {
"noCommonJs": "error", "noCommonJs": "error",
"noEnum": "error", "noEnum": "error",
"noImplicitBoolean": "error",
"noIncrementDecrement": "error",
"noInferrableTypes": "error", "noInferrableTypes": "error",
"noMultiAssign": "error",
"noMultilineString": "error",
"noNamespace": "error", "noNamespace": "error",
"noNegationElse": "error", "noNegationElse": "error",
"noNestedTernary": "error", "noNestedTernary": "error",
"noParameterAssign": "error", "noParameterAssign": "error",
"noParameterProperties": "error", "noParameterProperties": "error",
"noRestrictedTypes": {
"level": "error",
"options": {
"types": {
"Element": {
"message": "Element is too generic",
"use": "HTMLElement"
}
}
}
},
"noSubstr": "error", "noSubstr": "error",
"noUnusedTemplateLiteral": "error", "noUnusedTemplateLiteral": "error",
"noUselessElse": "error", "noUselessElse": "error",
@@ -87,7 +95,6 @@
} }
}, },
"useConsistentBuiltinInstantiation": "error", "useConsistentBuiltinInstantiation": "error",
"useConsistentEnumValueType": "error",
"useConsistentMemberAccessibility": { "useConsistentMemberAccessibility": {
"level": "error", "level": "error",
"options": { "options": {
@@ -100,14 +107,7 @@
"syntax": "explicit" "syntax": "explicit"
} }
}, },
"useConsistentTypeDefinitions": {
"level": "error",
"options": {
"style": "type"
}
},
"useDefaultSwitchClause": "error", "useDefaultSwitchClause": "error",
"useDestructuring": "error",
"useExplicitLengthCheck": "error", "useExplicitLengthCheck": "error",
"useForOf": "error", "useForOf": "error",
"useGroupedAccessorPairs": "error", "useGroupedAccessorPairs": "error",
@@ -115,7 +115,6 @@
"useNumericSeparators": "error", "useNumericSeparators": "error",
"useObjectSpread": "error", "useObjectSpread": "error",
"useReadonlyClassProperties": "error", "useReadonlyClassProperties": "error",
"useSelfClosingElements": "error",
"useShorthandAssign": "error", "useShorthandAssign": "error",
"useSingleVarDeclarator": "error", "useSingleVarDeclarator": "error",
"useThrowNewError": "error", "useThrowNewError": "error",
@@ -124,17 +123,11 @@
"useUnifiedTypeSignatures": "error" "useUnifiedTypeSignatures": "error"
}, },
"suspicious": { "suspicious": {
"noAlert": "error",
"noBitwiseOperators": "error", "noBitwiseOperators": "error",
"noConstantBinaryExpressions": "error", "noConstantBinaryExpressions": "error",
"noDeprecatedImports": "error",
"noEmptyBlockStatements": "error", "noEmptyBlockStatements": "error",
"noEqualsToNull": "error",
"noEvolvingTypes": "error", "noEvolvingTypes": "error",
"noForIn": "error",
"noImportCycles": "error",
"noNestedPromises": "error",
"noParametersOnlyUsedInRecursion": "error",
"noReturnAssign": "error",
"noUnassignedVariables": "error", "noUnassignedVariables": "error",
"noVar": "error", "noVar": "error",
"useNumberToFixedDigitsArgument": "error", "useNumberToFixedDigitsArgument": "error",

View File

@@ -2,7 +2,7 @@
/* /*
this file is generated automatically by searxng_extra/update/update_pygments.py this file is generated automatically by searxng_extra/update/update_pygments.py
using pygments version 2.20.0: using pygments version 2.19.2:
./manage templates.simple.pygments ./manage templates.simple.pygments
*/ */
@@ -10,7 +10,7 @@
.code-highlight { .code-highlight {
pre { line-height: 125%; } pre { line-height: 100%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } 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; } 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; } td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
@@ -89,89 +89,89 @@
.code-highlight-dark(){ .code-highlight-dark(){
.code-highlight { .code-highlight {
pre { line-height: 125%; } pre { line-height: 100%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } td.linenos .normal { color: #3c4354; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; 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: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } td.linenos .special { color: #3c4354; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; 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: #49483e } .hll { background-color: #6e7681 }
.c { color: #959077 } /* Comment */ .c { color: #7E8AA1 } /* Comment */
.err { color: #ED007E; background-color: #1E0010 } /* Error */ .err { color: #F88F7F } /* Error */
.esc { color: #F8F8F2 } /* Escape */ .esc { color: #D4D2C8 } /* Escape */
.g { color: #F8F8F2 } /* Generic */ .g { color: #D4D2C8 } /* Generic */
.k { color: #66D9EF } /* Keyword */ .k { color: #FFAD66 } /* Keyword */
.l { color: #AE81FF } /* Literal */ .l { color: #D5FF80 } /* Literal */
.n { color: #F8F8F2 } /* Name */ .n { color: #D4D2C8 } /* Name */
.o { color: #FF4689 } /* Operator */ .o { color: #FFAD66 } /* Operator */
.x { color: #F8F8F2 } /* Other */ .x { color: #D4D2C8 } /* Other */
.p { color: #F8F8F2 } /* Punctuation */ .p { color: #D4D2C8 } /* Punctuation */
.ch { color: #959077 } /* Comment.Hashbang */ .ch { color: #F88F7F; font-style: italic } /* Comment.Hashbang */
.cm { color: #959077 } /* Comment.Multiline */ .cm { color: #7E8AA1 } /* Comment.Multiline */
.cp { color: #959077 } /* Comment.Preproc */ .cp { color: #FFAD66; font-weight: bold } /* Comment.Preproc */
.cpf { color: #959077 } /* Comment.PreprocFile */ .cpf { color: #7E8AA1 } /* Comment.PreprocFile */
.c1 { color: #959077 } /* Comment.Single */ .c1 { color: #7E8AA1 } /* Comment.Single */
.cs { color: #959077 } /* Comment.Special */ .cs { color: #7E8AA1; font-style: italic } /* Comment.Special */
.gd { color: #FF4689 } /* Generic.Deleted */ .gd { color: #F88F7F; background-color: #3D1E20 } /* Generic.Deleted */
.ge { color: #F8F8F2; font-style: italic } /* Generic.Emph */ .ge { color: #D4D2C8; font-style: italic } /* Generic.Emph */
.ges { color: #F8F8F2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ .ges { color: #D4D2C8 } /* Generic.EmphStrong */
.gr { color: #F8F8F2 } /* Generic.Error */ .gr { color: #F88F7F } /* Generic.Error */
.gh { color: #F8F8F2 } /* Generic.Heading */ .gh { color: #D4D2C8 } /* Generic.Heading */
.gi { color: #A6E22E } /* Generic.Inserted */ .gi { color: #6AD4AF; background-color: #19362C } /* Generic.Inserted */
.go { color: #66D9EF } /* Generic.Output */ .go { color: #7E8AA1 } /* Generic.Output */
.gp { color: #FF4689; font-weight: bold } /* Generic.Prompt */ .gp { color: #D4D2C8 } /* Generic.Prompt */
.gs { color: #F8F8F2; font-weight: bold } /* Generic.Strong */ .gs { color: #D4D2C8; font-weight: bold } /* Generic.Strong */
.gu { color: #959077 } /* Generic.Subheading */ .gu { color: #D4D2C8 } /* Generic.Subheading */
.gt { color: #F8F8F2 } /* Generic.Traceback */ .gt { color: #F88F7F } /* Generic.Traceback */
.kc { color: #66D9EF } /* Keyword.Constant */ .kc { color: #FFAD66 } /* Keyword.Constant */
.kd { color: #66D9EF } /* Keyword.Declaration */ .kd { color: #FFAD66 } /* Keyword.Declaration */
.kn { color: #FF4689 } /* Keyword.Namespace */ .kn { color: #FFAD66 } /* Keyword.Namespace */
.kp { color: #66D9EF } /* Keyword.Pseudo */ .kp { color: #FFAD66 } /* Keyword.Pseudo */
.kr { color: #66D9EF } /* Keyword.Reserved */ .kr { color: #FFAD66 } /* Keyword.Reserved */
.kt { color: #66D9EF } /* Keyword.Type */ .kt { color: #73D0FF } /* Keyword.Type */
.ld { color: #E6DB74 } /* Literal.Date */ .ld { color: #D5FF80 } /* Literal.Date */
.m { color: #AE81FF } /* Literal.Number */ .m { color: #DFBFFF } /* Literal.Number */
.s { color: #E6DB74 } /* Literal.String */ .s { color: #D5FF80 } /* Literal.String */
.na { color: #A6E22E } /* Name.Attribute */ .na { color: #FFD173 } /* Name.Attribute */
.nb { color: #F8F8F2 } /* Name.Builtin */ .nb { color: #FFD173 } /* Name.Builtin */
.nc { color: #A6E22E } /* Name.Class */ .nc { color: #73D0FF } /* Name.Class */
.no { color: #66D9EF } /* Name.Constant */ .no { color: #FFD173 } /* Name.Constant */
.nd { color: #A6E22E } /* Name.Decorator */ .nd { color: #7E8AA1; font-weight: bold; font-style: italic } /* Name.Decorator */
.ni { color: #F8F8F2 } /* Name.Entity */ .ni { color: #95E6CB } /* Name.Entity */
.ne { color: #A6E22E } /* Name.Exception */ .ne { color: #73D0FF } /* Name.Exception */
.nf { color: #A6E22E } /* Name.Function */ .nf { color: #FFD173 } /* Name.Function */
.nl { color: #F8F8F2 } /* Name.Label */ .nl { color: #D4D2C8 } /* Name.Label */
.nn { color: #F8F8F2 } /* Name.Namespace */ .nn { color: #D4D2C8 } /* Name.Namespace */
.nx { color: #A6E22E } /* Name.Other */ .nx { color: #D4D2C8 } /* Name.Other */
.py { color: #F8F8F2 } /* Name.Property */ .py { color: #FFD173 } /* Name.Property */
.nt { color: #FF4689 } /* Name.Tag */ .nt { color: #5CCFE6 } /* Name.Tag */
.nv { color: #F8F8F2 } /* Name.Variable */ .nv { color: #D4D2C8 } /* Name.Variable */
.ow { color: #FF4689 } /* Operator.Word */ .ow { color: #FFAD66 } /* Operator.Word */
.pm { color: #F8F8F2 } /* Punctuation.Marker */ .pm { color: #D4D2C8 } /* Punctuation.Marker */
.w { color: #F8F8F2 } /* Text.Whitespace */ .w { color: #D4D2C8 } /* Text.Whitespace */
.mb { color: #AE81FF } /* Literal.Number.Bin */ .mb { color: #DFBFFF } /* Literal.Number.Bin */
.mf { color: #AE81FF } /* Literal.Number.Float */ .mf { color: #DFBFFF } /* Literal.Number.Float */
.mh { color: #AE81FF } /* Literal.Number.Hex */ .mh { color: #DFBFFF } /* Literal.Number.Hex */
.mi { color: #AE81FF } /* Literal.Number.Integer */ .mi { color: #DFBFFF } /* Literal.Number.Integer */
.mo { color: #AE81FF } /* Literal.Number.Oct */ .mo { color: #DFBFFF } /* Literal.Number.Oct */
.sa { color: #E6DB74 } /* Literal.String.Affix */ .sa { color: #F29E74 } /* Literal.String.Affix */
.sb { color: #E6DB74 } /* Literal.String.Backtick */ .sb { color: #D5FF80 } /* Literal.String.Backtick */
.sc { color: #E6DB74 } /* Literal.String.Char */ .sc { color: #D5FF80 } /* Literal.String.Char */
.dl { color: #E6DB74 } /* Literal.String.Delimiter */ .dl { color: #D5FF80 } /* Literal.String.Delimiter */
.sd { color: #E6DB74 } /* Literal.String.Doc */ .sd { color: #7E8AA1 } /* Literal.String.Doc */
.s2 { color: #E6DB74 } /* Literal.String.Double */ .s2 { color: #D5FF80 } /* Literal.String.Double */
.se { color: #AE81FF } /* Literal.String.Escape */ .se { color: #95E6CB } /* Literal.String.Escape */
.sh { color: #E6DB74 } /* Literal.String.Heredoc */ .sh { color: #D5FF80 } /* Literal.String.Heredoc */
.si { color: #E6DB74 } /* Literal.String.Interpol */ .si { color: #95E6CB } /* Literal.String.Interpol */
.sx { color: #E6DB74 } /* Literal.String.Other */ .sx { color: #95E6CB } /* Literal.String.Other */
.sr { color: #E6DB74 } /* Literal.String.Regex */ .sr { color: #95E6CB } /* Literal.String.Regex */
.s1 { color: #E6DB74 } /* Literal.String.Single */ .s1 { color: #D5FF80 } /* Literal.String.Single */
.ss { color: #E6DB74 } /* Literal.String.Symbol */ .ss { color: #DFBFFF } /* Literal.String.Symbol */
.bp { color: #F8F8F2 } /* Name.Builtin.Pseudo */ .bp { color: #5CCFE6 } /* Name.Builtin.Pseudo */
.fm { color: #A6E22E } /* Name.Function.Magic */ .fm { color: #FFD173 } /* Name.Function.Magic */
.vc { color: #F8F8F2 } /* Name.Variable.Class */ .vc { color: #D4D2C8 } /* Name.Variable.Class */
.vg { color: #F8F8F2 } /* Name.Variable.Global */ .vg { color: #D4D2C8 } /* Name.Variable.Global */
.vi { color: #F8F8F2 } /* Name.Variable.Instance */ .vi { color: #D4D2C8 } /* Name.Variable.Instance */
.vm { color: #F8F8F2 } /* Name.Variable.Magic */ .vm { color: #D4D2C8 } /* Name.Variable.Magic */
.il { color: #AE81FF } /* Literal.Number.Integer.Long */ .il { color: #DFBFFF } /* Literal.Number.Integer.Long */
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -19,31 +19,33 @@
"lint:tsc": "tsc --noEmit" "lint:tsc": "tsc --noEmit"
}, },
"browserslist": [ "browserslist": [
"baseline 2022", "Chrome >= 93",
"Firefox >= 92",
"Safari >= 15.4",
"not dead" "not dead"
], ],
"dependencies": { "dependencies": {
"ionicons": "^8.0.13", "ionicons": "~8.0.0",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"ol": "^10.9.0", "ol": "~10.6.0",
"swiped-events": "1.2.0" "swiped-events": "1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.0", "@biomejs/biome": "2.2.0",
"@types/node": "^26.0.0", "@types/node": "~24.3.0",
"browserslist": "^4.28.2", "browserslist": "~4.25.2",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "~2.1.0",
"edge.js": "^6.5.1", "edge.js": "~6.3.0",
"less": "^4.6.6", "less": "~4.4.0",
"mathjs": "^15.2.0", "lightningcss": "~1.30.0",
"sharp": "~0.35.1", "sharp": "~0.34.0",
"sort-package-json": "^4.0.0", "sort-package-json": "~3.4.0",
"stylelint": "^17.13.0", "stylelint": "~16.23.1",
"stylelint-config-standard-less": "^4.1.0", "stylelint-config-standard-less": "~3.0.0",
"stylelint-prettier": "^5.0.3", "stylelint-prettier": "~5.0.0",
"svgo": "^4.0.1", "svgo": "~4.0.0",
"typescript": "~6.0.3", "typescript": "~5.9.0",
"vite": "^8.0.16", "vite": "npm:rolldown-vite@7.1.3",
"vite-bundle-analyzer": "^1.3.8" "vite-bundle-analyzer": "~1.2.1"
} }
} }

View File

@@ -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>;
}

View File

@@ -0,0 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import "./nojs.ts";
import "./router.ts";
import "./toolkit.ts";
import "./listener.ts";

View File

@@ -0,0 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { listen } from "./toolkit.ts";
listen("click", ".close", function (this: HTMLElement) {
(this.parentNode as HTMLElement)?.classList.add("invisible");
});

View File

@@ -0,0 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { ready } from "./toolkit.ts";
ready(() => {
document.documentElement.classList.remove("no-js");
document.documentElement.classList.add("js");
});

View File

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

View File

@@ -1,15 +1,16 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import type { KeyBindingLayout } from "./main/keyboard.ts"; import type { KeyBindingLayout } from "../main/keyboard.ts";
// synced with searx/webapp.py get_client_settings // synced with searx/webapp.py get_client_settings
type Settings = { type Settings = {
plugins?: string[]; advanced_search?: boolean;
autocomplete?: string; autocomplete?: string;
autocomplete_min?: number; autocomplete_min?: number;
doi_resolver?: string; doi_resolver?: string;
favicon_resolver?: string; favicon_resolver?: string;
hotkeys?: KeyBindingLayout; hotkeys?: KeyBindingLayout;
infinite_scroll?: boolean;
method?: "GET" | "POST"; method?: "GET" | "POST";
query_in_title?: boolean; query_in_title?: boolean;
results_on_new_tab?: boolean; results_on_new_tab?: boolean;
@@ -31,6 +32,8 @@ type ReadyOptions = {
on?: (boolean | undefined)[]; on?: (boolean | undefined)[];
}; };
type AssertElement = (element?: HTMLElement | null) => asserts element is HTMLElement;
export type EndpointsKeys = keyof typeof Endpoints; export type EndpointsKeys = keyof typeof Endpoints;
export const Endpoints = { export const Endpoints = {
@@ -70,6 +73,12 @@ const getSettings = (): Settings => {
} }
}; };
export const assertElement: AssertElement = (element?: HTMLElement | null): asserts element is HTMLElement => {
if (!element) {
throw new Error("Bad assertion: DOM element not found");
}
};
export const http = async (method: string, url: string | URL, options?: HTTPOptions): Promise<Response> => { export const http = async (method: string, url: string | URL, options?: HTTPOptions): Promise<Response> => {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), options?.timeout ?? 30_000); const timeoutId = setTimeout(() => controller.abort(), options?.timeout ?? 30_000);
@@ -122,10 +131,10 @@ export const ready = (callback: () => void, options?: ReadyOptions): void => {
} }
} }
if (document.readyState === "loading") { if (document.readyState !== "loading") {
listen("DOMContentLoaded", document, callback, { once: true });
} else {
callback(); callback();
} else {
listen("DOMContentLoaded", document, callback, { once: true });
} }
}; };

View File

@@ -1,4 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// core
void import.meta.glob(["./*.ts", "./util/**/.ts"], { eager: true });

View File

@@ -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;
}
}
};

View File

@@ -1,7 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import { http, listen, settings } from "../toolkit.ts"; import { assertElement, http, listen, settings } from "../core/toolkit.ts";
import { assertElement } from "../util/assertElement.ts";
const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => { const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
try { try {
@@ -44,6 +43,8 @@ const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<vo
const form = document.querySelector<HTMLFormElement>("#search"); const form = document.querySelector<HTMLFormElement>("#search");
form?.submit(); form?.submit();
autocomplete.classList.remove("open");
}); });
fragment.append(li); fragment.append(li);
@@ -78,11 +79,6 @@ listen("input", qInput, () => {
const autocomplete: HTMLElement | null = document.querySelector<HTMLElement>(".autocomplete"); const autocomplete: HTMLElement | null = document.querySelector<HTMLElement>(".autocomplete");
const autocompleteList: HTMLUListElement | null = document.querySelector<HTMLUListElement>(".autocomplete ul"); const autocompleteList: HTMLUListElement | null = document.querySelector<HTMLUListElement>(".autocomplete ul");
if (autocompleteList) { if (autocompleteList) {
listen("keydown", qInput, (event: KeyboardEvent) => {
if (event.key === "Escape") {
autocomplete?.classList.remove("open");
}
});
listen("keyup", qInput, (event: KeyboardEvent) => { listen("keyup", qInput, (event: KeyboardEvent) => {
const listItems = [...autocompleteList.children] as HTMLElement[]; const listItems = [...autocompleteList.children] as HTMLElement[];
@@ -108,6 +104,7 @@ if (autocompleteList) {
newCurrentIndex = (currentIndex + 1) % listItems.length; newCurrentIndex = (currentIndex + 1) % listItems.length;
break; break;
} }
case "Tab":
case "Enter": case "Enter":
if (autocomplete) { if (autocomplete) {
autocomplete.classList.remove("open"); autocomplete.classList.remove("open");
@@ -131,12 +128,4 @@ if (autocompleteList) {
} }
} }
}); });
listen("blur", qInput, () => {
autocomplete?.classList.remove("open");
});
listen("focus", qInput, () => {
autocomplete?.classList.add("open");
});
} }

View File

@@ -0,0 +1,100 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { assertElement, http, settings } from "../core/toolkit.ts";
const newLoadSpinner = (): HTMLDivElement => {
return Object.assign(document.createElement("div"), {
className: "loader"
});
};
const loadNextPage = async (onlyImages: boolean, 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(newLoadSpinner());
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(...Array.from(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 resultsElement: HTMLElement | null = document.getElementById("results");
if (!resultsElement) {
throw new Error("Results element not found");
}
const onlyImages: boolean = resultsElement.classList.contains("only_template_images");
const observedSelector = "article.result:last-child";
const intersectionObserveOptions: IntersectionObserverInit = {
rootMargin: "320px"
};
const observer: IntersectionObserver = new IntersectionObserver((entries: IntersectionObserverEntry[]) => {
const [paginationEntry] = entries;
if (paginationEntry?.isIntersecting) {
observer.unobserve(paginationEntry.target);
loadNextPage(onlyImages, () => {
const nextObservedElement = document.querySelector<HTMLElement>(observedSelector);
if (nextObservedElement) {
observer.observe(nextObservedElement);
}
}).then(() => {
// wait until promise is resolved
});
}
}, intersectionObserveOptions);
const initialObservedElement: HTMLElement | null = document.querySelector<HTMLElement>(observedSelector);
if (initialObservedElement) {
observer.observe(initialObservedElement);
}

View File

@@ -1,7 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import { listen, mutable, settings } from "../toolkit.ts"; import { assertElement, listen, mutable, settings } from "../core/toolkit.ts";
import { assertElement } from "../util/assertElement.ts";
export type KeyBindingLayout = "default" | "vim"; export type KeyBindingLayout = "default" | "vim";
@@ -190,7 +189,9 @@ const highlightResult =
let next: HTMLElement | undefined; let next: HTMLElement | undefined;
if (typeof effectiveWhich === "string") { if (typeof effectiveWhich !== "string") {
next = effectiveWhich;
} else {
switch (effectiveWhich) { switch (effectiveWhich) {
case "visible": { case "visible": {
const top = document.documentElement.scrollTop || document.body.scrollTop; const top = document.documentElement.scrollTop || document.body.scrollTop;
@@ -218,10 +219,8 @@ const highlightResult =
// biome-ignore lint/complexity/noUselessSwitchCase: fallthrough is intended // biome-ignore lint/complexity/noUselessSwitchCase: fallthrough is intended
case "top": case "top":
default: default:
[next] = results; next = results[0];
} }
} else {
next = effectiveWhich;
} }
if (next) { if (next) {
@@ -343,7 +342,7 @@ const initHelpContent = (divElement: HTMLElement, keyBindings: typeof baseKeyBin
const categories: Record<string, KeyBinding[]> = {}; const categories: Record<string, KeyBinding[]> = {};
for (const binding of Object.values(keyBindings)) { for (const binding of Object.values(keyBindings)) {
const { cat } = binding; const cat = binding.cat;
categories[cat] ??= []; categories[cat] ??= [];
categories[cat].push(binding); categories[cat].push(binding);
} }
@@ -400,7 +399,7 @@ const toggleHelp = (keyBindings: typeof baseKeyBinding): void => {
className: "dialog-modal" className: "dialog-modal"
}); });
initHelpContent(helpPanel, keyBindings); initHelpContent(helpPanel, keyBindings);
const [body] = document.getElementsByTagName("body"); const body = document.getElementsByTagName("body")[0];
if (body) { if (body) {
body.appendChild(helpPanel); body.appendChild(helpPanel);
} }
@@ -408,31 +407,12 @@ const toggleHelp = (keyBindings: typeof baseKeyBinding): void => {
}; };
const copyURLToClipboard = async (): Promise<void> => { const copyURLToClipboard = async (): Promise<void> => {
const selectedResult = document.querySelector<HTMLElement>(".result[data-vim-selected]"); const currentUrlElement = document.querySelector<HTMLAnchorElement>(".result[data-vim-selected] h3 a");
if (!selectedResult) return; assertElement(currentUrlElement);
const resultAnchor = selectedResult.querySelector<HTMLAnchorElement>("a"); const url = currentUrlElement.getAttribute("href");
assertElement(resultAnchor);
const url = resultAnchor.getAttribute("href");
if (url) { if (url) {
if (window.isSecureContext) {
await navigator.clipboard.writeText(url); 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();
}
}
} }
}; };

View File

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

View File

@@ -1,7 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import { http, listen, settings } from "../toolkit.ts"; import { http, listen, settings } from "../core/toolkit.ts";
import { assertElement } from "../util/assertElement.ts";
let engineDescriptions: Record<string, [string, string]> | undefined; let engineDescriptions: Record<string, [string, string]> | undefined;
@@ -53,24 +52,19 @@ for (const engine of disableAllEngines) {
listen("click", engine, () => toggleEngines(false, engineToggles)); listen("click", engine, () => toggleEngines(false, engineToggles));
} }
listen("click", "#copy-hash", async function (this: HTMLElement) { const copyHashButton: HTMLElement | null = document.querySelector<HTMLElement>("#copy-hash");
const target = this.parentElement?.querySelector<HTMLPreElement>("pre"); if (copyHashButton) {
assertElement(target); listen("click", copyHashButton, async (event: Event) => {
event.preventDefault();
if (window.isSecureContext) { const { copiedText, hash } = copyHashButton.dataset;
await navigator.clipboard.writeText(target.innerText); if (!(copiedText && hash)) return;
} 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) { try {
this.innerText = this.dataset.copiedText; await navigator.clipboard.writeText(hash);
copyHashButton.innerText = copiedText;
} catch (error) {
console.error("Failed to copy hash:", error);
} }
}); });
}

View File

@@ -1,8 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import "../../../node_modules/swiped-events/src/swiped-events.js"; import "../../../node_modules/swiped-events/src/swiped-events.js";
import { listen, mutable, settings } from "../toolkit.ts"; import { assertElement, listen, mutable, settings } from "../core/toolkit.ts";
import { assertElement } from "../util/assertElement.ts";
let imgTimeoutID: number; let imgTimeoutID: number;
@@ -122,21 +121,10 @@ listen("click", "#copy_url", async function (this: HTMLElement) {
const target = this.parentElement?.querySelector<HTMLPreElement>("pre"); const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
assertElement(target); assertElement(target);
if (window.isSecureContext) {
await navigator.clipboard.writeText(target.innerText); await navigator.clipboard.writeText(target.innerText);
} else { const copiedText = this.dataset.copiedText;
const selection = window.getSelection(); if (copiedText) {
if (selection) { this.innerText = copiedText;
const range = document.createRange();
range.selectNodeContents(target);
selection.removeAllRanges();
selection.addRange(range);
document.execCommand("copy");
}
}
if (this.dataset.copiedText) {
this.innerText = this.dataset.copiedText;
} }
}); });

View File

@@ -1,51 +1,88 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import { listen } from "../toolkit.ts"; import { assertElement, listen, settings } from "../core/toolkit.ts";
import { getElement } from "../util/getElement.ts";
const searchForm: HTMLFormElement = getElement<HTMLFormElement>("search"); const submitIfQuery = (qInput: HTMLInputElement): void => {
const searchInput: HTMLInputElement = getElement<HTMLInputElement>("q"); if (qInput.value.length > 0) {
const searchReset: HTMLButtonElement = getElement<HTMLButtonElement>("clear_search"); const search = document.getElementById("search") as HTMLFormElement | null;
search?.submit();
}
};
const updateClearButton = (qInput: HTMLInputElement, cs: HTMLElement): void => {
cs.classList.toggle("empty", qInput.value.length === 0);
};
const createClearButton = (qInput: HTMLInputElement): void => {
const cs = document.getElementById("clear_search");
assertElement(cs);
updateClearButton(qInput, cs);
listen("click", cs, (event: MouseEvent) => {
event.preventDefault();
qInput.value = "";
qInput.focus();
updateClearButton(qInput, cs);
});
listen("input", qInput, () => updateClearButton(qInput, cs), { passive: true });
};
const qInput = document.getElementById("q") as HTMLInputElement | null;
assertElement(qInput);
const isMobile: boolean = window.matchMedia("(max-width: 50em)").matches; const isMobile: boolean = window.matchMedia("(max-width: 50em)").matches;
const isResultsPage: boolean = document.querySelector("main")?.id === "main_results"; 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 // focus search input on large screens
if (!(isMobile || isResultsPage)) { if (!(isMobile || isResultsPage)) {
searchInput.focus(); qInput.focus();
} }
// On mobile, move cursor to the end of the input on focus // On mobile, move cursor to the end of the input on focus
if (isMobile) { if (isMobile) {
listen("focus", searchInput, () => { listen("focus", qInput, () => {
// Defer cursor move until the next frame to prevent a visual jump // Defer cursor move until the next frame to prevent a visual jump
requestAnimationFrame(() => { requestAnimationFrame(() => {
const end = searchInput.value.length; const end = qInput.value.length;
searchInput.setSelectionRange(end, end); qInput.setSelectionRange(end, end);
searchInput.scrollLeft = searchInput.scrollWidth; qInput.scrollLeft = qInput.scrollWidth;
}); });
}); });
} }
listen("input", searchInput, () => { createClearButton(qInput);
searchReset.classList.toggle("empty", searchInput.value.length === 0);
});
listen("click", searchReset, (event: MouseEvent) => { // Additionally to searching when selecting a new category, we also
event.preventDefault(); // automatically start a new search request when the user changes a search
searchInput.value = ""; // filter (safesearch, time range or language) (this requires JavaScript
searchInput.focus(); // though)
searchReset.classList.add("empty"); if (
}); settings.search_on_category_select &&
// If .search_filters is undefined (invisible) we are on the homepage and
// hence don't have to set any listeners
document.querySelector(".search_filters")
) {
const safesearchElement = document.getElementById("safesearch");
if (safesearchElement) {
listen("change", safesearchElement, () => submitIfQuery(qInput));
}
const timeRangeElement = document.getElementById("time_range");
if (timeRangeElement) {
listen("change", timeRangeElement, () => submitIfQuery(qInput));
}
const languageElement = document.getElementById("language");
if (languageElement) {
listen("change", languageElement, () => submitIfQuery(qInput));
}
}
const categoryButtons: HTMLButtonElement[] = [
...document.querySelectorAll<HTMLButtonElement>("button.category_button")
];
for (const button of categoryButtons) { for (const button of categoryButtons) {
listen("click", button, (event: MouseEvent) => { listen("click", button, (event: MouseEvent) => {
if (event.shiftKey) { if (event.shiftKey) {
@@ -61,34 +98,21 @@ for (const button of categoryButtons) {
}); });
} }
if (document.querySelector("div.search_filters")) { const form: HTMLFormElement | null = document.querySelector<HTMLFormElement>("#search");
const safesearchElement = document.getElementById("safesearch"); assertElement(form);
if (safesearchElement) {
listen("change", safesearchElement, () => searchForm.submit());
}
const timeRangeElement = document.getElementById("time_range"); // override form submit action to update the actually selected categories
if (timeRangeElement) { listen("submit", form, (event: Event) => {
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(); event.preventDefault();
if (categoryButtons.length > 0) { const categoryValuesInput = document.querySelector<HTMLInputElement>("#selected-categories");
const searchCategories = getElement<HTMLInputElement>("selected-categories"); if (categoryValuesInput) {
searchCategories.value = categoryButtons const categoryValues = categoryButtons
.filter((button) => button.classList.contains("selected")) .filter((button) => button.classList.contains("selected"))
.map((button) => button.name.replace("category_", "")) .map((button) => button.name.replace("category_", ""));
.join(",");
categoryValuesInput.value = categoryValues.join(",");
} }
searchForm.submit(); form.submit();
}); });

View File

@@ -0,0 +1,28 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { Feature, Map as OlMap, View } from "ol";
import { createEmpty } from "ol/extent";
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";
export {
View,
OlMap,
TileLayer,
VectorLayer,
OSM,
createEmpty,
VectorSource,
Style,
Stroke,
Fill,
Circle,
fromLonLat,
GeoJSON,
Feature,
Point
};

View File

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

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

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

View File

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

View File

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

View File

@@ -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;
}

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
@import "../../../generated/pygments.less"; @import "../../generated/pygments.less";
.codelines { .codelines {
margin: @results-margin 0 0 0; margin: @results-margin 0 0 0;
@@ -30,12 +30,6 @@
span.linenos { span.linenos {
color: var(--color-line-number); color: var(--color-line-number);
} }
.err {
// The code view in the result list should not act as a code-checker.
border: none;
color: inherit;
}
} }
} }

View File

@@ -1,16 +1,19 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
iframe[src^="https://w.soundcloud.com"] { iframe[src^="https://w.soundcloud.com"]
{
height: 120px; 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 real size is 92px, but 94px are needed to avoid an inner scrollbar of
// the embedded HTML. // the embedded HTML.
height: 94px; 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 // 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 // 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 // request. The alternative text (<img alt='Mixcloud Logo'> then cause an
@@ -20,16 +23,19 @@ iframe[src^="https://www.mixcloud.com"] {
height: 250px; height: 250px;
} }
iframe[src^="https://bandcamp.com/EmbeddedPlayer"] { iframe[src^="https://bandcamp.com/EmbeddedPlayer"]
{
// show playlist // show playlist
height: 350px; height: 350px;
} }
iframe[src^="https://bandcamp.com/EmbeddedPlayer/track"] { iframe[src^="https://bandcamp.com/EmbeddedPlayer/track"]
{
// hide playlist // hide playlist
height: 120px; height: 120px;
} }
iframe[src^="https://genius.com/songs"] { iframe[src^="https://genius.com/songs"]
{
height: 65px; height: 65px;
} }

View File

@@ -8,7 +8,7 @@
text-align: center; text-align: center;
.title { .title {
background: url("./img/searxng.png") no-repeat; background: url("../img/searxng.png") no-repeat;
min-height: 4rem; min-height: 4rem;
margin: 4rem auto; margin: 4rem auto;
background-position: center; background-position: center;

View File

@@ -0,0 +1,38 @@
// 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;
}
}

View File

@@ -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;
}
}

View File

@@ -17,7 +17,7 @@
} }
table { table {
word-break: break-all; word-break: break-word;
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
background-color: var(--color-result-keyvalue-table); background-color: var(--color-result-keyvalue-table);

View File

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

View File

@@ -178,6 +178,7 @@ html.no-js #clear_search.hide_if_nojs {
#send_search { #send_search {
display: block; display: block;
margin: 0; margin: 0;
padding: 0.8rem;
background: none repeat scroll 0 0 var(--color-search-background); background: none repeat scroll 0 0 var(--color-search-background);
border: none; border: none;
outline: none; outline: none;
@@ -195,7 +196,6 @@ html.no-js #clear_search.hide_if_nojs {
#send_search { #send_search {
.ltr-rounded-right-corners(0.8rem); .ltr-rounded-right-corners(0.8rem);
padding: 0.8rem;
&:hover { &:hover {
cursor: pointer; cursor: pointer;

View File

@@ -5,11 +5,14 @@
@import (inline) "../../node_modules/normalize.css/normalize.css"; @import (inline) "../../node_modules/normalize.css/normalize.css";
@import "definitions.less"; @import "definitions.less";
@import "mixins.less"; @import "mixins.less";
@import "code.less";
@import "toolkit.less"; @import "toolkit.less";
@import "autocomplete.less"; @import "autocomplete.less";
@import "detail.less";
@import "animations.less"; @import "animations.less";
@import "embedded.less"; @import "embedded.less";
@import "info.less"; @import "info.less";
@import "new_issue.less";
@import "stats.less"; @import "stats.less";
@import "result_templates.less"; @import "result_templates.less";
@import "weather.less"; @import "weather.less";
@@ -161,22 +164,12 @@ article[data-vim-selected].category-videos,
article[data-vim-selected].category-news, article[data-vim-selected].category-news,
article[data-vim-selected].category-map, article[data-vim-selected].category-map,
article[data-vim-selected].category-music, article[data-vim-selected].category-music,
article[data-vim-selected].category-files,
article[data-vim-selected].category-social { article[data-vim-selected].category-social {
border: 1px solid var(--color-result-vim-arrow); border: 1px solid var(--color-result-vim-arrow);
.rounded-corners; .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 { .result {
margin: @results-margin 0; margin: @results-margin 0;
padding: @result-padding; padding: @result-padding;
@@ -186,7 +179,7 @@ article[data-vim-selected].category-social {
h3 { h3 {
font-size: 1.2rem; font-size: 1.2rem;
overflow-wrap: break-word; word-wrap: break-word;
margin: 0.4rem 0 0.4rem 0; margin: 0.4rem 0 0.4rem 0;
padding: 0; padding: 0;
@@ -220,7 +213,7 @@ article[data-vim-selected].category-social {
margin: 0; margin: 0;
padding: 0; padding: 0;
max-width: 54em; max-width: 54em;
overflow-wrap: break-word; word-wrap: break-word;
line-height: 1.24; line-height: 1.24;
.highlight { .highlight {
@@ -303,22 +296,12 @@ article[data-vim-selected].category-social {
color: var(--color-result-description-highlight-font); color: var(--color-result-description-highlight-font);
} }
a.thumbnail_link {
position: relative;
margin-top: 0.6rem;
.ltr-margin-right(1rem);
.ltr-float-left();
img.thumbnail { img.thumbnail {
.ltr-float-left();
padding-top: 0.6rem;
.ltr-padding-right(1rem);
width: 7rem; width: 7rem;
height: unset; // remove height value that was needed for lazy loading height: unset; // remove height value that was needed for lazy loading
display: block;
}
.thumbnail_length {
.image-label-bottom-right();
right: 6px;
}
} }
.break { .break {
@@ -326,11 +309,11 @@ article[data-vim-selected].category-social {
} }
} }
.result-paper,
.result-packages { .result-packages {
.attributes { .attributes {
display: table; display: table;
border-spacing: 0.125rem; border-spacing: 0.125rem;
margin-top: 0.3rem;
div { div {
display: table-row; display: table-row;
@@ -364,12 +347,18 @@ article[data-vim-selected].category-social {
font-size: 0.9rem; font-size: 0.9rem;
margin: 0.25rem 0 0 0; margin: 0.25rem 0 0 0;
padding: 0; padding: 0;
overflow-wrap: break-word; word-wrap: break-word;
line-height: 1.24; line-height: 1.24;
font-style: italic; font-style: italic;
} }
} }
.result-packages {
.attributes {
margin-top: 0.3rem;
}
}
.template_group_images { .template_group_images {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -384,6 +373,7 @@ article[data-vim-selected].category-social {
.category-news, .category-news,
.category-map, .category-map,
.category-music, .category-music,
.category-files,
.category-social { .category-social {
border: 1px solid var(--color-result-border); border: 1px solid var(--color-result-border);
margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important; margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important;
@@ -408,19 +398,23 @@ article[data-vim-selected].category-social {
} }
.result-videos { .result-videos {
a.thumbnail_link img.thumbnail { img.thumbnail {
.ltr-float-left();
padding-top: 0.6rem;
.ltr-padding-right(1rem);
width: 20rem; width: 20rem;
height: unset; // remove height value that was needed for lazy loading
} }
}
.content { .result-videos .content {
overflow: hidden; overflow: hidden;
} }
.embedded-video iframe { .result-videos .embedded-video iframe {
width: 100%; width: 100%;
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;
padding: 10px 0 0 0; padding: 10px 0 0 0;
}
} }
@supports not (aspect-ratio: 1 / 1) { @supports not (aspect-ratio: 1 / 1) {
@@ -485,7 +479,14 @@ article[data-vim-selected].category-social {
} }
.image_resolution { .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, span.title,
@@ -569,16 +570,13 @@ article[data-vim-selected].category-social {
#suggestions { #suggestions {
.wrapper { .wrapper {
padding-left: 0; display: flex;
margin: 0; flex-flow: column;
list-style-position: inside; justify-content: flex-end;
li::marker {
color: var(--color-result-link-font);
}
form { form {
display: inline-block; display: inline-block;
flex: 1 1 50%;
} }
} }
} }
@@ -586,8 +584,8 @@ article[data-vim-selected].category-social {
#suggestions, #suggestions,
#infoboxes { #infoboxes {
input { input {
padding: 3px; padding: 0;
margin: 0; margin: 3px;
font-size: 0.9em; font-size: 0.9em;
display: inline-block; display: inline-block;
background: transparent; background: transparent;
@@ -689,7 +687,7 @@ summary.title {
#sidebar { #sidebar {
grid-area: sidebar; grid-area: sidebar;
overflow-wrap: break-word; word-wrap: break-word;
color: var(--color-sidebar-font); color: var(--color-sidebar-font);
.infobox { .infobox {
@@ -1030,6 +1028,10 @@ summary.title {
/ 100%; / 100%;
gap: 0; gap: 0;
#sidebar {
display: none;
}
#urls { #urls {
margin: 0; margin: 0;
display: flex; display: flex;
@@ -1116,6 +1118,7 @@ summary.title {
display: none; display: none;
} }
.result-paper,
.result-packages { .result-packages {
.attributes { .attributes {
display: block; display: block;
@@ -1161,7 +1164,3 @@ pre code {
// import layouts of the Result types // import layouts of the Result types
@import "result_types/keyvalue.less"; @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";

View File

@@ -193,15 +193,6 @@ div.selectable_url {
border-color: var(--color-warning); 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-modal {
.dialog(); .dialog();
@@ -610,7 +601,7 @@ td:hover .engine-tooltip,
.stacked-bar-chart-base(); .stacked-bar-chart-base();
background: var(--color-base-font); 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; padding: 0.3rem 0;
} }
@@ -618,7 +609,7 @@ td:hover .engine-tooltip,
.stacked-bar-chart-base(); .stacked-bar-chart-base();
background: transparent; 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; padding: 0.3rem 0;
} }
@@ -626,7 +617,7 @@ td:hover .engine-tooltip,
.stacked-bar-chart-base(); .stacked-bar-chart-base();
background: transparent; 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; padding: 0;
} }
@@ -634,7 +625,7 @@ td:hover .engine-tooltip,
.stacked-bar-chart-base(); .stacked-bar-chart-base();
background: transparent; 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; padding: 0.4rem 0;
width: 1px; width: 1px;
} }

View File

@@ -18,10 +18,8 @@ export type Src2Dest = {
* Convert a list of SVG files to PNG. * Convert a list of SVG files to PNG.
* *
* @param items - Array of SVG files (src: SVG, dest:PNG) to convert. * @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 => { export const svg2png = (items: Src2Dest[]): void => {
for (const item of items) { for (const item of items) {
fs.mkdirSync(path.dirname(item.dest), { recursive: true }); fs.mkdirSync(path.dirname(item.dest), { recursive: true });
@@ -31,9 +29,6 @@ export const svg2png = (items: Src2Dest[], width?: number, height?: number): voi
compressionLevel: 9, compressionLevel: 9,
palette: true palette: true
}) })
.resize(width, height, {
fit: "contain"
})
.toFile(item.dest) .toFile(item.dest)
.then((info) => { .then((info) => {
console.log(`[svg2png] created ${item.dest} -- bytes: ${info.size}, w:${info.width}px, h:${info.height}px`); console.log(`[svg2png] created ${item.dest} -- bytes: ${info.size}, w:${info.width}px, h:${info.height}px`);

View File

@@ -4,7 +4,7 @@
* Custom vite plugins to build the web-client components of the simple theme. * Custom vite plugins to build the web-client components of the simple theme.
* *
* HINT: * HINT:
* This is an initial implementation for the migration of the build process * 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 * from grunt to vite. For fully support (vite: build & serve) more work is
* needed. * needed.
*/ */
@@ -17,15 +17,13 @@ import { type Src2Dest, svg2png, svg2svg } from "./img.ts";
* Vite plugin to convert a list of SVG files to PNG. * Vite plugin to convert a list of SVG files to PNG.
* *
* @param items - Array of SVG files (src: SVG, dest:PNG) to convert. * @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 => { export const plg_svg2png = (items: Src2Dest[]): Plugin => {
return { return {
name: "searxng-simple-svg2png", name: "searxng-simple-svg2png",
apply: "build", apply: "build",
writeBundle: () => { writeBundle: () => {
svg2png(items, width, height); svg2png(items);
} }
}; };
}; };

View File

@@ -31,6 +31,7 @@
"noUnusedParameters": true, "noUnusedParameters": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"baseUrl": ".",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"types": ["vite/client"] "types": ["vite/client"]
}, },

View File

@@ -34,49 +34,56 @@ const svg2svg_favicon_opts: Config = {
}; };
export default { export default {
base: "./", base: "/static/themes/simple/",
publicDir: "static/", publicDir: "static/",
mode: "production",
// mode: "development",
build: { build: {
target: browserslistToEsbuild(manifest.browserslist), target: browserslistToEsbuild(manifest.browserslist),
assetsDir: "", cssTarget: browserslistToEsbuild(manifest.browserslist),
outDir: PATH.dist,
manifest: "manifest.json", manifest: "manifest.json",
emptyOutDir: true, emptyOutDir: true,
sourcemap: true, assetsDir: "",
rolldownOptions: { outDir: PATH.dist,
input: {
// entrypoint
core: `${PATH.src}/js/index.ts`,
// stylesheets sourcemap: true,
ltr: `${PATH.src}/less/style-ltr.less`,
rtl: `${PATH.src}/less/style-rtl.less`, rollupOptions: {
rss: `${PATH.src}/less/rss.less` input: {
// build CSS files
"searxng-ltr.css": `${PATH.src}/less/style-ltr.less`,
"searxng-rtl.css": `${PATH.src}/less/style-rtl.less`,
"rss.css": `${PATH.src}/less/rss.less`,
// build script files
"searxng.core": `${PATH.src}/js/core/index.ts`,
// ol
ol: `${PATH.src}/js/pkg/ol.ts`,
"ol.css": `${PATH.modules}/ol/ol.css`
}, },
// file naming conventions / pathnames are relative to outDir (PATH.dist) // file naming conventions / pathnames are relative to outDir (PATH.dist)
output: { output: {
entryFileNames: "sxng-[name].min.js", entryFileNames: "js/[name].min.js",
chunkFileNames: "chunk/[hash].min.js", chunkFileNames: "js/[name].min.js",
assetFileNames: ({ names }: PreRenderedAsset): string => { assetFileNames: ({ names }: PreRenderedAsset): string => {
const [name] = names; const [name] = names;
switch (name?.split(".").pop()) { const extension = name?.split(".").pop();
switch (extension) {
case "css": case "css":
return "sxng-[name].min[extname]"; return "css/[name].min[extname]";
case "js":
return "js/[name].min[extname]";
case "png":
case "svg":
return "img/[name][extname]";
default: default:
return "sxng-[name][extname]"; console.warn("Unknown asset:", name);
return "[name][extname]";
} }
},
sanitizeFileName: (name: string): string => {
return name
.normalize("NFD")
.replace(/[^a-zA-Z0-9.-]/g, "_")
.toLowerCase();
},
comments: {
legal: true
} }
} }
} }
@@ -104,90 +111,37 @@ export default {
// -- svg images // -- svg images
plg_svg2svg( plg_svg2svg(
[ [
{ { src: `${PATH.src}/svg/empty_favicon.svg`, dest: `${PATH.dist}/img/empty_favicon.svg` },
src: `${PATH.src}/svg/empty_favicon.svg`, { src: `${PATH.src}/svg/select-dark.svg`, dest: `${PATH.dist}/img/select-dark.svg` },
dest: `${PATH.dist}/img/empty_favicon.svg` { src: `${PATH.src}/svg/select-light.svg`, dest: `${PATH.dist}/img/select-light.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 svg2svg_opts
), ),
// SearXNG brand (static) // SearXNG brand (static)
plg_svg2png([ plg_svg2png([
{ { src: `${PATH.brand}/searxng-wordmark.svg`, dest: `${PATH.dist}/img/favicon.png` },
src: `${PATH.brand}/searxng-wordmark.svg`, { src: `${PATH.brand}/searxng.svg`, dest: `${PATH.dist}/img/searxng.png` }
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 // -- svg
plg_svg2svg( plg_svg2svg(
[ [
{ { src: `${PATH.brand}/searxng.svg`, dest: `${PATH.dist}/img/searxng.svg` },
src: `${PATH.brand}/searxng.svg`, { src: `${PATH.brand}/img_load_error.svg`, dest: `${PATH.dist}/img/img_load_error.svg` }
dest: `${PATH.dist}/img/searxng.svg`
},
{
src: `${PATH.brand}/img_load_error.svg`,
dest: `${PATH.dist}/img/img_load_error.svg`
}
], ],
svg2svg_opts svg2svg_opts
), ),
// -- favicon // -- favicon
plg_svg2svg( plg_svg2svg(
[ [{ src: `${PATH.brand}/searxng-wordmark.svg`, dest: `${PATH.dist}/img/favicon.svg` }],
{
src: `${PATH.brand}/searxng-wordmark.svg`,
dest: `${PATH.dist}/img/favicon.svg`
}
],
svg2svg_favicon_opts svg2svg_favicon_opts
), ),
// -- simple templates // -- simple templates
plg_svg2svg( plg_svg2svg(
[ [{ src: `${PATH.brand}/searxng-wordmark.svg`, dest: `${PATH.templates}/searxng-wordmark.min.svg` }],
{
src: `${PATH.brand}/searxng-wordmark.svg`,
dest: `${PATH.templates}/searxng-wordmark.min.svg`
}
],
svg2svg_opts svg2svg_opts
) )
], // end: plugins ], // end: plugins
@@ -206,5 +160,9 @@ export default {
targets: browserslistToTargets(manifest.browserslist) targets: browserslistToTargets(manifest.browserslist)
}, },
devSourcemap: true devSourcemap: true
} // end: css }, // end: css
experimental: {
enableNativePlugin: true
} // end: experimental
} satisfies UserConfig; } satisfies UserConfig;

View File

@@ -1,15 +0,0 @@
# Read the documentation before using the `docker-compose.yml` file:
# https://docs.searxng.org/admin/installation-docker.html
#
# Additional ENVs:
# https://docs.searxng.org/admin/settings/settings_general.html#settings-general
# https://docs.searxng.org/admin/settings/settings_server.html#settings-server
# Use a specific version tag. E.g. "latest" or "2026.3.25-541c6c3cb".
#SEARXNG_VERSION=latest
# Listen to a specific address.
#SEARXNG_HOST=[::]
# Listen to a specific port.
#SEARXNG_PORT=8080

View File

@@ -0,0 +1,25 @@
contents:
repositories:
- https://dl-cdn.alpinelinux.org/alpine/edge/main
- https://dl-cdn.alpinelinux.org/alpine/edge/community
packages:
- alpine-base
- build-base
- python3-dev
- py3-pip
- brotli
entrypoint:
command: /bin/sh -l
work-dir: /usr/local/searxng/
environment:
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
HISTFILE: /dev/null
archs:
- x86_64
- aarch64
- armv7

58
container/base.yml Normal file
View File

@@ -0,0 +1,58 @@
contents:
repositories:
- https://dl-cdn.alpinelinux.org/alpine/edge/main
packages:
- alpine-baselayout
- ca-certificates-bundle
- busybox
- python3
# healthcheck
- wget
entrypoint:
command: /bin/sh -l
work-dir: /usr/local/searxng/
accounts:
groups:
- groupname: searxng
gid: 977
users:
- username: searxng
uid: 977
shell: /bin/ash
environment:
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
HISTFILE: /dev/null
CONFIG_PATH: /etc/searxng
DATA_PATH: /var/cache/searxng
paths:
# Workdir
- path: /usr/local/searxng/
type: directory
uid: 977
gid: 977
permissions: 0o555
# Config volume
- path: /etc/searxng/
type: directory
uid: 977
gid: 977
permissions: 0o755
# Data volume
- path: /var/cache/searxng/
type: directory
uid: 977
gid: 977
permissions: 0o755
archs:
- x86_64
- aarch64
- armv7

View File

@@ -1,31 +1,24 @@
FROM ghcr.io/searxng/base:searxng-builder AS builder FROM ghcr.io/searxng/base:searxng-builder AS builder
COPY ./requirements.txt ./requirements-server.txt ./ COPY ./requirements*.txt ./
ENV UV_NO_MANAGED_PYTHON="true" RUN --mount=type=cache,id=pip,target=/root/.cache/pip set -eux; \
ENV UV_NATIVE_TLS="true" python -m venv ./.venv/; \
. ./.venv/bin/activate; \
pip install -r ./requirements.txt -r ./requirements-server.txt
ARG TIMESTAMP_VENV="0" COPY ./searx/ ./searx/
RUN --mount=type=cache,id=uv,target=/root/.cache/uv set -eux -o pipefail; \ ARG TIMESTAMP_SETTINGS="0"
export SOURCE_DATE_EPOCH="$TIMESTAMP_VENV"; \
uv venv; \
uv pip install --requirements ./requirements.txt --requirements ./requirements-server.txt; \
uv cache prune --ci; \
find ./.venv/lib/ -type f -exec strip --strip-unneeded {} + || true; \
find ./.venv/lib/ -type d -name "__pycache__" -exec rm -rf {} +; \
find ./.venv/lib/ -type f -name "*.pyc" -delete; \
python -m compileall -q -f -j 0 --invalidation-mode=unchecked-hash ./.venv/lib/; \
find ./.venv/lib/python*/site-packages/*.dist-info/ -type f -name "RECORD" -exec sort -t, -k1,1 -o {} {} \;; \
find ./.venv/ -exec touch -h --date="@$TIMESTAMP_VENV" {} +
COPY --exclude=./searx/version_frozen.py ./searx/ ./searx/ RUN set -eux; \
python -m compileall -q ./searx/; \
RUN set -eux -o pipefail; \ touch -c --date=@$TIMESTAMP_SETTINGS ./searx/settings.yml; \
python -m compileall -q -f -j 0 --invalidation-mode=unchecked-hash ./searx/; \
find ./searx/static/ -type f \ find ./searx/static/ -type f \
\( -name "*.html" -o -name "*.css" -o -name "*.js" -o -name "*.svg" \) \ \( -name "*.html" -o -name "*.css" -o -name "*.js" -o -name "*.svg" \) \
-exec gzip -9 -k {} + \ -exec gzip -9 -k {} + \
-exec brotli -9 -k {} + \ -exec brotli -9 -k {} + \
-exec gzip --test {}.gz + \ -exec gzip --test {}.gz + \
-exec brotli --test {}.br + -exec brotli --test {}.br +; \
# Move always changing files to /usr/local/searxng/
mv ./searx/version_frozen.py ./

View File

@@ -1,13 +1,12 @@
FROM ghcr.io/searxng/base:searxng AS dist
ARG CONTAINER_IMAGE_ORGANIZATION="searxng" ARG CONTAINER_IMAGE_ORGANIZATION="searxng"
ARG CONTAINER_IMAGE_NAME="searxng" ARG CONTAINER_IMAGE_NAME="searxng"
FROM localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder AS builder COPY --chown=searxng:searxng --from=localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder /usr/local/searxng/.venv/ ./.venv/
FROM ghcr.io/searxng/base:searxng AS dist COPY --chown=searxng:searxng --from=localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder /usr/local/searxng/searx/ ./searx/
COPY --chown=searxng:searxng ./container/ ./
COPY --chown=977:977 --from=builder /usr/local/searxng/.venv/ ./.venv/ COPY --chown=searxng:searxng --from=localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder /usr/local/searxng/version_frozen.py ./searx/
COPY --chown=977:977 --from=builder /usr/local/searxng/searx/ ./searx/
COPY --chown=977:977 ./container/ ./
COPY --chown=977:977 ./searx/version_frozen.py ./searx/
ARG CREATED="0001-01-01T00:00:00Z" ARG CREATED="0001-01-01T00:00:00Z"
ARG VERSION="unknown" ARG VERSION="unknown"
@@ -24,20 +23,21 @@ LABEL org.opencontainers.image.created="$CREATED" \
org.opencontainers.image.url="https://searxng.org" \ org.opencontainers.image.url="https://searxng.org" \
org.opencontainers.image.version="$VERSION" org.opencontainers.image.version="$VERSION"
ENV __SEARXNG_VERSION="$VERSION" \ ENV SEARXNG_VERSION="$VERSION" \
__SEARXNG_SETTINGS_PATH="$__SEARXNG_CONFIG_PATH/settings.yml" \ SEARXNG_SETTINGS_PATH="$CONFIG_PATH/settings.yml" \
GRANIAN_PROCESS_NAME="searxng" \ GRANIAN_PROCESS_NAME="searxng" \
GRANIAN_INTERFACE="wsgi" \ GRANIAN_INTERFACE="wsgi" \
GRANIAN_HOST="::" \ GRANIAN_HOST="::" \
GRANIAN_PORT="8080" \ GRANIAN_PORT="8080" \
GRANIAN_WEBSOCKETS="false" \ GRANIAN_WEBSOCKETS="false" \
GRANIAN_LOOP="uvloop" \
GRANIAN_BLOCKING_THREADS="4" \ GRANIAN_BLOCKING_THREADS="4" \
GRANIAN_WORKERS_KILL_TIMEOUT="30s" \ GRANIAN_WORKERS_KILL_TIMEOUT="30s" \
GRANIAN_BLOCKING_THREADS_IDLE_TIMEOUT="5m" GRANIAN_BLOCKING_THREADS_IDLE_TIMEOUT="5m"
# "*_PATH" ENVs are defined in base images # "*_PATH" ENVs are defined in base images
VOLUME $__SEARXNG_CONFIG_PATH VOLUME $CONFIG_PATH
VOLUME $__SEARXNG_DATA_PATH VOLUME $DATA_PATH
EXPOSE 8080 EXPOSE 8080

View File

@@ -1,28 +0,0 @@
# Read the documentation before using the `docker-compose.yml` file:
# https://docs.searxng.org/admin/installation-docker.html
name: searxng
services:
core:
container_name: searxng-core
image: docker.io/searxng/searxng:${SEARXNG_VERSION:-latest}
restart: always
ports:
- ${SEARXNG_HOST:+${SEARXNG_HOST}:}${SEARXNG_PORT:-8080}:${SEARXNG_PORT:-8080}
env_file: ./.env
volumes:
- ./core-config/:/etc/searxng/:Z
- core-data:/var/cache/searxng/
valkey:
container_name: searxng-valkey
image: docker.io/valkey/valkey:9-alpine
command: valkey-server --save 30 1 --loglevel warning
restart: always
volumes:
- valkey-data:/data/
volumes:
core-data:
valkey-data:

View File

@@ -77,41 +77,54 @@ volume_handler() {
setup_ownership "$target" "directory" setup_ownership "$target" "directory"
} }
setup() { # Handle configuration file updates
local template_settings="/usr/local/searxng/settings.template.yml" config_handler() {
local target_settings="$__SEARXNG_CONFIG_PATH/settings.yml" local target="$1"
local template="$2"
local new_template_target="$target.new"
# Create/Update the configuration file
if [ -f "$target" ]; then
setup_ownership "$target" "file"
if [ "$template" -nt "$target" ]; then
cp -pfT "$template" "$new_template_target"
if [ ! -f "$target_settings" ]; then
cat <<EOF cat <<EOF
... ...
... INFORMATION ... INFORMATION
... "$target_settings" does not exist, creating from template... ... Update available for "$target"
... It is recommended to update the configuration file to ensure proper functionality
...
... New version placed at "$new_template_target"
... Please review and merge changes
... ...
EOF EOF
cp -pfT "$template_settings" "$target_settings" fi
else
cat <<EOF
...
... INFORMATION
... "$target" does not exist, creating from template...
...
EOF
cp -pfT "$template" "$target"
sed -i "s/ultrasecretkey/$(head -c 24 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9')/g" "$target_settings" sed -i "s/ultrasecretkey/$(head -c 24 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9')/g" "$target"
fi fi
check_file "$target_settings" check_file "$target"
} }
cat <<EOF cat <<EOF
SearXNG $__SEARXNG_VERSION SearXNG $SEARXNG_VERSION
EOF EOF
# Check for volume mounts # Check for volume mounts
volume_handler "$__SEARXNG_CONFIG_PATH" volume_handler "$CONFIG_PATH"
volume_handler "$__SEARXNG_DATA_PATH" volume_handler "$DATA_PATH"
setup # Check for files
config_handler "$SEARXNG_SETTINGS_PATH" "/usr/local/searxng/searx/settings.yml"
# root only features
if [ "$(id -u)" -eq 0 ]; then
update-ca-certificates
fi
# ENVs aliases
export GRANIAN_PORT="${SEARXNG_PORT:-$GRANIAN_PORT}"
exec /usr/local/searxng/.venv/bin/granian searx.webapp:app exec /usr/local/searxng/.venv/bin/granian searx.webapp:app

View File

@@ -1,8 +0,0 @@
# Read the documentation before extending the defaults:
# https://docs.searxng.org/admin/settings/
use_default_settings: true
server:
secret_key: "ultrasecretkey"
image_proxy: true

View File

@@ -1,57 +0,0 @@
.content {
width: 52em; /* instead of 46em */
}
p code.literal {
text-wrap: nowrap;
}
aside.sidebar {
border-color: lightsteelblue;
border-radius: 3pt;
}
p.sidebar-title, .sidebar p {
margin: 6pt;
}
.sidebar li,
.hlist li {
list-style-type: disclosure-closed;
}
.sphinxsidebar .current > a {
font-weight: bold;
}
/* admonitions with (rendered) reST markup examples (:class: rst-example)
*
* .. admonition:: title of the example
* :class: rst-example
* ....
*/
div.rst-example {
background-color: inherit;
margin: 0;
border-top: none;
border-right: 1px solid #ccc;
border-bottom: none;
border-left: none;
border-radius: none;
padding: 0;
}
div.rst-example > p.admonition-title {
font-family: Sans Serif;
font-style: italic;
font-size: 0.8em;
display: block;
border-bottom: 1px solid #ccc;
padding: 0.5em 1em;
text-align: right;
}
div.sphinx-tabs {
clear: both;
}

151
docs/_themes/searxng/static/searxng.css vendored Normal file
View File

@@ -0,0 +1,151 @@
@import url("pocoo.css");
a, a.reference, a.footnote-reference {
color: #004b6b;
border-color: #004b6b;
}
a:hover {
color: #6d4100;
border-color: #6d4100;
}
p.version-warning {
background-color: #004b6b;
}
aside.sidebar {
background-color: whitesmoke;
border-color: lightsteelblue;
border-radius: 3pt;
}
div.sphinxsidebar p.caption {
display: none;
}
p.sidebar-title, .sidebar p {
margin: 6pt;
}
.sidebar li,
.hlist li {
list-style-type: disclosure-closed;
}
.sphinxsidebar .current > a {
font-weight: bold;
}
/* admonitions
*/
div.admonition, div.topic, nav.contents, div.toctree-wrapper {
background-color: #fafafa;
margin: 8px 0px;
padding: 1em;
border-radius: 3pt 0 0 3pt;
border-top: none;
border-right: none;
border-bottom: none;
border-left: 5pt solid #ccc;
list-style-type: disclosure-closed;
}
div.toctree-wrapper p.caption {
font-weight: normal;
font-size: 24px;
margin: 0 0 10px 0;
padding: 0;
line-height: 1;
display: inline;
}
p.admonition-title:after {
content: none;
}
.admonition.hint { border-color: #416dc0b0; }
.admonition.note { border-color: #6c856cb0; }
.admonition.tip { border-color: #85c5c2b0; }
.admonition.attention { border-color: #ecec97b0; }
.admonition.caution { border-color: #a6c677b0; }
.admonition.danger { border-color: #d46262b0; }
.admonition.important { border-color: #dfa3a3b0; }
.admonition.error { border-color: red; }
.admonition.warning { border-color: darkred; }
.admonition.admonition-generic-admonition-title {
border-color: #416dc0b0;
}
/* admonitions with (rendered) reST markup examples (:class: rst-example)
*
* .. admonition:: title of the example
* :class: rst-example
* ....
*/
div.rst-example {
background-color: inherit;
margin: 0;
border-top: none;
border-right: 1px solid #ccc;
border-bottom: none;
border-left: none;
border-radius: none;
padding: 0;
}
div.rst-example > p.admonition-title {
font-family: Sans Serif;
font-style: italic;
font-size: 0.8em;
display: block;
border-bottom: 1px solid #ccc;
padding: 0.5em 1em;
text-align: right;
}
/* code block in figures
*/
div.highlight pre {
text-align: left;
}
/* Table theme
*/
thead, tfoot {
background-color: #fff;
}
th:hover, td:hover {
background-color: #ffc;
}
thead th, tfoot th, tfoot td, tbody th {
background-color: #fffaef;
}
tbody tr:nth-child(odd) {
background-color: #fff;
}
tbody tr:nth-child(even) {
background-color: #fafafa;
}
caption {
font-family: Sans Serif;
padding: 0.5em;
margin: 0.5em 0 0.5em 0;
caption-side: top;
text-align: left;
}
div.sphinx-tabs {
clear: both;
}

7
docs/_themes/searxng/theme.conf vendored Normal file
View File

@@ -0,0 +1,7 @@
[theme]
inherit = pocoo
stylesheet = searxng.css
[options]
touch_icon =
globaltoc_maxdepth = 5

View File

@@ -48,7 +48,7 @@ solve the CAPTCHA from `qwant.com <https://www.qwant.com/>`__.
.. group-tab:: Firefox .. group-tab:: Firefox
.. kernel-figure:: /assets/answer-captcha/ffox-setting-proxy-socks.png .. kernel-figure:: answer-captcha/ffox-setting-proxy-socks.png
:alt: FFox proxy on SOCKS5, 127.0.0.1:8080 :alt: FFox proxy on SOCKS5, 127.0.0.1:8080
Firefox's network settings Firefox's network settings
@@ -66,3 +66,4 @@ solve the CAPTCHA from `qwant.com <https://www.qwant.com/>`__.
-N -N
Do not execute a remote command. This is useful for just forwarding ports. Do not execute a remote command. This is useful for just forwarding ports.

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -4,6 +4,11 @@
Buildhosts Buildhosts
========== ==========
.. contents::
:depth: 2
:local:
:backlinks: entry
To get best results from build, it's recommend to install additional packages on To get best results from build, it's recommend to install additional packages on
build hosts (see :ref:`searxng.sh`). build hosts (see :ref:`searxng.sh`).

View File

@@ -61,6 +61,11 @@ section might give you some guidance.
- `Apache Fedora`_ - `Apache Fedora`_
- `Apache directives`_ - `Apache directives`_
.. contents::
:depth: 2
:local:
:backlinks: entry
The Apache HTTP server The Apache HTTP server
====================== ======================

View File

@@ -9,6 +9,7 @@ Installation container
.. _Podman rootless containers: https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md .. _Podman rootless containers: https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md
.. _DockerHub mirror: https://hub.docker.com/r/searxng/searxng .. _DockerHub mirror: https://hub.docker.com/r/searxng/searxng
.. _GHCR mirror: https://ghcr.io/searxng/searxng .. _GHCR mirror: https://ghcr.io/searxng/searxng
.. _Docker compose: https://github.com/searxng/searxng-docker
.. sidebar:: info .. sidebar:: info
@@ -26,7 +27,7 @@ Installation container
`Docker 101`_ before proceeding. `Docker 101`_ before proceeding.
Container images are the basis for deployments in containerized environments, Container images are the basis for deployments in containerized environments,
Compose, Kubernetes and more. `Docker compose`_, Kubernetes and more.
.. _Container installation: .. _Container installation:
@@ -54,10 +55,10 @@ In the case of Docker, you need to add the user running the container to the
In the case of Podman, no additional steps are generally required, but there In the case of Podman, no additional steps are generally required, but there
are some considerations when running `Podman rootless containers`_. are some considerations when running `Podman rootless containers`_.
.. _Container registries: .. _Container pulling images:
Registries Pulling images
---------- --------------
.. note:: .. note::
@@ -69,115 +70,26 @@ The official images are mirrored at:
- `DockerHub mirror`_ - `DockerHub mirror`_
- `GHCR mirror`_ (GitHub Container Registry) - `GHCR mirror`_ (GitHub Container Registry)
.. _Container compose instancing: Pull the latest image:
Compose instancing
==================
This is the recommended way to deploy SearXNG in a containerized environment.
Compose templates allow you to define container configurations in a
declarative manner.
.. _Container compose instancing setup:
Setup
-----
1. Create the environment:
.. code:: sh .. code:: sh
# Create the environment and configuration directories $ docker pull docker.io/searxng/searxng:latest
$ mkdir -p ./searxng/core-config/
$ cd ./searxng/
# Fetch the latest compose template \.\. or if you want to lock in to a specific version:
$ curl -fsSL \
-O https://raw.githubusercontent.com/searxng/searxng/master/container/docker-compose.yml \
-O https://raw.githubusercontent.com/searxng/searxng/master/container/.env.example
2. Copy the ``.env.example`` file and edit the values as needed:
.. code:: sh .. code:: sh
$ cp -i .env.example .env $ docker pull docker.io/searxng/searxng:2025.8.1-3d96414
# nano or your preferred text editor... .. _Container instancing:
$ nano .env
3. Start & stop the services: Instancing
==========
.. code:: sh
$ docker compose up -d
$ docker compose down
4. Setup your settings in ``core-config/settings.yml`` according to your preferences.
.. _Container compose instancing maintenance:
Management
----------
.. important::
Remember to review the new templates for any changes that may affect your
deployment, and update the ``.env`` file accordingly.
To update the templates to their latest versions:
.. code:: sh
$ docker compose down
$ curl -fsSLO \
https://raw.githubusercontent.com/searxng/searxng/master/container/docker-compose.yml \
https://raw.githubusercontent.com/searxng/searxng/master/container/.env.example
$ docker compose up -d
To update the services to their latest versions:
.. code:: sh
$ docker compose down
$ docker compose pull
$ docker compose up -d
List running services:
.. code:: sh
$ docker compose ps
NAME IMAGE ... CREATED STATUS PORTS
searxng-core ... ... 3 minutes ago Up 3 minutes 0.0.0.0:8080->8080/tcp
searxng-valkey ... ... 3 minutes ago Up 3 minutes 6379/tcp
Print a service container logs:
.. code:: sh
$ docker compose logs -f core
Access a service container shell (troubleshooting):
.. code:: sh
$ docker compose exec -it --user root core /bin/sh -l
/usr/local/searxng #
Stop and remove the services:
.. code:: sh
$ docker compose down
.. _Container manual instancing:
Manual instancing
=================
This section is intended for advanced users who need custom deployments. We This section is intended for advanced users who need custom deployments. We
recommend using `Container compose instancing`_, which provides a preconfigured recommend using `Docker compose`_, which provides a preconfigured environment
environment with sensible defaults. with sensible defaults.
Basic container instancing example: Basic container instancing example:
@@ -188,7 +100,7 @@ Basic container instancing example:
$ cd ./searxng/ $ cd ./searxng/
# Run the container # Run the container
$ docker run --name searxng -d \ $ docker run --name searxng --replace -d \
-p 8888:8080 \ -p 8888:8080 \
-v "./config/:/etc/searxng/" \ -v "./config/:/etc/searxng/" \
-v "./data/:/var/cache/searxng/" \ -v "./data/:/var/cache/searxng/" \
@@ -209,18 +121,12 @@ List running containers:
CONTAINER ID IMAGE ... CREATED PORTS NAMES CONTAINER ID IMAGE ... CREATED PORTS NAMES
1af574997e63 ... ... 3 minutes ago 0.0.0.0:8888->8080/tcp searxng 1af574997e63 ... ... 3 minutes ago 0.0.0.0:8888->8080/tcp searxng
Print the container logs:
.. code:: sh
$ docker container logs -f searxng
Access the container shell (troubleshooting): Access the container shell (troubleshooting):
.. code:: sh .. code:: sh
$ docker container exec -it --user root searxng /bin/sh -l $ docker container exec -it --user root searxng /bin/sh -l
/usr/local/searxng # 1af574997e63:/usr/local/searxng#
Stop and remove the container: Stop and remove the container:
@@ -247,24 +153,17 @@ Environment variables
The following environment variables can be configured: The following environment variables can be configured:
- ``$SEARXNG_*``: Controls the SearXNG configuration options, look out for - ``$SEARXNG_*``: Controls the SearXNG configuration options, look out for
environment ``$SEARXNG_*`` in :ref:`settings server`, :ref:`settings environment ``$SEARXNG_*`` in :ref:`settings server` and :ref:`settings
general` and the :origin:`container/.env.example` template. general`.
- ``$GRANIAN_*``: Controls the :ref:`Granian server options <Granian configuration>`. - ``$GRANIAN_*``: Controls the :ref:`Granian server options <Granian configuration>`.
- ``$FORCE_OWNERSHIP``: Ensures mounted volumes/files are owned by the - ``$FORCE_OWNERSHIP``: Ensures mounted volumes/files are owned by the
``searxng:searxng`` user (default: ``true``) ``searxng:searxng`` user (default: ``true``)
.. _Container custom certificates: Container internal paths (don't modify unless you know what you're doing):
Custom certificates - ``$CONFIG_PATH``: Path to the SearXNG configuration directory (default: ``/etc/searxng``)
=================== - ``$SEARXNG_SETTINGS_PATH``: Path to the SearXNG settings file (default: ``$CONFIG_PATH/settings.yml``)
- ``$DATA_PATH``: Path to the SearXNG data directory (default: ``/var/cache/searxng``)
You can mount ``/usr/local/share/ca-certificates/`` folder to add/remove
additional certificates as needed.
They will be available on container (re)start or when running
``update-ca-certificates`` in the container shell.
This requires the container to be running with ``root`` privileges.
.. _Container custom images: .. _Container custom images:
@@ -289,61 +188,3 @@ container images are not officially supported):
localhost/searxng/searxng builder ... About a minute ago 524 MB localhost/searxng/searxng builder ... About a minute ago 524 MB
ghcr.io/searxng/base searxng-builder ... 2 days ago 378 MB ghcr.io/searxng/base searxng-builder ... 2 days ago 378 MB
ghcr.io/searxng/base searxng ... 2 days ago 42.2 MB ghcr.io/searxng/base searxng ... 2 days ago 42.2 MB
Migrate from ``searxng-docker``
===============================
We expect the following source directory structure:
.. code:: sh
.
└── searxng-docker
├── searxng
│ ├── favicons.toml
│ ├── limiter.toml
│ ├── settings.yml
│ └── ...
├── .env
├── Caddyfile
├── docker-compose.yml
└── ...
Create a brand new environment outside ``searxng-docker`` directory, following
`Container compose instancing setup`_.
Once up and running, stop the services and move the configuration files from
the old mount to the new one:
.. code:: sh
$ mv ./searxng-docker/searxng/* ./searxng/core-config/
If using Valkey features like bot protection (limiter), you will need to update
the URL hostname in :origin:`searx/settings.yml` file to ``valkey`` or
``searxng-valkey``.
If you have any environment variables in the old ``.env`` file, make
sure to add them to the new one.
Consider setting up a reverse proxy if exposing the instance to the public.
You should end with the following directory structure:
.. code:: sh
.
├── searxng
│ ├── core-config
│ │ ├── favicons.toml
│ │ ├── limiter.toml
│ │ ├── settings.yml
│ │ └── ...
│ ├── .env.example
│ ├── .env
│ └── docker-compose.yml
└── searxng-docker
└── ...
If everything is working on the new environment, you can remove the old
``searxng-docker`` directory and its contents.

View File

@@ -41,6 +41,12 @@ section might give you some guidance.
- `uWSGI support from nginx`_ - `uWSGI support from nginx`_
.. contents::
:depth: 2
:local:
:backlinks: entry
The nginx HTTP server The nginx HTTP server
===================== =====================

View File

@@ -4,6 +4,12 @@
Step by step installation Step by step installation
========================= =========================
.. contents::
:depth: 2
:local:
:backlinks: entry
In this section we show the setup of a SearXNG instance that will be installed In this section we show the setup of a SearXNG instance that will be installed
by the :ref:`installation scripts`. by the :ref:`installation scripts`.

View File

@@ -9,6 +9,12 @@ uWSGI
- `systemd.unit`_ - `systemd.unit`_
- `uWSGI Emperor`_ - `uWSGI Emperor`_
.. contents::
:depth: 2
:local:
:backlinks: entry
.. _systemd.unit: https://www.freedesktop.org/software/systemd/man/systemd.unit.html .. _systemd.unit: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
.. _One service per app in systemd: .. _One service per app in systemd:
https://uwsgi-docs.readthedocs.io/en/latest/Systemd.html#one-service-per-app-in-systemd https://uwsgi-docs.readthedocs.io/en/latest/Systemd.html#one-service-per-app-in-systemd

View File

@@ -8,6 +8,11 @@ Favicons
Don't activate the favicons before reading the documentation. Don't activate the favicons before reading the documentation.
.. contents::
:depth: 2
:local:
:backlinks: entry
Activating the favicons in SearXNG is very easy, but this **generates a Activating the favicons in SearXNG is very easy, but this **generates a
significantly higher load** in the client/server communication and increases significantly higher load** in the client/server communication and increases
resources needed on the server. resources needed on the server.
@@ -245,3 +250,4 @@ into the *proxy*:
.. _data URL: .. _data URL:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs
.. _FQN: https://en.wikipedia.org/wiki/Fully_qualified_name .. _FQN: https://en.wikipedia.org/wiki/Fully_qualified_name

View File

@@ -8,5 +8,10 @@ Limiter
The limiter requires a :ref:`Valkey <settings valkey>` database. The limiter requires a :ref:`Valkey <settings valkey>` database.
.. contents::
:depth: 2
:local:
:backlinks: entry
.. automodule:: searx.limiter .. automodule:: searx.limiter
:members: :members:

View File

@@ -19,7 +19,6 @@ Settings
settings_search settings_search
settings_server settings_server
settings_ui settings_ui
settings_preferences
settings_redis settings_redis
settings_valkey settings_valkey
settings_outgoing settings_outgoing

View File

@@ -12,6 +12,11 @@ file.
- :ref:`use_default_settings.yml` - :ref:`use_default_settings.yml`
- :ref:`search API` - :ref:`search API`
.. contents::
:depth: 2
:local:
:backlinks: entry
.. _settings location: .. _settings location:
settings.yml location settings.yml location

View File

@@ -4,5 +4,22 @@
``brand:`` ``brand:``
========== ==========
.. autoclass:: searx.brand.SettingsBrand .. code:: yaml
:members:
brand:
issue_url: https://github.com/searxng/searxng/issues
docs_url: https://docs.searxng.org
public_instances: https://searx.space
wiki_url: https://github.com/searxng/searxng/wiki
``issue_url`` :
If you host your own issue tracker change this URL.
``docs_url`` :
If you host your own documentation change this URL.
``public_instances`` :
If you host your own https://searx.space change this URL.
``wiki_url`` :
Link to your wiki (or ``false``)

View File

@@ -124,17 +124,14 @@ engine is shown. Most of the options have a default value or even are optional.
``api_key`` : optional ``api_key`` : optional
In a few cases, using an API needs the use of a secret key. How to obtain them In a few cases, using an API needs the use of a secret key. How to obtain them
is described in the file. Engines that require an API key are set to is described in the file.
``inactive: true`` by default. To enable such an engine, provide the API key
and set ``inactive: false``.
``disabled`` : optional ``disabled`` : optional
To disable by default the engine, but not deleting it. It will allow the user To disable by default the engine, but not deleting it. It will allow the user
to manually activate it in the settings. to manually activate it in the settings.
``inactive``: optional ``inactive``: optional
Remove the engine from the settings (*disabled & removed*). This defaults to ``true`` for engines Remove the engine from the settings (*disabled & removed*).
that require an API key, please see the ``api_key`` section if you want to enable such an engine.
``language`` : optional ``language`` : optional
If you want to use another language for a specific engine, you can define it If you want to use another language for a specific engine, you can define it

View File

@@ -69,9 +69,6 @@ The built-in plugins are all located in the namespace `searx.plugins`.
searx.plugins.calculator.SXNGPlugin: searx.plugins.calculator.SXNGPlugin:
active: true active: true
searx.plugins.infinite_scroll.SXNGPlugin:
active: false
searx.plugins.hash_plugin.SXNGPlugin: searx.plugins.hash_plugin.SXNGPlugin:
active: true active: true
@@ -102,18 +99,11 @@ The built-in plugins are all located in the namespace `searx.plugins`.
external plugins external plugins
================ ================
SearXNG supports :ref:`external plugins <dev plugin>` / there is no need to
install one, SearXNG runs out of the box.
`Only show green hosted results`_:
SearXNG plugin to check if a domain is part of the Green WEB.
`SearXNG BM25 Reranker`_:
SearXNG plugin that reranks search results using BM25 text relevance scoring
to improve search quality.
.. _Only show green hosted results: .. _Only show green hosted results:
https://github.com/return42/tgwf-searx-plugins/ https://github.com/return42/tgwf-searx-plugins/
.. _SearXNG BM25 Reranker: SearXNG supports *external plugins* / there is no need to install one, SearXNG
https://github.com/Oaklight/searxng-bm25-reranker runs out of the box.
- `Only show green hosted results`_
- ..

View File

@@ -1,8 +0,0 @@
.. _settings preferences:
================
``preferences:``
================
.. autoclass:: searx._settings.SettingsPref
:members:

View File

@@ -36,19 +36,18 @@
- ``360search`` - ``360search``
- ``baidu`` - ``baidu``
- ``bing``
- ``brave`` - ``brave``
- ``dbpedia`` - ``dbpedia``
- ``duckduckgo`` - ``duckduckgo``
- ``google`` - ``google``
- ``mwmbl`` - ``mwmbl``
- ``naver`` - ``naver``
- ``privacywall``
- ``quark`` - ``quark``
- ``qwant`` - ``qwant``
- ``seznam`` - ``seznam``
- ``sogou`` - ``sogou``
- ``startpage`` - ``startpage``
- ``stract``
- ``swisscows`` - ``swisscows``
- ``wikipedia`` - ``wikipedia``
- ``yandex`` - ``yandex``

View File

@@ -47,7 +47,6 @@
activated: activated:
- :py:obj:`searx.botdetection.link_token` in the :ref:`limiter` - :py:obj:`searx.botdetection.link_token` in the :ref:`limiter`
- :ref:`image_proxy`
.. _image_proxy: .. _image_proxy:
@@ -56,22 +55,11 @@
.. _method: .. _method:
``method`` : ``GET`` | ``POST`` ``method`` : ``$SEARXNG_METHOD``
Whether to use ``GET`` or ``POST`` HTTP method when searching.
HTTP method. By defaults ``POST`` is used / The ``POST`` method has the
advantage with some WEB browsers that the history is not easy to read, but
there are also various disadvantages that sometimes **severely restrict the
ease of use for the end user** (e.g. back button to jump back to the previous
search page and drag & drop of search term to new tabs do not work as
expected .. and several more). We had some discussions about the *pros
versus cons*:
- `[doc] adds the missing documentation of the server.method settings
<https://github.com/searxng/searxng/pull/3619>`__
- look out for `label:"http methods GET & POST"
<https://github.com/search?q=repo%3Asearxng%2Fsearxng+label%3A%22http+methods+GET+%26+POST%22>`__
.. _HTTP headers: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers .. _HTTP headers: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
``default_http_headers`` : ``default_http_headers`` :
Set additional `HTTP headers`_, see `#755 <https://github.com/searx/searx/issues/715>`__ Set additional HTTP headers, see `#755 <https://github.com/searx/searx/issues/715>`__

View File

@@ -12,6 +12,7 @@
ui: ui:
default_locale: "" default_locale: ""
query_in_title: false query_in_title: false
infinite_scroll: false
center_alignment: false center_alignment: false
cache_url: https://web.archive.org/web/ cache_url: https://web.archive.org/web/
default_theme: simple default_theme: simple
@@ -31,6 +32,9 @@
When true, the result page's titles contains the query it decreases the When true, the result page's titles contains the query it decreases the
privacy, since the browser can records the page titles. privacy, since the browser can records the page titles.
``infinite_scroll``:
When true, automatically loads the next page when scrolling to bottom of the current page.
``center_alignment`` : default ``false`` ``center_alignment`` : default ``false``
When enabled, the results are centered instead of being in the left (or RTL) When enabled, the results are centered instead of being in the left (or RTL)
side of the screen. This setting only affects the *desktop layout* side of the screen. This setting only affects the *desktop layout*

View File

@@ -9,6 +9,11 @@ SearXNG maintenance
- :ref:`toolboxing` - :ref:`toolboxing`
- :ref:`uWSGI maintenance` - :ref:`uWSGI maintenance`
.. contents::
:depth: 2
:local:
:backlinks: entry
.. _update searxng: .. _update searxng:
How to update How to update

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.2 KiB

Some files were not shown because too many files have changed in this diff Show More