mirror of
https://github.com/searxng/searxng.git
synced 2026-07-29 19:31:35 +00:00
Compare commits
1 Commits
update_dat
...
ff87499564
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff87499564 |
163
.dir-locals-template.el
Normal file
163
.dir-locals-template.el
Normal file
@@ -0,0 +1,163 @@
|
||||
;;; .dir-locals.el
|
||||
;;
|
||||
;; Per-Directory Local Variables:
|
||||
;; https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html
|
||||
;;
|
||||
;; For full fledge developer tools install emacs packages:
|
||||
;;
|
||||
;; M-x package-install ...
|
||||
;;
|
||||
;; magit gitconfig
|
||||
;; nvm lsp-mode lsp-pyright lsp-eslint
|
||||
;; pyvenv pylint pip-requirements
|
||||
;; jinja2-mode
|
||||
;; json-mode
|
||||
;; company company-jedi company-quickhelp company-shell
|
||||
;; realgud
|
||||
;; sphinx-doc markdown-mode graphviz-dot-mode
|
||||
;; apache-mode nginx-mode
|
||||
;;
|
||||
;; To setup a developer environment, build target::
|
||||
;;
|
||||
;; $ make node.env.dev pyenv.install
|
||||
;;
|
||||
;; Some buffer locals are referencing the project environment:
|
||||
;;
|
||||
;; - prj-root --> <repo>/
|
||||
;; - nvm-dir --> <repo>/.nvm
|
||||
;; - python-environment-directory --> <repo>/local
|
||||
;; - python-environment-default-root-name --> py3
|
||||
;; - python-shell-virtualenv-root --> <repo>/local/py3
|
||||
;; When this variable is set with the path of the virtualenv to use,
|
||||
;; `process-environment' and `exec-path' get proper values in order to run
|
||||
;; shells inside the specified virtualenv, example::
|
||||
;; (setq python-shell-virtualenv-root "/path/to/env/")
|
||||
;; - python-shell-interpreter --> <repo>/local/py3/bin/python
|
||||
;;
|
||||
;; Python development:
|
||||
;;
|
||||
;; Jedi, flycheck & other python stuff should use the 'python-shell-interpreter'
|
||||
;; from the local py3 environment.
|
||||
;;
|
||||
|
||||
((nil
|
||||
. ((fill-column . 80)
|
||||
(indent-tabs-mode . nil)
|
||||
(eval . (progn
|
||||
|
||||
(add-to-list 'auto-mode-alist '("\\.html\\'" . jinja2-mode))
|
||||
|
||||
;; project root folder is where the `.dir-locals.el' is located
|
||||
(setq-local prj-root
|
||||
(locate-dominating-file default-directory ".dir-locals.el"))
|
||||
|
||||
(setq-local python-environment-directory
|
||||
(expand-file-name "./local" prj-root))
|
||||
|
||||
;; to get in use of NVM environment, install https://github.com/rejeep/nvm.el
|
||||
(setq-local nvm-dir (expand-file-name "./.nvm" prj-root))
|
||||
|
||||
;; use nodejs from the (local) NVM environment (see nvm-dir)
|
||||
(nvm-use-for-buffer)
|
||||
(ignore-errors (require 'lsp))
|
||||
(setq-local lsp-server-install-dir (car (cdr nvm-current-version)))
|
||||
(setq-local lsp-enable-file-watchers nil)
|
||||
|
||||
;; use 'py3' environment as default
|
||||
(setq-local python-environment-default-root-name
|
||||
"py3")
|
||||
|
||||
(setq-local python-shell-virtualenv-root
|
||||
(expand-file-name
|
||||
python-environment-default-root-name python-environment-directory))
|
||||
|
||||
(setq-local python-shell-interpreter
|
||||
(expand-file-name
|
||||
"bin/python" python-shell-virtualenv-root))))))
|
||||
(makefile-gmake-mode
|
||||
. ((indent-tabs-mode . t)))
|
||||
|
||||
(yaml-mode
|
||||
. ((eval . (progn
|
||||
|
||||
;; flycheck should use the local py3 environment
|
||||
(setq-local flycheck-yaml-yamllint-executable
|
||||
(expand-file-name "bin/yamllint" python-shell-virtualenv-root))
|
||||
|
||||
(setq-local flycheck-yamllintrc
|
||||
(expand-file-name ".yamllint.yml" prj-root))
|
||||
|
||||
(flycheck-checker . yaml-yamllint)))))
|
||||
|
||||
(json-mode
|
||||
. ((eval . (progn
|
||||
(setq-local js-indent-level 4)
|
||||
(flycheck-checker . json-python-json)))))
|
||||
|
||||
(js-mode
|
||||
. ((eval . (progn
|
||||
(ignore-errors (require 'lsp-eslint))
|
||||
(setq-local js-indent-level 2)
|
||||
;; flycheck should use the eslint checker from developer tools
|
||||
(setq-local flycheck-javascript-eslint-executable
|
||||
(expand-file-name "node_modules/.bin/eslint" prj-root))
|
||||
;; (flycheck-mode)
|
||||
|
||||
(if (featurep 'lsp-eslint)
|
||||
(lsp))
|
||||
))))
|
||||
|
||||
(python-mode
|
||||
. ((eval . (progn
|
||||
(ignore-errors (require 'jedi-core))
|
||||
(ignore-errors (require 'lsp-pyright))
|
||||
(ignore-errors (sphinx-doc-mode))
|
||||
(setq-local python-environment-virtualenv
|
||||
(list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root)
|
||||
;;"--system-site-packages"
|
||||
"--quiet"))
|
||||
|
||||
(setq-local pylint-command
|
||||
(expand-file-name "bin/pylint" python-shell-virtualenv-root))
|
||||
|
||||
(if (featurep 'lsp-pyright)
|
||||
(lsp))
|
||||
|
||||
;; pylint will find the '.pylintrc' file next to the CWD
|
||||
;; https://pylint.readthedocs.io/en/latest/user_guide/run.html#command-line-options
|
||||
(setq-local flycheck-pylintrc
|
||||
".pylintrc")
|
||||
|
||||
;; flycheck & other python stuff should use the local py3 environment
|
||||
(setq-local flycheck-python-pylint-executable
|
||||
python-shell-interpreter)
|
||||
|
||||
;; use 'M-x jedi:show-setup-info' and 'M-x epc:controller' to inspect jedi server
|
||||
;; https://tkf.github.io/emacs-jedi/latest/#jedi:environment-root -- You
|
||||
;; can specify a full path instead of a name (relative path). In that case,
|
||||
;; python-environment-directory is ignored and Python virtual environment
|
||||
;; is created at the specified path.
|
||||
(setq-local jedi:environment-root
|
||||
python-shell-virtualenv-root)
|
||||
|
||||
;; https://tkf.github.io/emacs-jedi/latest/#jedi:server-command
|
||||
(setq-local jedi:server-command
|
||||
(list python-shell-interpreter
|
||||
jedi:server-script))
|
||||
|
||||
;; jedi:environment-virtualenv --> see above 'python-environment-virtualenv'
|
||||
;; is set buffer local! No need to setup jedi:environment-virtualenv:
|
||||
;;
|
||||
;; Virtualenv command to use. A list of string. If it is nil,
|
||||
;; python-environment-virtualenv is used instead. You must set non-nil
|
||||
;; value to jedi:environment-root in order to make this setting work.
|
||||
;;
|
||||
;; https://tkf.github.io/emacs-jedi/latest/#jedi:environment-virtualenv
|
||||
;;
|
||||
;; (setq-local jedi:environment-virtualenv
|
||||
;; (list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root)
|
||||
;; "--python"
|
||||
;; "/usr/bin/python3.4"
|
||||
;; ))
|
||||
))))
|
||||
)
|
||||
@@ -1,6 +1,5 @@
|
||||
*
|
||||
|
||||
!container/*.template.*
|
||||
!container/entrypoint.sh
|
||||
!searx/**
|
||||
!requirements*.txt
|
||||
|
||||
@@ -10,7 +10,7 @@ trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
|
||||
[{*.py,*.pyi}]
|
||||
[*.py]
|
||||
# code formatter accepts length of 120, but editor should prefer 80
|
||||
max_line_length = 80
|
||||
|
||||
@@ -36,24 +36,12 @@ indent_size = 2
|
||||
[*.js]
|
||||
indent_size = 2
|
||||
|
||||
[*.ts]
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
insert_final_newline = ignore
|
||||
|
||||
[*.map]
|
||||
indent_size = ignore
|
||||
insert_final_newline = ignore
|
||||
|
||||
# Minified JavaScript files shouldn't be changed
|
||||
[*.min.js]
|
||||
indent_style = ignore
|
||||
insert_final_newline = ignore
|
||||
|
||||
# Minified CSS files shouldn't be changed
|
||||
[*.min.css]
|
||||
[**.min.js]
|
||||
indent_style = ignore
|
||||
insert_final_newline = ignore
|
||||
|
||||
|
||||
65
.github/ISSUE_TEMPLATE/bug-report.md
vendored
65
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -1,50 +1,39 @@
|
||||
---
|
||||
name: "Bug report"
|
||||
about: Report a bug in SearXNG"
|
||||
labels: ["bug"]
|
||||
type: "bug"
|
||||
name: Bug report
|
||||
about: Report a bug in SearXNG
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- PLEASE FILL THESE FIELDS, IT REALLY HELPS THE MAINTAINERS OF SearXNG -->
|
||||
|
||||
_Replace this placeholder with a meaningful and precise description of the bug._
|
||||
**Version of SearXNG, commit number if you are using on master branch and stipulate if you forked SearXNG**
|
||||
<!-- If you are running on master branch using git execute this command
|
||||
in order to fetch the latest commit ID:
|
||||
```
|
||||
git log -1
|
||||
```
|
||||
If you are using searxng-docker then look at the bottom of the SearXNG page
|
||||
and check for the version after "Powered by SearXNG"
|
||||
|
||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
||||
|
||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
||||
Please also stipulate if you are using a forked version of SearXNG and
|
||||
include a link to the fork source code.
|
||||
-->
|
||||
**How did you install SearXNG?**
|
||||
<!-- Did you install SearXNG using the official wiki or using searxng-docker
|
||||
or manually by executing the searx/webapp.py file? -->
|
||||
**What happened?**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
### How To Reproduce?
|
||||
|
||||
<!-- How can we reproduce this issue? (as minimally and as precisely as
|
||||
possible) -->
|
||||
|
||||
### Expected behavior
|
||||
**How To Reproduce**
|
||||
<!-- How can we reproduce this issue? (as minimally and as precisely as possible) -->
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
### Screenshots & Logs
|
||||
|
||||
**Screenshots & Logs**
|
||||
<!-- If applicable, add screenshots, logs to help explain your problem. -->
|
||||
|
||||
### Version of SearXNG
|
||||
|
||||
<!-- Commit number if you are using on master branch and stipulate if you forked
|
||||
SearXNG -->
|
||||
|
||||
<!-- Look at the bottom of the SearXNG page and check for the version after
|
||||
"Powered by SearXNG" If you are using a forked version of SearXNG include a
|
||||
link to the fork source code. -->
|
||||
|
||||
### How did you install SearXNG?
|
||||
|
||||
<!-- Did you install SearXNG using the official documentation or using
|
||||
searxng-docker? -->
|
||||
|
||||
### Additional context
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
|
||||
- [ ] I read the [AI Policy] and hereby confirm that this issue conforms with the policy.
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Questions & Answers (Q&A)
|
||||
url: https://github.com/searxng/searxng/discussions/categories/q-a
|
||||
|
||||
61
.github/ISSUE_TEMPLATE/engine-request.md
vendored
61
.github/ISSUE_TEMPLATE/engine-request.md
vendored
@@ -1,46 +1,31 @@
|
||||
---
|
||||
name: Engine request"
|
||||
about: Request a new engine in SearXNG"
|
||||
labels: ["engine request"]
|
||||
type: "feature"
|
||||
name: Engine request
|
||||
about: Request a new engine in SearXNG
|
||||
title: ''
|
||||
labels: enhancement, engine request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- PLEASE FILL THESE FIELDS, IT REALLY HELPS THE MAINTAINERS OF SearXNG -->
|
||||
|
||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
||||
**Working URL to the engine**
|
||||
<!-- Please check if the engine is responding correctly before submitting it. -->
|
||||
|
||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
||||
-->
|
||||
**Why do you want to add this engine?**
|
||||
<!-- What's special about this engine? Is it open source or libre? -->
|
||||
|
||||
### Working URL to the engine
|
||||
**Features of this engine**
|
||||
<!-- Features of this engine: Doesn't track its users, fast, easy to integrate, ... -->
|
||||
|
||||
<!-- Please check if the engine is responding correctly before submitting -->
|
||||
**How can SearXNG fetch the information from this engine?**
|
||||
<!-- List API URL, example code (using the correct markdown) and more
|
||||
that could be useful for the developers in order to implement this engine.
|
||||
If you don't know what to write, let this part blank. -->
|
||||
|
||||
### Why do you want to add this engine?
|
||||
**Applicable category of this engine**
|
||||
<!-- Where should this new engine fit in SearXNG? Current categories in SearXNG:
|
||||
general, files, images, it, map, music, news, science, social media and videos.
|
||||
You can add multiple categories at the same time. -->
|
||||
|
||||
<!-- What's special about this engine? -->
|
||||
|
||||
### Features of this engine
|
||||
|
||||
<!-- Features of this engine: Serves special content, is fast, is easy to
|
||||
integrate, ... ? -->
|
||||
|
||||
### How can SearXNG fetch results from this engine?
|
||||
|
||||
<!-- List API URL, example code and more that could be useful for the developers
|
||||
in order to implement this engine. If you don't know what to write, let
|
||||
this part blank. -->
|
||||
|
||||
### Applicable category of this engine
|
||||
|
||||
<!-- Where should this new engine fit in SearXNG? Current categories in
|
||||
SearXNG: general, files, images, it, map, music, news, science, social
|
||||
media and videos. -->
|
||||
|
||||
### Additional context
|
||||
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
|
||||
- [ ] I read the [AI Policy] and hereby confirm that this issue conforms with the policy.
|
||||
**Additional context**
|
||||
<!-- Add any other context about this engine here. -->
|
||||
|
||||
37
.github/ISSUE_TEMPLATE/feature-request.md
vendored
37
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@@ -1,32 +1,21 @@
|
||||
---
|
||||
name: "Feature request"
|
||||
about: "Request a new feature in SearXNG"
|
||||
labels: ["new feature"]
|
||||
type: "feature"
|
||||
name: Feature request
|
||||
about: Request a new feature in SearXNG
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- PLEASE FILL THESE FIELDS, IT REALLY HELPS THE MAINTAINERS OF SearXNG -->
|
||||
|
||||
_Replace this placeholder with a concise description of the feature._
|
||||
|
||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
||||
|
||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
||||
-->
|
||||
|
||||
### Is your feature request related to a problem?
|
||||
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always
|
||||
frustrated when [...] -->
|
||||
|
||||
### Describe the solution you'd like
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
### Describe alternatives you've considered
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
|
||||
- [ ] I read the [AI Policy] and hereby confirm that this issue conforms with the policy.
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
|
||||
47
.github/workflows/checker.yml
vendored
Normal file
47
.github/workflows/checker.yml
vendored
Normal 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
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
37
.github/workflows/cleanup.yml
vendored
Normal 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"
|
||||
170
.github/workflows/container.yml
vendored
170
.github/workflows/container.yml
vendored
@@ -18,98 +18,158 @@ concurrency:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Organization GHCR
|
||||
packages: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
PYTHON_VERSION: "3.13"
|
||||
|
||||
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
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:
|
||||
if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch'
|
||||
name: Build (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: build-base
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# Faster runners first to cache arch independent wheels
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
march: amd64
|
||||
os: ubuntu-24.04
|
||||
emulation: false
|
||||
- arch: arm64
|
||||
march: arm64
|
||||
os: ubuntu-24.04-arm
|
||||
emulation: false
|
||||
- arch: armv7
|
||||
march: arm64
|
||||
os: ubuntu-24.04-arm
|
||||
emulation: true
|
||||
|
||||
permissions:
|
||||
# Organization GHCR
|
||||
packages: write
|
||||
# Clean key cache step
|
||||
actions: write
|
||||
|
||||
outputs:
|
||||
docker_tag: ${{ steps.build.outputs.docker_tag }}
|
||||
git_url: ${{ steps.build.outputs.git_url }}
|
||||
|
||||
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
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
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 }}-
|
||||
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
|
||||
path: "./local/"
|
||||
|
||||
- name: Get date
|
||||
id: date
|
||||
run: echo "date=$(date +'%Y%m%d')" >>$GITHUB_OUTPUT
|
||||
|
||||
- name: Setup cache container
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
- name: Restore cache container mounts
|
||||
id: cache-container-mounts
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
key: "container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: |
|
||||
container-${{ matrix.arch }}-${{ steps.date.outputs.date }}-
|
||||
container-${{ matrix.arch }}-
|
||||
path: "/var/tmp/buildah-cache-*/*"
|
||||
key: "container-mounts-${{ hashFiles('./container/*.dockerfile') }}"
|
||||
restore-keys: "container-mounts-"
|
||||
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 }}
|
||||
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
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: "${{ github.repository_owner }}"
|
||||
@@ -121,6 +181,15 @@ jobs:
|
||||
OVERRIDE_ARCH: "${{ matrix.arch }}"
|
||||
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:
|
||||
name: Test (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -141,16 +210,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
- if: ${{ matrix.emulation }}
|
||||
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
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: "${{ github.repository_owner }}"
|
||||
@@ -171,27 +240,28 @@ jobs:
|
||||
- test
|
||||
|
||||
permissions:
|
||||
# Organization GHCR
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: "${{ github.repository_owner }}"
|
||||
password: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: "docker.io"
|
||||
username: "${{ secrets.DOCKER_USER }}"
|
||||
password: "${{ secrets.DOCKER_TOKEN }}"
|
||||
username: "${{ secrets.DOCKERHUB_USERNAME }}"
|
||||
password: "${{ secrets.DOCKERHUB_TOKEN }}"
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
|
||||
14
.github/workflows/data-update.yml
vendored
14
.github/workflows/data-update.yml
vendored
@@ -15,7 +15,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
PYTHON_VERSION: "3.13"
|
||||
|
||||
jobs:
|
||||
data:
|
||||
@@ -33,7 +33,6 @@ jobs:
|
||||
- update_engine_traits.py
|
||||
- update_wikidata_units.py
|
||||
- update_engine_descriptions.py
|
||||
- update_gsa_useragents.py
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -41,21 +40,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
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 }}-
|
||||
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup venv
|
||||
@@ -66,7 +64,7 @@ jobs:
|
||||
|
||||
- name: Create PR
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
author: "searxng-bot <searxng-bot@users.noreply.github.com>"
|
||||
committer: "searxng-bot <searxng-bot@users.noreply.github.com>"
|
||||
|
||||
18
.github/workflows/documentation.yml
vendored
18
.github/workflows/documentation.yml
vendored
@@ -19,7 +19,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
PYTHON_VERSION: "3.13"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -32,36 +32,32 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
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 }}-
|
||||
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup dependencies
|
||||
run: sudo ./utils/searxng.sh install buildhost
|
||||
|
||||
- name: Setup venv
|
||||
run: make V=1 install
|
||||
|
||||
- name: Build documentation
|
||||
run: make V=1 docs.html
|
||||
run: make V=1 docs.clean docs.html
|
||||
|
||||
- if: github.ref_name == 'master'
|
||||
name: Release
|
||||
uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4.8.0
|
||||
uses: JamesIves/github-pages-deploy-action@6c2d9db40f9296374acc17b90404b6e8864128c8 # v4.7.3
|
||||
with:
|
||||
folder: "dist/docs"
|
||||
branch: "gh-pages"
|
||||
|
||||
30
.github/workflows/integration.yml
vendored
30
.github/workflows/integration.yml
vendored
@@ -18,7 +18,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
PYTHON_VERSION: "3.13"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -27,28 +27,28 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
- "3.14"
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "${{ matrix.python-version }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
||||
restore-keys: |
|
||||
python-${{ matrix.python-version }}-${{ runner.arch }}-
|
||||
restore-keys: "python-${{ matrix.python-version }}-${{ runner.arch }}-"
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup venv
|
||||
@@ -62,39 +62,35 @@ jobs:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: "./.nvmrc"
|
||||
|
||||
- name: Setup cache Node.js
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
key: "nodejs-${{ runner.arch }}-${{ hashFiles('./.nvmrc', './package.json') }}"
|
||||
path: "./client/simple/node_modules/"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
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 }}-
|
||||
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup venv
|
||||
run: make V=1 install
|
||||
|
||||
- name: Lint
|
||||
run: make themes.lint
|
||||
|
||||
- name: Build
|
||||
run: make themes.all
|
||||
|
||||
24
.github/workflows/l10n.yml
vendored
24
.github/workflows/l10n.yml
vendored
@@ -22,7 +22,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
PYTHON_VERSION: "3.13"
|
||||
|
||||
jobs:
|
||||
update:
|
||||
@@ -35,22 +35,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
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 }}-
|
||||
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup venv
|
||||
@@ -83,22 +82,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "${{ env.PYTHON_VERSION }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: Setup cache Python
|
||||
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
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 }}-
|
||||
restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
|
||||
path: "./local/"
|
||||
|
||||
- name: Setup venv
|
||||
@@ -119,7 +117,7 @@ jobs:
|
||||
|
||||
- name: Create PR
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
author: "searxng-bot <searxng-bot@users.noreply.github.com>"
|
||||
committer: "searxng-bot <searxng-bot@users.noreply.github.com>"
|
||||
@@ -132,7 +130,7 @@ jobs:
|
||||
body: |
|
||||
[l10n] update translations from Weblate
|
||||
labels: |
|
||||
area:i18n
|
||||
translation
|
||||
|
||||
- name: Display information
|
||||
run: |
|
||||
|
||||
10
.github/workflows/security.yml
vendored
10
.github/workflows/security.yml
vendored
@@ -24,16 +24,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: "false"
|
||||
|
||||
- name: Sync GHCS from Docker Scout
|
||||
uses: docker/scout-action@7520205ff60037fdc436b40b6a1d1e55a839ec2d # v1.22.0
|
||||
uses: docker/scout-action@f8c776824083494ab0d56b8105ba2ca85c86e4de # v1.18.2
|
||||
with:
|
||||
organization: "searxng"
|
||||
dockerhub-user: "${{ secrets.DOCKER_USER }}"
|
||||
dockerhub-password: "${{ secrets.DOCKER_TOKEN }}"
|
||||
dockerhub-user: "${{ secrets.DOCKERHUB_USERNAME }}"
|
||||
dockerhub-password: "${{ secrets.DOCKERHUB_TOKEN }}"
|
||||
image: "registry://ghcr.io/searxng/searxng:latest"
|
||||
command: "cves"
|
||||
sarif-file: "./scout.sarif"
|
||||
@@ -41,6 +41,6 @@ jobs:
|
||||
write-comment: "false"
|
||||
|
||||
- name: Upload SARIFs
|
||||
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
uses: github/codeql-action/upload-sarif@a4e1a019f5e24960714ff6296aee04b736cbc3cf # v3.29.6
|
||||
with:
|
||||
sarif_file: "./scout.sarif"
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
[[language]]
|
||||
name = "python"
|
||||
language-servers = ["basedpyright", "pylsp"]
|
||||
auto-format = true
|
||||
|
||||
[language-server.pylsp.config.pylsp]
|
||||
plugins.pylint.enabled = true
|
||||
plugins.isort.enabled = true
|
||||
plugins.black.enabled = true
|
||||
plugins.black.skip_string_normalization = true
|
||||
plugins.black.line_length = 120
|
||||
@@ -162,7 +162,7 @@ no-docstring-rgx=^_
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Regular expression matching correct variable names
|
||||
variable-rgx=([a-zA-Z0-9_]*)$
|
||||
variable-rgx=(([a-z][a-zA-Z0-9_]{2,30})|(_[a-z0-9_]*)|([a-z]))$
|
||||
|
||||
|
||||
[FORMAT]
|
||||
@@ -311,7 +311,7 @@ dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=yes
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
|
||||
4
.tool-versions
Normal file
4
.tool-versions
Normal file
@@ -0,0 +1,4 @@
|
||||
nodejs 23.5.0
|
||||
python 3.13.1
|
||||
shellcheck 0.10.0
|
||||
sqlite 3.47.2
|
||||
@@ -1,20 +0,0 @@
|
||||
.. SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
AI Policy
|
||||
=========
|
||||
|
||||
Restrictions on Generative AI Usage
|
||||
-----------------------------------
|
||||
- **All AI usage in any form must be disclosed.** You must state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted.
|
||||
- **The human-in-the-loop must fully understand all code.** If you use generative AI tools as an aid in developing code or documentation changes, ensure that you fully understand the proposed changes and can explain why they are the correct approach.
|
||||
- **AI should never be the main author of the PR.** AI may be used as a tool to help with developing, but the human contribution to the code changes should always be reasonably larger than the part written by AI. For example, you should be the one that decides about the structure of the PR, not the LLM.
|
||||
- **Issues and PR descriptions must be fully human-written.** Do not post output from Large Language Models or similar generative AI as comments on any of our discussion forums (e.g. GitHub Issues, Matrix, ...), as such comments tend to be formulaic and low content. If you're a not a native English speaker, using AI for translating self-written issue texts to English is okay, but please keep the wording as close as possible to the original wording.
|
||||
- **Bad AI drivers will be denounced.** People who produce bad contributions that are clearly AI (slop) will be blocked for all future contributions.
|
||||
|
||||
There are Humans Here
|
||||
---------------------
|
||||
Every discussion, issue, and pull request is read and reviewed by humans. It is a boundary point at which people interact with each other and the work done. It is rude and disrespectful to approach this boundary with low-effort, unqualified work, since it puts the burden of validation on the maintainer.
|
||||
|
||||
It takes a lot of maintainer time and energy to review AI-generated contributions! Sending the output of an LLM to open source project maintainers extracts work from them in the form of design and code review, so we call this kind of contribution an "extractive contribution".
|
||||
|
||||
The *golden rule* is that a contribution should be worth more to the project than the time it takes to review it, which is usually not the case if large parts of your PR were written by LLMs.
|
||||
@@ -178,4 +178,3 @@ features or generally made SearXNG better:
|
||||
- `Bearz314 <https://github.com/bearz314>`_
|
||||
- Tommaso Colella `<https://github.com/gioleppe>`
|
||||
- @AgentScrubbles
|
||||
- Filip Mikina `<https://github.com/fiffek>`
|
||||
|
||||
49
CONTRIBUTING.md
Normal file
49
CONTRIBUTING.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# How to contribute
|
||||
|
||||
## Resources in the documentation
|
||||
|
||||
* [Development quickstart](https://docs.searxng.org/dev/quickstart.html)
|
||||
* [Contribution guide](https://docs.searxng.org/dev/contribution_guide.html)
|
||||
|
||||
## Submitting PRs
|
||||
|
||||
Please follow the provided PR template when writing a description for your changes.
|
||||
|
||||
Do not take criticism personally. When you get feedback, it is about your work,
|
||||
not your character, personality, etc. Keep in mind we all want to make the project better.
|
||||
|
||||
When something is not clear, please ask questions to clear things up.
|
||||
|
||||
If you would like to introduce a big architectural changes or do a refactoring
|
||||
either in the codebase or the development tools, please open an issue with a proposal
|
||||
first. This way we can think together about the problem and probably come up
|
||||
with a better solution.
|
||||
|
||||
## Coding conventions and guidelines
|
||||
|
||||
### Commit messages
|
||||
|
||||
* Always write descriptive commit messages ("fix bug" is not acceptable).
|
||||
* Use the present tense ("Add feature" not "Added feature").
|
||||
* Use the imperative mood ("Move cursor to..." not "Moves cursor to...").
|
||||
* Limit the first line to 72 characters or less.
|
||||
* Include the number of the issue you are fixing.
|
||||
|
||||
### Coding guidelines
|
||||
|
||||
As a Python project, we must follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) and [PEP 20](https://www.python.org/dev/peps/pep-0020/) guidelines.
|
||||
|
||||
Furthermore, follow the Clean code conventions. The most important
|
||||
in this project are the following rules:
|
||||
|
||||
* Simpler is better. [KISS principle](https://en.wikipedia.org/wiki/KISS_principle)
|
||||
* Be consistent.
|
||||
* Every function must do one thing.
|
||||
* Use descriptive names for functions and variables.
|
||||
* Always look for the root cause.
|
||||
* Keep configurable data high level.
|
||||
* Avoid negative conditionals.
|
||||
* Prefer fewer arguments.
|
||||
* Do not add obvious comment to code.
|
||||
* Do not comment out code, just delete lines.
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
.. SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.. _Quickstart guide: https://docs.searxng.org/dev/quickstart.html
|
||||
.. _Commits guide: https://docs.searxng.org/dev/commits.html
|
||||
.. _AI Policy: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
.. _Weblate: https://translate.codeberg.org/projects/searxng/searxng/
|
||||
.. _GitHub Codespaces: https://docs.github.com/en/codespaces/overview
|
||||
.. _120 hours per month: https://github.com/settings/billing
|
||||
.. _list of existing Codespaces: https://github.com/codespaces
|
||||
|
||||
Thank you for your interest in SearXNG.
|
||||
|
||||
Have a look at our `Quickstart guide`_, it's very easy to contribute.
|
||||
|
||||
Further information on *how-to* can be found
|
||||
`here <https://docs.searxng.org/dev/index.html>`_.
|
||||
|
||||
Translations
|
||||
============
|
||||
|
||||
Help translate SearXNG at `Weblate`_.
|
||||
|
||||
.. image:: https://translate.codeberg.org/widget/searxng/searxng/horizontal-auto.svg
|
||||
:target: https://translate.codeberg.org/engage/searxng/
|
||||
:alt: Weblate
|
||||
:width: 768px
|
||||
|
||||
Cloud development
|
||||
=================
|
||||
|
||||
You can contribute from your browser using `GitHub Codespaces`_:
|
||||
|
||||
- Fork the repository.
|
||||
- Click on the ``<> Code`` green button.
|
||||
- Click on the ``Codespaces`` tab instead of ``Local``.
|
||||
- Click on ``Create codespace on master``.
|
||||
- VSCode is going to start in the browser.
|
||||
- Wait for ``git pull && make install`` to appear and then disappear.
|
||||
- You have `120 hours per month`_ (see also your `list of existing Codespaces`_).
|
||||
- You can start SearXNG using ``make run`` in the terminal or by pressing ``Ctrl+Shift+B``.
|
||||
|
||||
How-to contribute
|
||||
=================
|
||||
|
||||
Submitting pull requests
|
||||
------------------------
|
||||
|
||||
Please follow the provided PR template when writing a description for your
|
||||
changes.
|
||||
|
||||
Do not take criticism personally. When you get feedback, it is about your work,
|
||||
not your character or personality. Keep in mind we all want to make SearXNG
|
||||
better.
|
||||
|
||||
When something is not clear, please ask questions to clear things up.
|
||||
|
||||
If you would like to introduce a big architectural change or do a refactor,
|
||||
either in the codebase or the development tooling, please open an issue with a
|
||||
proposal first. This way we can think together about the problem and probably
|
||||
come up with a better solution.
|
||||
|
||||
Coding conventions and guidelines
|
||||
---------------------------------
|
||||
|
||||
Commit messages
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
- Always write descriptive commit messages *("fix bug" is not acceptable)*.
|
||||
- Use the present tense *("Add feature", not "Added feature")*.
|
||||
- Use the imperative mood *("Move cursor to...", not "Moves cursor to...")*.
|
||||
- Limit the first line (commit title) to 72 characters or less.
|
||||
|
||||
See `Commits guide`_ for more details.
|
||||
|
||||
Coding guidelines
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
As a Python project, we must follow `PEP 8 <https://www.python.org/dev/peps/pep-0008/>`_
|
||||
and `PEP 20 <https://www.python.org/dev/peps/pep-0020/>`_ guidelines.
|
||||
|
||||
Furthermore, follow Clean Code conventions. The most important
|
||||
rules in this project are:
|
||||
|
||||
- Simpler is better. `KISS principle <https://en.wikipedia.org/wiki/KISS_principle>`_
|
||||
- Be consistent.
|
||||
- Every function must do one thing.
|
||||
- Use descriptive names for functions and variables.
|
||||
- Always look for the root cause.
|
||||
- Keep configurable data high level.
|
||||
- Avoid negative conditionals.
|
||||
- Prefer fewer arguments.
|
||||
- Do not add obvious comments to code.
|
||||
- Do not comment out code, delete lines instead.
|
||||
|
||||
AI Policy
|
||||
~~~~~~~~~
|
||||
|
||||
For our policy on the use of AI tools, please read `AI Policy`_.
|
||||
15
Makefile
15
Makefile
@@ -17,6 +17,7 @@ help:
|
||||
@echo 'install - developer install of SearxNG into virtualenv'
|
||||
@echo 'uninstall - uninstall developer installation'
|
||||
@echo 'clean - clean up working tree'
|
||||
@echo 'search.checker - check search engines'
|
||||
@echo 'test - run shell & CI tests'
|
||||
@echo 'test.shell - test shell scripts'
|
||||
@echo 'ci.test - run CI tests'
|
||||
@@ -38,9 +39,15 @@ clean: py.clean docs.clean node.clean nvm.clean go.clean test.clean
|
||||
$(Q)find . -name '*~' -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
|
||||
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 test.pylint test.unit test.robot test.rst test.shell test.shfmt
|
||||
ci.test: test test.pybabel
|
||||
test.shell:
|
||||
$(Q)shellcheck -x -s dash \
|
||||
@@ -63,7 +70,7 @@ format: format.python format.shell
|
||||
# wrap ./manage script
|
||||
|
||||
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 += podman.build
|
||||
MANAGE += docker.build docker.buildx
|
||||
@@ -73,8 +80,8 @@ MANAGE += node.env node.env.dev node.clean
|
||||
MANAGE += py.build py.clean
|
||||
MANAGE += pyenv pyenv.install pyenv.uninstall
|
||||
MANAGE += format.python format.shell
|
||||
MANAGE += test.yamllint test.pylint test.black test.pybabel test.unit test.coverage test.robot test.rst test.clean test.themes test.pyright test.pyright_modified test.shfmt
|
||||
MANAGE += themes.all themes.simple themes.simple.analyze themes.fix themes.lint themes.test
|
||||
MANAGE += test.yamllint test.pylint test.black test.pybabel test.unit test.coverage test.robot test.rst test.clean test.themes test.pyright test.shfmt
|
||||
MANAGE += themes.all themes.simple themes.fix themes.lint themes.test
|
||||
MANAGE += static.build.commit static.build.drop static.build.restore
|
||||
MANAGE += nvm.install nvm.clean nvm.status nvm.nodejs
|
||||
MANAGE += go.env.dev go.clean
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
||||
## What does this PR do?
|
||||
|
||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
||||
-->
|
||||
<!-- MANDATORY -->
|
||||
|
||||
### What does this PR do?
|
||||
<!-- explain the changes in your PR, algorithms, design, architecture -->
|
||||
|
||||
<!-- Explain the motivation and changes in your pull request. -->
|
||||
## Why is this change important?
|
||||
|
||||
### How to test this PR locally?
|
||||
<!-- MANDATORY -->
|
||||
|
||||
<!-- Commands to run the tests or instructions to test the changes. Are there
|
||||
any edge cases (environment, language, or other contexts) to take into
|
||||
account? -->
|
||||
<!-- explain the motivation behind your PR -->
|
||||
|
||||
### Related issues
|
||||
## How to test this PR locally?
|
||||
|
||||
<!-- commands to run the tests or instructions to test the changes -->
|
||||
|
||||
## Author's checklist
|
||||
|
||||
<!-- additional notes for reviewers -->
|
||||
|
||||
## Related issues
|
||||
|
||||
<!--
|
||||
Closes: #234
|
||||
Closes #234
|
||||
-->
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
<!-- ⚠️ Bad AI drivers will be denounced: People who produce bad contributions
|
||||
that are clearly AI (slop) will be blocked for all future contributions.
|
||||
-->
|
||||
|
||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
||||
|
||||
- [ ] **I hereby confirm that this PR conforms with the [AI Policy].**
|
||||
|
||||
If I have used AI tools for working on the changes in this PR, I will
|
||||
attach a list of all AI tools I used and how I used them. I hereby confirm
|
||||
that I haven't used any other tools than the ones I mention below.
|
||||
|
||||
134
README.rst
134
README.rst
@@ -1,62 +1,130 @@
|
||||
.. SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.. _metasearch engine: https://en.wikipedia.org/wiki/Metasearch_engine
|
||||
.. _Installation guide: https://docs.searxng.org/admin/installation.html
|
||||
.. _Configuration guide: https://docs.searxng.org/admin/settings/index.html
|
||||
.. _CONTRIBUTING: https://github.com/searxng/searxng/blob/master/CONTRIBUTING.rst
|
||||
.. _LICENSE: https://github.com/searxng/searxng/blob/master/LICENSE
|
||||
----
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/searxng/searxng/master/client/simple/src/brand/searxng.svg
|
||||
:target: https://searxng.org
|
||||
:target: https://docs.searxng.org/
|
||||
:alt: SearXNG
|
||||
:width: 512px
|
||||
:width: 100%
|
||||
:align: center
|
||||
|
||||
----
|
||||
|
||||
SearXNG is a `metasearch engine`_. Users are neither tracked nor profiled.
|
||||
Privacy-respecting, hackable `metasearch engine`_
|
||||
|
||||
.. image:: https://img.shields.io/badge/organization-3050ff?style=flat-square&logo=searxng&logoColor=fff&cacheSeconds=86400
|
||||
:target: https://github.com/searxng
|
||||
:alt: Organization
|
||||
Searx.space_ lists ready-to-use running instances.
|
||||
|
||||
.. image:: https://img.shields.io/badge/documentation-3050ff?style=flat-square&logo=readthedocs&logoColor=fff&cacheSeconds=86400
|
||||
:target: https://docs.searxng.org
|
||||
:alt: Documentation
|
||||
A user_, admin_ and developer_ handbook is available on the homepage_.
|
||||
|
||||
.. image:: https://img.shields.io/github/license/searxng/searxng?style=flat-square&label=license&color=3050ff&cacheSeconds=86400
|
||||
|SearXNG install|
|
||||
|SearXNG homepage|
|
||||
|SearXNG wiki|
|
||||
|AGPL License|
|
||||
|Issues|
|
||||
|commits|
|
||||
|weblate|
|
||||
|SearXNG logo|
|
||||
|
||||
----
|
||||
|
||||
.. _searx.space: https://searx.space
|
||||
.. _user: https://docs.searxng.org/user
|
||||
.. _admin: https://docs.searxng.org/admin
|
||||
.. _developer: https://docs.searxng.org/dev
|
||||
.. _homepage: https://docs.searxng.org/
|
||||
.. _metasearch engine: https://en.wikipedia.org/wiki/Metasearch_engine
|
||||
|
||||
.. |SearXNG logo| image:: https://raw.githubusercontent.com/searxng/searxng/master/client/simple/src/brand/searxng-wordmark.svg
|
||||
:target: https://docs.searxng.org/
|
||||
:width: 5%
|
||||
|
||||
.. |SearXNG install| image:: https://img.shields.io/badge/-install-blue
|
||||
:target: https://docs.searxng.org/admin/installation.html
|
||||
|
||||
.. |SearXNG homepage| image:: https://img.shields.io/badge/-homepage-blue
|
||||
:target: https://docs.searxng.org/
|
||||
|
||||
.. |SearXNG wiki| image:: https://img.shields.io/badge/-wiki-blue
|
||||
:target: https://github.com/searxng/searxng/wiki
|
||||
|
||||
.. |AGPL License| image:: https://img.shields.io/badge/license-AGPL-blue.svg
|
||||
:target: https://github.com/searxng/searxng/blob/master/LICENSE
|
||||
:alt: License
|
||||
|
||||
.. image:: https://img.shields.io/github/commit-activity/y/searxng/searxng/master?style=flat-square&label=commits&color=3050ff&cacheSeconds=3600
|
||||
:target: https://github.com/searxng/searxng/commits/master/
|
||||
:alt: Commits
|
||||
.. |Issues| image:: https://img.shields.io/github/issues/searxng/searxng?color=yellow&label=issues
|
||||
:target: https://github.com/searxng/searxng/issues
|
||||
|
||||
.. image:: https://img.shields.io/weblate/progress/searxng?server=https%3A%2F%2Ftranslate.codeberg.org&style=flat-square&label=translated&color=3050ff&cacheSeconds=86400
|
||||
.. |PR| image:: https://img.shields.io/github/issues-pr-raw/searxng/searxng?color=yellow&label=PR
|
||||
:target: https://github.com/searxng/searxng/pulls
|
||||
|
||||
.. |commits| image:: https://img.shields.io/github/commit-activity/y/searxng/searxng?color=yellow&label=commits
|
||||
:target: https://github.com/searxng/searxng/commits/master
|
||||
|
||||
.. |weblate| image:: https://translate.codeberg.org/widgets/searxng/-/searxng/svg-badge.svg
|
||||
:target: https://translate.codeberg.org/projects/searxng/
|
||||
:alt: Translated
|
||||
|
||||
|
||||
Contact
|
||||
=======
|
||||
|
||||
Ask questions or chat with the SearXNG community (this not a chatbot) on
|
||||
|
||||
IRC
|
||||
`#searxng on libera.chat <https://web.libera.chat/?channel=#searxng>`_
|
||||
which is bridged to Matrix.
|
||||
|
||||
Matrix
|
||||
`#searxng:matrix.org <https://matrix.to/#/#searxng:matrix.org>`_
|
||||
|
||||
|
||||
Setup
|
||||
=====
|
||||
|
||||
To install SearXNG, see `Installation guide`_.
|
||||
- A well maintained `Docker image`_, also built for ARM64 and ARM/v7
|
||||
architectures.
|
||||
- Alternatively there are *up to date* `installation scripts`_.
|
||||
- For individual setup consult our detailed `Step by step`_ instructions.
|
||||
- To fine-tune your instance, take a look at the `Administrator documentation`_.
|
||||
|
||||
To fine-tune SearXNG, see `Configuration guide`_.
|
||||
.. _Administrator documentation: https://docs.searxng.org/admin/index.html
|
||||
.. _Step by step: https://docs.searxng.org/admin/installation-searxng.html
|
||||
.. _installation scripts: https://docs.searxng.org/admin/installation-scripts.html
|
||||
.. _Docker image: https://github.com/searxng/searxng-docker
|
||||
|
||||
Further information on *how-to* can be found `here <https://docs.searxng.org/admin/index.html>`_.
|
||||
Translations
|
||||
============
|
||||
|
||||
Connect
|
||||
=======
|
||||
.. _Weblate: https://translate.codeberg.org/projects/searxng/searxng/
|
||||
|
||||
If you have questions or want to connect with others in the community:
|
||||
Help translate SearXNG at `Weblate`_
|
||||
|
||||
.. figure:: https://translate.codeberg.org/widgets/searxng/-/multi-auto.svg
|
||||
:target: https://translate.codeberg.org/projects/searxng/
|
||||
|
||||
- `#searxng:matrix.org <https://matrix.to/#/#searxng:matrix.org>`_
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
See CONTRIBUTING_ for more details.
|
||||
.. _development quickstart: https://docs.searxng.org/dev/quickstart.html
|
||||
.. _developer documentation: https://docs.searxng.org/dev/index.html
|
||||
|
||||
License
|
||||
=======
|
||||
Are you a developer? Have a look at our `development quickstart`_ guide, it's
|
||||
very easy to contribute. Additionally we have a `developer documentation`_.
|
||||
|
||||
This project is licensed under the GNU Affero General Public License (AGPL-3.0).
|
||||
See LICENSE_ for more details.
|
||||
|
||||
Codespaces
|
||||
==========
|
||||
|
||||
You can contribute from your browser using `GitHub Codespaces`_:
|
||||
|
||||
- Fork the repository
|
||||
- Click on the ``<> Code`` green button
|
||||
- Click on the ``Codespaces`` tab instead of ``Local``
|
||||
- Click on ``Create codespace on master``
|
||||
- VSCode is going to start in the browser
|
||||
- Wait for ``git pull && make install`` to appear and then disappear
|
||||
- You have `120 hours per month`_ (see also your `list of existing Codespaces`_)
|
||||
- You can start SearXNG using ``make run`` in the terminal or by pressing ``Ctrl+Shift+B``
|
||||
|
||||
.. _GitHub Codespaces: https://docs.github.com/en/codespaces/overview
|
||||
.. _120 hours per month: https://github.com/settings/billing
|
||||
.. _list of existing Codespaces: https://github.com/codespaces
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
"extends": ["stylelint-config-standard-less"],
|
||||
"rules": {
|
||||
"at-rule-no-vendor-prefix": null,
|
||||
"at-rule-prelude-no-invalid": null,
|
||||
"declaration-empty-line-before": null,
|
||||
"declaration-property-value-no-unknown": null,
|
||||
"no-invalid-position-at-import-rule": null,
|
||||
"prettier/prettier": true,
|
||||
"property-no-vendor-prefix": null,
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
{
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": ["**", "!node_modules", "!src/brand", "!src/svg"]
|
||||
"includes": ["**", "!dist/**", "!node_modules/**"],
|
||||
"ignoreUnknown": true
|
||||
},
|
||||
"vcs": {
|
||||
"clientKind": "git",
|
||||
"enabled": false,
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": {
|
||||
"preset": "recommended",
|
||||
"source": {
|
||||
"useSortedAttributes": "on",
|
||||
"useSortedProperties": "on"
|
||||
}
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": true,
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
@@ -27,119 +28,7 @@
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"preset": "recommended",
|
||||
"complexity": {
|
||||
"noForEach": "error",
|
||||
"noImplicitCoercions": "error",
|
||||
"noRedundantDefaultExport": "error",
|
||||
"noUselessCatchBinding": "error",
|
||||
"noUselessUndefined": "error",
|
||||
"useArrayFind": "error",
|
||||
"useSimplifiedLogicExpression": "error"
|
||||
},
|
||||
"correctness": {
|
||||
"noGlobalDirnameFilename": "error",
|
||||
"useImportExtensions": "error",
|
||||
"useJsonImportAttributes": "error",
|
||||
"useSingleJsDocAsterisk": "error"
|
||||
},
|
||||
"nursery": {
|
||||
"noFloatingPromises": "warn",
|
||||
"noMisusedPromises": "warn",
|
||||
"useAwaitThenable": "off",
|
||||
"useExhaustiveSwitchCases": "warn",
|
||||
"useExplicitType": "off",
|
||||
"useRegexpExec": "warn"
|
||||
},
|
||||
"performance": {
|
||||
"noAwaitInLoops": "error",
|
||||
"noBarrelFile": "error",
|
||||
"noDelete": "error",
|
||||
"noNamespaceImport": "error",
|
||||
"noReExportAll": "error",
|
||||
"useTopLevelRegex": "error"
|
||||
},
|
||||
"style": {
|
||||
"noCommonJs": "error",
|
||||
"noEnum": "error",
|
||||
"noImplicitBoolean": "error",
|
||||
"noIncrementDecrement": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noMultiAssign": "error",
|
||||
"noMultilineString": "error",
|
||||
"noNamespace": "error",
|
||||
"noNegationElse": "error",
|
||||
"noNestedTernary": "error",
|
||||
"noParameterAssign": "error",
|
||||
"noParameterProperties": "error",
|
||||
"noSubstr": "error",
|
||||
"noUnusedTemplateLiteral": "error",
|
||||
"noUselessElse": "error",
|
||||
"noYodaExpression": "error",
|
||||
"useAsConstAssertion": "error",
|
||||
"useAtIndex": "error",
|
||||
"useCollapsedElseIf": "error",
|
||||
"useCollapsedIf": "error",
|
||||
"useConsistentArrayType": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"syntax": "shorthand"
|
||||
}
|
||||
},
|
||||
"useConsistentBuiltinInstantiation": "error",
|
||||
"useConsistentEnumValueType": "error",
|
||||
"useConsistentMemberAccessibility": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"accessibility": "explicit"
|
||||
}
|
||||
},
|
||||
"useConsistentObjectDefinitions": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"syntax": "explicit"
|
||||
}
|
||||
},
|
||||
"useConsistentTypeDefinitions": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"style": "type"
|
||||
}
|
||||
},
|
||||
"useDefaultSwitchClause": "error",
|
||||
"useDestructuring": "error",
|
||||
"useExplicitLengthCheck": "error",
|
||||
"useForOf": "error",
|
||||
"useGroupedAccessorPairs": "error",
|
||||
"useNumberNamespace": "error",
|
||||
"useNumericSeparators": "error",
|
||||
"useObjectSpread": "error",
|
||||
"useReadonlyClassProperties": "error",
|
||||
"useSelfClosingElements": "error",
|
||||
"useShorthandAssign": "error",
|
||||
"useSingleVarDeclarator": "error",
|
||||
"useThrowNewError": "error",
|
||||
"useThrowOnlyError": "error",
|
||||
"useTrimStartEnd": "error",
|
||||
"useUnifiedTypeSignatures": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noBitwiseOperators": "error",
|
||||
"noConstantBinaryExpressions": "error",
|
||||
"noDeprecatedImports": "error",
|
||||
"noEmptyBlockStatements": "error",
|
||||
"noEqualsToNull": "error",
|
||||
"noEvolvingTypes": "error",
|
||||
"noForIn": "error",
|
||||
"noImportCycles": "error",
|
||||
"noNestedPromises": "error",
|
||||
"noParametersOnlyUsedInRecursion": "error",
|
||||
"noReturnAssign": "error",
|
||||
"noUnassignedVariables": "error",
|
||||
"noVar": "error",
|
||||
"useNumberToFixedDigitsArgument": "error",
|
||||
"useStaticResponseMethods": "error"
|
||||
}
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
this file is generated automatically by searxng_extra/update/update_pygments.py
|
||||
using pygments version 2.20.0:
|
||||
|
||||
./manage templates.simple.pygments
|
||||
using pygments version 2.19.2
|
||||
*/
|
||||
|
||||
|
||||
.code-highlight {
|
||||
|
||||
pre { line-height: 125%; }
|
||||
pre { line-height: 100%; }
|
||||
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
@@ -89,89 +85,89 @@
|
||||
.code-highlight-dark(){
|
||||
.code-highlight {
|
||||
|
||||
pre { line-height: 125%; }
|
||||
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
.hll { background-color: #49483e }
|
||||
.c { color: #959077 } /* Comment */
|
||||
.err { color: #ED007E; background-color: #1E0010 } /* Error */
|
||||
.esc { color: #F8F8F2 } /* Escape */
|
||||
.g { color: #F8F8F2 } /* Generic */
|
||||
.k { color: #66D9EF } /* Keyword */
|
||||
.l { color: #AE81FF } /* Literal */
|
||||
.n { color: #F8F8F2 } /* Name */
|
||||
.o { color: #FF4689 } /* Operator */
|
||||
.x { color: #F8F8F2 } /* Other */
|
||||
.p { color: #F8F8F2 } /* Punctuation */
|
||||
.ch { color: #959077 } /* Comment.Hashbang */
|
||||
.cm { color: #959077 } /* Comment.Multiline */
|
||||
.cp { color: #959077 } /* Comment.Preproc */
|
||||
.cpf { color: #959077 } /* Comment.PreprocFile */
|
||||
.c1 { color: #959077 } /* Comment.Single */
|
||||
.cs { color: #959077 } /* Comment.Special */
|
||||
.gd { color: #FF4689 } /* Generic.Deleted */
|
||||
.ge { color: #F8F8F2; font-style: italic } /* Generic.Emph */
|
||||
.ges { color: #F8F8F2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
.gr { color: #F8F8F2 } /* Generic.Error */
|
||||
.gh { color: #F8F8F2 } /* Generic.Heading */
|
||||
.gi { color: #A6E22E } /* Generic.Inserted */
|
||||
.go { color: #66D9EF } /* Generic.Output */
|
||||
.gp { color: #FF4689; font-weight: bold } /* Generic.Prompt */
|
||||
.gs { color: #F8F8F2; font-weight: bold } /* Generic.Strong */
|
||||
.gu { color: #959077 } /* Generic.Subheading */
|
||||
.gt { color: #F8F8F2 } /* Generic.Traceback */
|
||||
.kc { color: #66D9EF } /* Keyword.Constant */
|
||||
.kd { color: #66D9EF } /* Keyword.Declaration */
|
||||
.kn { color: #FF4689 } /* Keyword.Namespace */
|
||||
.kp { color: #66D9EF } /* Keyword.Pseudo */
|
||||
.kr { color: #66D9EF } /* Keyword.Reserved */
|
||||
.kt { color: #66D9EF } /* Keyword.Type */
|
||||
.ld { color: #E6DB74 } /* Literal.Date */
|
||||
.m { color: #AE81FF } /* Literal.Number */
|
||||
.s { color: #E6DB74 } /* Literal.String */
|
||||
.na { color: #A6E22E } /* Name.Attribute */
|
||||
.nb { color: #F8F8F2 } /* Name.Builtin */
|
||||
.nc { color: #A6E22E } /* Name.Class */
|
||||
.no { color: #66D9EF } /* Name.Constant */
|
||||
.nd { color: #A6E22E } /* Name.Decorator */
|
||||
.ni { color: #F8F8F2 } /* Name.Entity */
|
||||
.ne { color: #A6E22E } /* Name.Exception */
|
||||
.nf { color: #A6E22E } /* Name.Function */
|
||||
.nl { color: #F8F8F2 } /* Name.Label */
|
||||
.nn { color: #F8F8F2 } /* Name.Namespace */
|
||||
.nx { color: #A6E22E } /* Name.Other */
|
||||
.py { color: #F8F8F2 } /* Name.Property */
|
||||
.nt { color: #FF4689 } /* Name.Tag */
|
||||
.nv { color: #F8F8F2 } /* Name.Variable */
|
||||
.ow { color: #FF4689 } /* Operator.Word */
|
||||
.pm { color: #F8F8F2 } /* Punctuation.Marker */
|
||||
.w { color: #F8F8F2 } /* Text.Whitespace */
|
||||
.mb { color: #AE81FF } /* Literal.Number.Bin */
|
||||
.mf { color: #AE81FF } /* Literal.Number.Float */
|
||||
.mh { color: #AE81FF } /* Literal.Number.Hex */
|
||||
.mi { color: #AE81FF } /* Literal.Number.Integer */
|
||||
.mo { color: #AE81FF } /* Literal.Number.Oct */
|
||||
.sa { color: #E6DB74 } /* Literal.String.Affix */
|
||||
.sb { color: #E6DB74 } /* Literal.String.Backtick */
|
||||
.sc { color: #E6DB74 } /* Literal.String.Char */
|
||||
.dl { color: #E6DB74 } /* Literal.String.Delimiter */
|
||||
.sd { color: #E6DB74 } /* Literal.String.Doc */
|
||||
.s2 { color: #E6DB74 } /* Literal.String.Double */
|
||||
.se { color: #AE81FF } /* Literal.String.Escape */
|
||||
.sh { color: #E6DB74 } /* Literal.String.Heredoc */
|
||||
.si { color: #E6DB74 } /* Literal.String.Interpol */
|
||||
.sx { color: #E6DB74 } /* Literal.String.Other */
|
||||
.sr { color: #E6DB74 } /* Literal.String.Regex */
|
||||
.s1 { color: #E6DB74 } /* Literal.String.Single */
|
||||
.ss { color: #E6DB74 } /* Literal.String.Symbol */
|
||||
.bp { color: #F8F8F2 } /* Name.Builtin.Pseudo */
|
||||
.fm { color: #A6E22E } /* Name.Function.Magic */
|
||||
.vc { color: #F8F8F2 } /* Name.Variable.Class */
|
||||
.vg { color: #F8F8F2 } /* Name.Variable.Global */
|
||||
.vi { color: #F8F8F2 } /* Name.Variable.Instance */
|
||||
.vm { color: #F8F8F2 } /* Name.Variable.Magic */
|
||||
.il { color: #AE81FF } /* Literal.Number.Integer.Long */
|
||||
pre { line-height: 100%; }
|
||||
td.linenos .normal { color: #3c4354; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: #3c4354; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #3c4354; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #3c4354; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
.hll { background-color: #6e7681 }
|
||||
.c { color: #7E8AA1 } /* Comment */
|
||||
.err { color: #F88F7F } /* Error */
|
||||
.esc { color: #D4D2C8 } /* Escape */
|
||||
.g { color: #D4D2C8 } /* Generic */
|
||||
.k { color: #FFAD66 } /* Keyword */
|
||||
.l { color: #D5FF80 } /* Literal */
|
||||
.n { color: #D4D2C8 } /* Name */
|
||||
.o { color: #FFAD66 } /* Operator */
|
||||
.x { color: #D4D2C8 } /* Other */
|
||||
.p { color: #D4D2C8 } /* Punctuation */
|
||||
.ch { color: #F88F7F; font-style: italic } /* Comment.Hashbang */
|
||||
.cm { color: #7E8AA1 } /* Comment.Multiline */
|
||||
.cp { color: #FFAD66; font-weight: bold } /* Comment.Preproc */
|
||||
.cpf { color: #7E8AA1 } /* Comment.PreprocFile */
|
||||
.c1 { color: #7E8AA1 } /* Comment.Single */
|
||||
.cs { color: #7E8AA1; font-style: italic } /* Comment.Special */
|
||||
.gd { color: #F88F7F; background-color: #3D1E20 } /* Generic.Deleted */
|
||||
.ge { color: #D4D2C8; font-style: italic } /* Generic.Emph */
|
||||
.ges { color: #D4D2C8 } /* Generic.EmphStrong */
|
||||
.gr { color: #F88F7F } /* Generic.Error */
|
||||
.gh { color: #D4D2C8 } /* Generic.Heading */
|
||||
.gi { color: #6AD4AF; background-color: #19362C } /* Generic.Inserted */
|
||||
.go { color: #7E8AA1 } /* Generic.Output */
|
||||
.gp { color: #D4D2C8 } /* Generic.Prompt */
|
||||
.gs { color: #D4D2C8; font-weight: bold } /* Generic.Strong */
|
||||
.gu { color: #D4D2C8 } /* Generic.Subheading */
|
||||
.gt { color: #F88F7F } /* Generic.Traceback */
|
||||
.kc { color: #FFAD66 } /* Keyword.Constant */
|
||||
.kd { color: #FFAD66 } /* Keyword.Declaration */
|
||||
.kn { color: #FFAD66 } /* Keyword.Namespace */
|
||||
.kp { color: #FFAD66 } /* Keyword.Pseudo */
|
||||
.kr { color: #FFAD66 } /* Keyword.Reserved */
|
||||
.kt { color: #73D0FF } /* Keyword.Type */
|
||||
.ld { color: #D5FF80 } /* Literal.Date */
|
||||
.m { color: #DFBFFF } /* Literal.Number */
|
||||
.s { color: #D5FF80 } /* Literal.String */
|
||||
.na { color: #FFD173 } /* Name.Attribute */
|
||||
.nb { color: #FFD173 } /* Name.Builtin */
|
||||
.nc { color: #73D0FF } /* Name.Class */
|
||||
.no { color: #FFD173 } /* Name.Constant */
|
||||
.nd { color: #7E8AA1; font-weight: bold; font-style: italic } /* Name.Decorator */
|
||||
.ni { color: #95E6CB } /* Name.Entity */
|
||||
.ne { color: #73D0FF } /* Name.Exception */
|
||||
.nf { color: #FFD173 } /* Name.Function */
|
||||
.nl { color: #D4D2C8 } /* Name.Label */
|
||||
.nn { color: #D4D2C8 } /* Name.Namespace */
|
||||
.nx { color: #D4D2C8 } /* Name.Other */
|
||||
.py { color: #FFD173 } /* Name.Property */
|
||||
.nt { color: #5CCFE6 } /* Name.Tag */
|
||||
.nv { color: #D4D2C8 } /* Name.Variable */
|
||||
.ow { color: #FFAD66 } /* Operator.Word */
|
||||
.pm { color: #D4D2C8 } /* Punctuation.Marker */
|
||||
.w { color: #D4D2C8 } /* Text.Whitespace */
|
||||
.mb { color: #DFBFFF } /* Literal.Number.Bin */
|
||||
.mf { color: #DFBFFF } /* Literal.Number.Float */
|
||||
.mh { color: #DFBFFF } /* Literal.Number.Hex */
|
||||
.mi { color: #DFBFFF } /* Literal.Number.Integer */
|
||||
.mo { color: #DFBFFF } /* Literal.Number.Oct */
|
||||
.sa { color: #F29E74 } /* Literal.String.Affix */
|
||||
.sb { color: #D5FF80 } /* Literal.String.Backtick */
|
||||
.sc { color: #D5FF80 } /* Literal.String.Char */
|
||||
.dl { color: #D5FF80 } /* Literal.String.Delimiter */
|
||||
.sd { color: #7E8AA1 } /* Literal.String.Doc */
|
||||
.s2 { color: #D5FF80 } /* Literal.String.Double */
|
||||
.se { color: #95E6CB } /* Literal.String.Escape */
|
||||
.sh { color: #D5FF80 } /* Literal.String.Heredoc */
|
||||
.si { color: #95E6CB } /* Literal.String.Interpol */
|
||||
.sx { color: #95E6CB } /* Literal.String.Other */
|
||||
.sr { color: #95E6CB } /* Literal.String.Regex */
|
||||
.s1 { color: #D5FF80 } /* Literal.String.Single */
|
||||
.ss { color: #DFBFFF } /* Literal.String.Symbol */
|
||||
.bp { color: #5CCFE6 } /* Name.Builtin.Pseudo */
|
||||
.fm { color: #FFD173 } /* Name.Function.Magic */
|
||||
.vc { color: #D4D2C8 } /* Name.Variable.Class */
|
||||
.vg { color: #D4D2C8 } /* Name.Variable.Global */
|
||||
.vi { color: #D4D2C8 } /* Name.Variable.Instance */
|
||||
.vm { color: #D4D2C8 } /* Name.Variable.Magic */
|
||||
.il { color: #DFBFFF } /* Literal.Number.Integer.Long */
|
||||
}
|
||||
}
|
||||
|
||||
3885
client/simple/package-lock.json
generated
3885
client/simple/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,49 +1,36 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@searxng/theme-simple",
|
||||
"version": "0.0.0",
|
||||
"name": "simple",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "npm run build:icons && npm run build:vite",
|
||||
"build:icons": "node theme_icons.ts",
|
||||
"build:icons": "node theme_icons.js",
|
||||
"build:vite": "vite build",
|
||||
"clean": "rm -Rf node_modules",
|
||||
"fix": "npm run fix:stylelint && npm run fix:biome && npm run fix:package",
|
||||
"fix:biome": "biome check --write",
|
||||
"fix:package": "sort-package-json --quiet",
|
||||
"fix:stylelint": "stylelint --fix strict 'src/**/*.{scss,sass,less,styl}'",
|
||||
"lint": "npm run lint:biome && npm run lint:tsc",
|
||||
"lint:biome": "biome lint",
|
||||
"lint:tsc": "tsc --noEmit"
|
||||
},
|
||||
"browserslist": [
|
||||
"baseline 2022",
|
||||
"not dead"
|
||||
],
|
||||
"dependencies": {
|
||||
"ionicons": "^8.0.13",
|
||||
"normalize.css": "8.0.1",
|
||||
"ol": "^10.9.0",
|
||||
"swiped-events": "1.2.0"
|
||||
"lint": "npm run lint:biome",
|
||||
"lint:biome": "biome lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.0",
|
||||
"@types/node": "^26.0.0",
|
||||
"browserslist": "^4.28.2",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"edge.js": "^6.5.1",
|
||||
"less": "^4.6.6",
|
||||
"mathjs": "^15.2.0",
|
||||
"sharp": "~0.35.1",
|
||||
"sort-package-json": "^4.0.0",
|
||||
"stylelint": "^17.13.0",
|
||||
"stylelint-config-standard-less": "^4.1.0",
|
||||
"@biomejs/biome": "~2.0.6",
|
||||
"edge.js": "^6.2.1",
|
||||
"ionicons": "^8.0.10",
|
||||
"leaflet": "^1.9.4",
|
||||
"less": "^4.3.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"sharp": "^0.34.2",
|
||||
"sort-package-json": "^3.4.0",
|
||||
"stylelint": "^16.21.1",
|
||||
"stylelint-config-standard-less": "^3.0.1",
|
||||
"stylelint-prettier": "^5.0.3",
|
||||
"svgo": "^4.0.1",
|
||||
"typescript": "~6.0.3",
|
||||
"vite": "^8.0.16",
|
||||
"vite-bundle-analyzer": "^1.3.8"
|
||||
"svgo": "^4.0.0",
|
||||
"swiped-events": "^1.2.0",
|
||||
"vite": "^7.0.2",
|
||||
"vite-plugin-static-copy": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
19
client/simple/src/js/head/00_init.js
Normal file
19
client/simple/src/js/head/00_init.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
((w, d) => {
|
||||
// add data- properties
|
||||
const getLastScriptElement = () => {
|
||||
const scripts = d.getElementsByTagName("script");
|
||||
return scripts[scripts.length - 1];
|
||||
};
|
||||
|
||||
const script = d.currentScript || getLastScriptElement();
|
||||
|
||||
w.searxng = {
|
||||
settings: JSON.parse(atob(script.getAttribute("client_settings")))
|
||||
};
|
||||
|
||||
// update the css
|
||||
const htmlElement = d.getElementsByTagName("html")[0];
|
||||
htmlElement.classList.remove("no-js");
|
||||
htmlElement.classList.add("js");
|
||||
})(window, document);
|
||||
@@ -1,4 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
// core
|
||||
void import.meta.glob(["./*.ts", "./util/**/.ts"], { eager: true });
|
||||
@@ -1,36 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type { Plugin } from "./Plugin.ts";
|
||||
import { type EndpointsKeys, endpoint } from "./toolkit.ts";
|
||||
|
||||
type Options =
|
||||
| {
|
||||
on: "global";
|
||||
}
|
||||
| {
|
||||
on: "endpoint";
|
||||
where: EndpointsKeys[];
|
||||
};
|
||||
|
||||
export const load = <T extends Plugin>(instance: () => Promise<T>, options: Options): void => {
|
||||
if (!check(options)) return;
|
||||
|
||||
void instance();
|
||||
};
|
||||
|
||||
const check = (options: Options): boolean => {
|
||||
// biome-ignore lint/style/useDefaultSwitchClause: options is typed
|
||||
switch (options.on) {
|
||||
case "global": {
|
||||
return true;
|
||||
}
|
||||
case "endpoint": {
|
||||
if (!options.where.includes(endpoint)) {
|
||||
// not on the expected endpoint
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
178
client/simple/src/js/main/00_toolkit.js
Normal file
178
client/simple/src/js/main/00_toolkit.js
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* @license
|
||||
* (C) Copyright Contributors to the SearXNG project.
|
||||
* (C) Copyright Contributors to the searx project (2014 - 2021).
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
window.searxng = ((w, d) => {
|
||||
// not invented here toolkit with bugs fixed elsewhere
|
||||
// purposes : be just good enough and as small as possible
|
||||
|
||||
// from https://plainjs.com/javascript/events/live-binding-event-handlers-14/
|
||||
if (w.Element) {
|
||||
((ElementPrototype) => {
|
||||
ElementPrototype.matches =
|
||||
ElementPrototype.matches ||
|
||||
ElementPrototype.matchesSelector ||
|
||||
ElementPrototype.webkitMatchesSelector ||
|
||||
ElementPrototype.msMatchesSelector ||
|
||||
function (selector) {
|
||||
const nodes = (this.parentNode || this.document).querySelectorAll(selector);
|
||||
let i = -1;
|
||||
while (nodes[++i] && nodes[i] !== this);
|
||||
return !!nodes[i];
|
||||
};
|
||||
})(Element.prototype);
|
||||
}
|
||||
|
||||
function callbackSafe(callback, el, e) {
|
||||
try {
|
||||
callback.call(el, e);
|
||||
} catch (exception) {
|
||||
console.log(exception);
|
||||
}
|
||||
}
|
||||
|
||||
const searxng = window.searxng || {};
|
||||
|
||||
searxng.on = (obj, eventType, callback, useCapture) => {
|
||||
useCapture = useCapture || false;
|
||||
if (typeof obj !== "string") {
|
||||
// obj HTMLElement, HTMLDocument
|
||||
obj.addEventListener(eventType, callback, useCapture);
|
||||
} else {
|
||||
// obj is a selector
|
||||
d.addEventListener(
|
||||
eventType,
|
||||
(e) => {
|
||||
let el = e.target || e.srcElement;
|
||||
let found = false;
|
||||
|
||||
while (el?.matches && el !== d) {
|
||||
found = el.matches(obj);
|
||||
|
||||
if (found) break;
|
||||
|
||||
el = el.parentElement;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
callbackSafe(callback, el, e);
|
||||
}
|
||||
},
|
||||
useCapture
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
searxng.ready = (callback) => {
|
||||
if (document.readyState !== "loading") {
|
||||
callback.call(w);
|
||||
} else {
|
||||
w.addEventListener("DOMContentLoaded", callback.bind(w));
|
||||
}
|
||||
};
|
||||
|
||||
searxng.http = (method, url, data = null) =>
|
||||
new Promise((resolve, reject) => {
|
||||
try {
|
||||
const req = new XMLHttpRequest();
|
||||
req.open(method, url, true);
|
||||
req.timeout = 20000;
|
||||
|
||||
// On load
|
||||
req.onload = () => {
|
||||
if (req.status === 200) {
|
||||
resolve(req.response, req.responseType);
|
||||
} else {
|
||||
reject(Error(req.statusText));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle network errors
|
||||
req.onerror = () => {
|
||||
reject(Error("Network Error"));
|
||||
};
|
||||
|
||||
req.onabort = () => {
|
||||
reject(Error("Transaction is aborted"));
|
||||
};
|
||||
|
||||
req.ontimeout = () => {
|
||||
reject(Error("Timeout"));
|
||||
};
|
||||
|
||||
// Make the request
|
||||
if (data) {
|
||||
req.send(data);
|
||||
} else {
|
||||
req.send();
|
||||
}
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
});
|
||||
|
||||
searxng.loadStyle = (src) => {
|
||||
const path = `${searxng.settings.theme_static_path}/${src}`;
|
||||
const id = `style_${src.replace(".", "_")}`;
|
||||
let s = d.getElementById(id);
|
||||
if (s === null) {
|
||||
s = d.createElement("link");
|
||||
s.setAttribute("id", id);
|
||||
s.setAttribute("rel", "stylesheet");
|
||||
s.setAttribute("type", "text/css");
|
||||
s.setAttribute("href", path);
|
||||
d.body.appendChild(s);
|
||||
}
|
||||
};
|
||||
|
||||
searxng.loadScript = (src, callback) => {
|
||||
const path = `${searxng.settings.theme_static_path}/${src}`;
|
||||
const id = `script_${src.replace(".", "_")}`;
|
||||
let s = d.getElementById(id);
|
||||
if (s === null) {
|
||||
s = d.createElement("script");
|
||||
s.setAttribute("id", id);
|
||||
s.setAttribute("src", path);
|
||||
s.onload = callback;
|
||||
s.onerror = () => {
|
||||
s.setAttribute("error", "1");
|
||||
};
|
||||
d.body.appendChild(s);
|
||||
} else if (!s.hasAttribute("error")) {
|
||||
try {
|
||||
callback.apply(s, []);
|
||||
} catch (exception) {
|
||||
console.log(exception);
|
||||
}
|
||||
} else {
|
||||
console.log(`callback not executed : script '${path}' not loaded.`);
|
||||
}
|
||||
};
|
||||
|
||||
searxng.insertBefore = (newNode, referenceNode) => {
|
||||
referenceNode.parentNode.insertBefore(newNode, referenceNode);
|
||||
};
|
||||
|
||||
searxng.insertAfter = (newNode, referenceNode) => {
|
||||
referenceNode.parentNode.insertAfter(newNode, referenceNode.nextSibling);
|
||||
};
|
||||
|
||||
searxng.on(".close", "click", function () {
|
||||
this.parentNode.classList.add("invisible");
|
||||
});
|
||||
|
||||
function getEndpoint() {
|
||||
for (const className of d.getElementsByTagName("body")[0].classList.values()) {
|
||||
if (className.endsWith("_endpoint")) {
|
||||
return className.split("_")[0];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
searxng.endpoint = getEndpoint();
|
||||
|
||||
return searxng;
|
||||
})(window, document);
|
||||
@@ -1,142 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { http, listen, settings } from "../toolkit.ts";
|
||||
import { assertElement } from "../util/assertElement.ts";
|
||||
|
||||
const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
|
||||
try {
|
||||
let res: Response;
|
||||
|
||||
if (settings.method === "GET") {
|
||||
res = await http("GET", `./autocompleter?q=${query}`);
|
||||
} else {
|
||||
res = await http("POST", "./autocompleter", { body: new URLSearchParams({ q: query }) });
|
||||
}
|
||||
|
||||
const results = await res.json();
|
||||
|
||||
const autocomplete = document.querySelector<HTMLElement>(".autocomplete");
|
||||
assertElement(autocomplete);
|
||||
|
||||
const autocompleteList = document.querySelector<HTMLUListElement>(".autocomplete ul");
|
||||
assertElement(autocompleteList);
|
||||
|
||||
autocomplete.classList.add("open");
|
||||
autocompleteList.replaceChildren();
|
||||
|
||||
// show an error message that no result was found
|
||||
if (results?.[1]?.length === 0) {
|
||||
const noItemFoundMessage = Object.assign(document.createElement("li"), {
|
||||
className: "no-item-found",
|
||||
textContent: settings.translations?.no_item_found ?? "No results found"
|
||||
});
|
||||
autocompleteList.append(noItemFoundMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const fragment = new DocumentFragment();
|
||||
|
||||
for (const result of results[1]) {
|
||||
const li = Object.assign(document.createElement("li"), { textContent: result });
|
||||
|
||||
listen("mousedown", li, () => {
|
||||
qInput.value = result;
|
||||
|
||||
const form = document.querySelector<HTMLFormElement>("#search");
|
||||
form?.submit();
|
||||
});
|
||||
|
||||
fragment.append(li);
|
||||
}
|
||||
|
||||
autocompleteList.append(fragment);
|
||||
} catch (error) {
|
||||
console.error("Error fetching autocomplete results:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const qInput = document.getElementById("q") as HTMLInputElement | null;
|
||||
assertElement(qInput);
|
||||
|
||||
let timeoutId: number;
|
||||
|
||||
listen("input", qInput, () => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const query = qInput.value;
|
||||
const minLength = settings.autocomplete_min ?? 2;
|
||||
|
||||
if (query.length < minLength) return;
|
||||
|
||||
timeoutId = window.setTimeout(async () => {
|
||||
if (query === qInput.value) {
|
||||
await fetchResults(qInput, query);
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
const autocomplete: HTMLElement | null = document.querySelector<HTMLElement>(".autocomplete");
|
||||
const autocompleteList: HTMLUListElement | null = document.querySelector<HTMLUListElement>(".autocomplete ul");
|
||||
if (autocompleteList) {
|
||||
listen("keydown", qInput, (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
autocomplete?.classList.remove("open");
|
||||
}
|
||||
});
|
||||
listen("keyup", qInput, (event: KeyboardEvent) => {
|
||||
const listItems = [...autocompleteList.children] as HTMLElement[];
|
||||
|
||||
const currentIndex = listItems.findIndex((item) => item.classList.contains("active"));
|
||||
let newCurrentIndex = -1;
|
||||
|
||||
switch (event.key) {
|
||||
case "ArrowUp": {
|
||||
const currentItem = listItems[currentIndex];
|
||||
if (currentItem && currentIndex >= 0) {
|
||||
currentItem.classList.remove("active");
|
||||
}
|
||||
// we need to add listItems.length to the index calculation here because the JavaScript modulos
|
||||
// operator doesn't work with negative numbers
|
||||
newCurrentIndex = (currentIndex - 1 + listItems.length) % listItems.length;
|
||||
break;
|
||||
}
|
||||
case "ArrowDown": {
|
||||
const currentItem = listItems[currentIndex];
|
||||
if (currentItem && currentIndex >= 0) {
|
||||
currentItem.classList.remove("active");
|
||||
}
|
||||
newCurrentIndex = (currentIndex + 1) % listItems.length;
|
||||
break;
|
||||
}
|
||||
case "Enter":
|
||||
if (autocomplete) {
|
||||
autocomplete.classList.remove("open");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (newCurrentIndex !== -1) {
|
||||
const selectedItem = listItems[newCurrentIndex];
|
||||
if (selectedItem) {
|
||||
selectedItem.classList.add("active");
|
||||
|
||||
if (!selectedItem.classList.contains("no-item-found")) {
|
||||
const qInput = document.getElementById("q") as HTMLInputElement | null;
|
||||
if (qInput) {
|
||||
qInput.value = selectedItem.textContent ?? "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
listen("blur", qInput, () => {
|
||||
autocomplete?.classList.remove("open");
|
||||
});
|
||||
|
||||
listen("focus", qInput, () => {
|
||||
autocomplete?.classList.add("open");
|
||||
});
|
||||
}
|
||||
84
client/simple/src/js/main/infinite_scroll.js
Normal file
84
client/simple/src/js/main/infinite_scroll.js
Normal file
@@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/* global searxng */
|
||||
|
||||
searxng.ready(() => {
|
||||
searxng.infinite_scroll_supported =
|
||||
"IntersectionObserver" in window &&
|
||||
"IntersectionObserverEntry" in window &&
|
||||
"intersectionRatio" in window.IntersectionObserverEntry.prototype;
|
||||
|
||||
if (searxng.endpoint !== "results") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!searxng.infinite_scroll_supported) {
|
||||
console.log("IntersectionObserver not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
const d = document;
|
||||
const onlyImages = d.getElementById("results").classList.contains("only_template_images");
|
||||
|
||||
function newLoadSpinner() {
|
||||
const loader = d.createElement("div");
|
||||
loader.classList.add("loader");
|
||||
return loader;
|
||||
}
|
||||
|
||||
function replaceChildrenWith(element, children) {
|
||||
element.textContent = "";
|
||||
children.forEach((child) => element.appendChild(child));
|
||||
}
|
||||
|
||||
function loadNextPage(callback) {
|
||||
const form = d.querySelector("#pagination form.next_page");
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
replaceChildrenWith(d.querySelector("#pagination"), [newLoadSpinner()]);
|
||||
const formData = new FormData(form);
|
||||
searxng
|
||||
.http("POST", d.querySelector("#search").getAttribute("action"), formData)
|
||||
.then((response) => {
|
||||
const nextPageDoc = new DOMParser().parseFromString(response, "text/html");
|
||||
const articleList = nextPageDoc.querySelectorAll("#urls article");
|
||||
const paginationElement = nextPageDoc.querySelector("#pagination");
|
||||
d.querySelector("#pagination").remove();
|
||||
if (articleList.length > 0 && !onlyImages) {
|
||||
// do not add <hr> element when there are only images
|
||||
d.querySelector("#urls").appendChild(d.createElement("hr"));
|
||||
}
|
||||
articleList.forEach((articleElement) => {
|
||||
d.querySelector("#urls").appendChild(articleElement);
|
||||
});
|
||||
if (paginationElement) {
|
||||
d.querySelector("#results").appendChild(paginationElement);
|
||||
callback();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
const e = d.createElement("div");
|
||||
e.textContent = searxng.settings.translations.error_loading_next_page;
|
||||
e.classList.add("dialog-error");
|
||||
e.setAttribute("role", "alert");
|
||||
replaceChildrenWith(d.querySelector("#pagination"), [e]);
|
||||
});
|
||||
}
|
||||
|
||||
if (searxng.settings.infinite_scroll && searxng.infinite_scroll_supported) {
|
||||
const intersectionObserveOptions = {
|
||||
rootMargin: "20rem"
|
||||
};
|
||||
const observedSelector = "article.result:last-child";
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
const paginationEntry = entries[0];
|
||||
if (paginationEntry.isIntersecting) {
|
||||
observer.unobserve(paginationEntry.target);
|
||||
loadNextPage(() => observer.observe(d.querySelector(observedSelector), intersectionObserveOptions));
|
||||
}
|
||||
});
|
||||
observer.observe(d.querySelector(observedSelector), intersectionObserveOptions);
|
||||
}
|
||||
});
|
||||
473
client/simple/src/js/main/keyboard.js
Normal file
473
client/simple/src/js/main/keyboard.js
Normal file
@@ -0,0 +1,473 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
/* global searxng */
|
||||
|
||||
searxng.ready(() => {
|
||||
function isElementInDetail(el) {
|
||||
while (el !== undefined) {
|
||||
if (el.classList.contains("detail")) {
|
||||
return true;
|
||||
}
|
||||
if (el.classList.contains("result")) {
|
||||
// we found a result, no need to go to the root of the document:
|
||||
// el is not inside a <div class="detail"> element
|
||||
return false;
|
||||
}
|
||||
el = el.parentNode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getResultElement(el) {
|
||||
while (el !== undefined) {
|
||||
if (el.classList.contains("result")) {
|
||||
return el;
|
||||
}
|
||||
el = el.parentNode;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isImageResult(resultElement) {
|
||||
return resultElement?.classList.contains("result-images");
|
||||
}
|
||||
|
||||
searxng.on(".result", "click", function (e) {
|
||||
if (!isElementInDetail(e.target)) {
|
||||
highlightResult(this)(true, true);
|
||||
const resultElement = getResultElement(e.target);
|
||||
if (isImageResult(resultElement)) {
|
||||
e.preventDefault();
|
||||
searxng.selectImage(resultElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
searxng.on(
|
||||
".result a",
|
||||
"focus",
|
||||
(e) => {
|
||||
if (!isElementInDetail(e.target)) {
|
||||
const resultElement = getResultElement(e.target);
|
||||
if (resultElement && resultElement.getAttribute("data-vim-selected") === null) {
|
||||
highlightResult(resultElement)(true);
|
||||
}
|
||||
if (isImageResult(resultElement)) {
|
||||
searxng.selectImage(resultElement);
|
||||
}
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
/* common base for layouts */
|
||||
const baseKeyBinding = {
|
||||
Escape: {
|
||||
key: "ESC",
|
||||
fun: removeFocus,
|
||||
des: "remove focus from the focused input",
|
||||
cat: "Control"
|
||||
},
|
||||
c: {
|
||||
key: "c",
|
||||
fun: copyURLToClipboard,
|
||||
des: "copy url of the selected result to the clipboard",
|
||||
cat: "Results"
|
||||
},
|
||||
h: {
|
||||
key: "h",
|
||||
fun: toggleHelp,
|
||||
des: "toggle help window",
|
||||
cat: "Other"
|
||||
},
|
||||
i: {
|
||||
key: "i",
|
||||
fun: searchInputFocus,
|
||||
des: "focus on the search input",
|
||||
cat: "Control"
|
||||
},
|
||||
n: {
|
||||
key: "n",
|
||||
fun: GoToNextPage(),
|
||||
des: "go to next page",
|
||||
cat: "Results"
|
||||
},
|
||||
o: {
|
||||
key: "o",
|
||||
fun: openResult(false),
|
||||
des: "open search result",
|
||||
cat: "Results"
|
||||
},
|
||||
p: {
|
||||
key: "p",
|
||||
fun: GoToPreviousPage(),
|
||||
des: "go to previous page",
|
||||
cat: "Results"
|
||||
},
|
||||
r: {
|
||||
key: "r",
|
||||
fun: reloadPage,
|
||||
des: "reload page from the server",
|
||||
cat: "Control"
|
||||
},
|
||||
t: {
|
||||
key: "t",
|
||||
fun: openResult(true),
|
||||
des: "open the result in a new tab",
|
||||
cat: "Results"
|
||||
}
|
||||
};
|
||||
const keyBindingLayouts = {
|
||||
default: Object.assign(
|
||||
{
|
||||
/* SearXNG layout */
|
||||
ArrowLeft: {
|
||||
key: "←",
|
||||
fun: highlightResult("up"),
|
||||
des: "select previous search result",
|
||||
cat: "Results"
|
||||
},
|
||||
ArrowRight: {
|
||||
key: "→",
|
||||
fun: highlightResult("down"),
|
||||
des: "select next search result",
|
||||
cat: "Results"
|
||||
}
|
||||
},
|
||||
baseKeyBinding
|
||||
),
|
||||
|
||||
vim: Object.assign(
|
||||
{
|
||||
/* Vim-like Key Layout. */
|
||||
b: {
|
||||
key: "b",
|
||||
fun: scrollPage(-window.innerHeight),
|
||||
des: "scroll one page up",
|
||||
cat: "Navigation"
|
||||
},
|
||||
f: {
|
||||
key: "f",
|
||||
fun: scrollPage(window.innerHeight),
|
||||
des: "scroll one page down",
|
||||
cat: "Navigation"
|
||||
},
|
||||
u: {
|
||||
key: "u",
|
||||
fun: scrollPage(-window.innerHeight / 2),
|
||||
des: "scroll half a page up",
|
||||
cat: "Navigation"
|
||||
},
|
||||
d: {
|
||||
key: "d",
|
||||
fun: scrollPage(window.innerHeight / 2),
|
||||
des: "scroll half a page down",
|
||||
cat: "Navigation"
|
||||
},
|
||||
g: {
|
||||
key: "g",
|
||||
fun: scrollPageTo(-document.body.scrollHeight, "top"),
|
||||
des: "scroll to the top of the page",
|
||||
cat: "Navigation"
|
||||
},
|
||||
v: {
|
||||
key: "v",
|
||||
fun: scrollPageTo(document.body.scrollHeight, "bottom"),
|
||||
des: "scroll to the bottom of the page",
|
||||
cat: "Navigation"
|
||||
},
|
||||
k: {
|
||||
key: "k",
|
||||
fun: highlightResult("up"),
|
||||
des: "select previous search result",
|
||||
cat: "Results"
|
||||
},
|
||||
j: {
|
||||
key: "j",
|
||||
fun: highlightResult("down"),
|
||||
des: "select next search result",
|
||||
cat: "Results"
|
||||
},
|
||||
y: {
|
||||
key: "y",
|
||||
fun: copyURLToClipboard,
|
||||
des: "copy url of the selected result to the clipboard",
|
||||
cat: "Results"
|
||||
}
|
||||
},
|
||||
baseKeyBinding
|
||||
)
|
||||
};
|
||||
|
||||
const keyBindings = keyBindingLayouts[searxng.settings.hotkeys] || keyBindingLayouts.default;
|
||||
|
||||
searxng.on(document, "keydown", (e) => {
|
||||
// check for modifiers so we don't break browser's hotkeys
|
||||
if (
|
||||
// biome-ignore lint/suspicious/noPrototypeBuiltins: FIXME: support for Chromium 93-87, Firefox 92-78, Safari 15.4-14
|
||||
Object.prototype.hasOwnProperty.call(keyBindings, e.key) &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.shiftKey &&
|
||||
!e.metaKey
|
||||
) {
|
||||
const tagName = e.target.tagName.toLowerCase();
|
||||
if (e.key === "Escape") {
|
||||
keyBindings[e.key].fun(e);
|
||||
} else {
|
||||
if (e.target === document.body || tagName === "a" || tagName === "button") {
|
||||
e.preventDefault();
|
||||
keyBindings[e.key].fun();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function highlightResult(which) {
|
||||
return (noScroll, keepFocus) => {
|
||||
let current = document.querySelector(".result[data-vim-selected]"),
|
||||
effectiveWhich = which;
|
||||
if (current === null) {
|
||||
// no selection : choose the first one
|
||||
current = document.querySelector(".result");
|
||||
if (current === null) {
|
||||
// no first one : there are no results
|
||||
return;
|
||||
}
|
||||
// replace up/down actions by selecting first one
|
||||
if (which === "down" || which === "up") {
|
||||
effectiveWhich = current;
|
||||
}
|
||||
}
|
||||
|
||||
let next,
|
||||
results = document.querySelectorAll(".result");
|
||||
results = Array.from(results); // convert NodeList to Array for further use
|
||||
|
||||
if (typeof effectiveWhich !== "string") {
|
||||
next = effectiveWhich;
|
||||
} else {
|
||||
switch (effectiveWhich) {
|
||||
case "visible": {
|
||||
const top = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const bot = top + document.documentElement.clientHeight;
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
next = results[i];
|
||||
const etop = next.offsetTop;
|
||||
const ebot = etop + next.clientHeight;
|
||||
|
||||
if (ebot <= bot && etop > top) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "down":
|
||||
next = results[results.indexOf(current) + 1] || current;
|
||||
break;
|
||||
case "up":
|
||||
next = results[results.indexOf(current) - 1] || current;
|
||||
break;
|
||||
case "bottom":
|
||||
next = results[results.length - 1];
|
||||
break;
|
||||
// biome-ignore lint/complexity/noUselessSwitchCase: fallthrough is intended
|
||||
case "top":
|
||||
/* falls through */
|
||||
default:
|
||||
next = results[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (next) {
|
||||
current.removeAttribute("data-vim-selected");
|
||||
next.setAttribute("data-vim-selected", "true");
|
||||
if (!keepFocus) {
|
||||
const link = next.querySelector("h3 a") || next.querySelector("a");
|
||||
if (link !== null) {
|
||||
link.focus();
|
||||
}
|
||||
}
|
||||
if (!noScroll) {
|
||||
scrollPageToSelected();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function reloadPage() {
|
||||
document.location.reload(true);
|
||||
}
|
||||
|
||||
function removeFocus(e) {
|
||||
const tagName = e.target.tagName.toLowerCase();
|
||||
if (document.activeElement && (tagName === "input" || tagName === "select" || tagName === "textarea")) {
|
||||
document.activeElement.blur();
|
||||
} else {
|
||||
searxng.closeDetail();
|
||||
}
|
||||
}
|
||||
|
||||
function pageButtonClick(css_selector) {
|
||||
return () => {
|
||||
const button = document.querySelector(css_selector);
|
||||
if (button) {
|
||||
button.click();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function GoToNextPage() {
|
||||
return pageButtonClick('nav#pagination .next_page button[type="submit"]');
|
||||
}
|
||||
|
||||
function GoToPreviousPage() {
|
||||
return pageButtonClick('nav#pagination .previous_page button[type="submit"]');
|
||||
}
|
||||
|
||||
function scrollPageToSelected() {
|
||||
const sel = document.querySelector(".result[data-vim-selected]");
|
||||
if (sel === null) {
|
||||
return;
|
||||
}
|
||||
const wtop = document.documentElement.scrollTop || document.body.scrollTop,
|
||||
wheight = document.documentElement.clientHeight,
|
||||
etop = sel.offsetTop,
|
||||
ebot = etop + sel.clientHeight,
|
||||
offset = 120;
|
||||
// first element ?
|
||||
if (sel.previousElementSibling === null && ebot < wheight) {
|
||||
// set to the top of page if the first element
|
||||
// is fully included in the viewport
|
||||
window.scroll(window.scrollX, 0);
|
||||
return;
|
||||
}
|
||||
if (wtop > etop - offset) {
|
||||
window.scroll(window.scrollX, etop - offset);
|
||||
} else {
|
||||
const wbot = wtop + wheight;
|
||||
if (wbot < ebot + offset) {
|
||||
window.scroll(window.scrollX, ebot - wheight + offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scrollPage(amount) {
|
||||
return () => {
|
||||
window.scrollBy(0, amount);
|
||||
highlightResult("visible")();
|
||||
};
|
||||
}
|
||||
|
||||
function scrollPageTo(position, nav) {
|
||||
return () => {
|
||||
window.scrollTo(0, position);
|
||||
highlightResult(nav)();
|
||||
};
|
||||
}
|
||||
|
||||
function searchInputFocus() {
|
||||
window.scrollTo(0, 0);
|
||||
const q = document.querySelector("#q");
|
||||
q.focus();
|
||||
if (q.setSelectionRange) {
|
||||
const len = q.value.length;
|
||||
q.setSelectionRange(len, len);
|
||||
}
|
||||
}
|
||||
|
||||
function openResult(newTab) {
|
||||
return () => {
|
||||
let link = document.querySelector(".result[data-vim-selected] h3 a");
|
||||
if (link === null) {
|
||||
link = document.querySelector(".result[data-vim-selected] > a");
|
||||
}
|
||||
if (link !== null) {
|
||||
const url = link.getAttribute("href");
|
||||
if (newTab) {
|
||||
window.open(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function initHelpContent(divElement) {
|
||||
const categories = {};
|
||||
|
||||
for (const k in keyBindings) {
|
||||
const key = keyBindings[k];
|
||||
categories[key.cat] = categories[key.cat] || [];
|
||||
categories[key.cat].push(key);
|
||||
}
|
||||
|
||||
const sorted = Object.keys(categories).sort((a, b) => categories[b].length - categories[a].length);
|
||||
|
||||
if (sorted.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<a href="#" class="close" aria-label="close" title="close">×</a>';
|
||||
html += "<h3>How to navigate SearXNG with hotkeys</h3>";
|
||||
html += "<table>";
|
||||
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const cat = categories[sorted[i]];
|
||||
|
||||
const lastCategory = i === sorted.length - 1;
|
||||
const first = i % 2 === 0;
|
||||
|
||||
if (first) {
|
||||
html += "<tr>";
|
||||
}
|
||||
html += "<td>";
|
||||
|
||||
html += `<h4>${cat[0].cat}</h4>`;
|
||||
html += '<ul class="list-unstyled">';
|
||||
|
||||
for (const cj in cat) {
|
||||
html += `<li><kbd>${cat[cj].key}</kbd> ${cat[cj].des}</li>`;
|
||||
}
|
||||
|
||||
html += "</ul>";
|
||||
html += "</td>"; // col-sm-*
|
||||
|
||||
if (!first || lastCategory) {
|
||||
html += "</tr>"; // row
|
||||
}
|
||||
}
|
||||
|
||||
html += "</table>";
|
||||
|
||||
divElement.innerHTML = html;
|
||||
}
|
||||
|
||||
function toggleHelp() {
|
||||
let helpPanel = document.querySelector("#vim-hotkeys-help");
|
||||
if (helpPanel === undefined || helpPanel === null) {
|
||||
// first call
|
||||
helpPanel = document.createElement("div");
|
||||
helpPanel.id = "vim-hotkeys-help";
|
||||
helpPanel.className = "dialog-modal";
|
||||
initHelpContent(helpPanel);
|
||||
const body = document.getElementsByTagName("body")[0];
|
||||
body.appendChild(helpPanel);
|
||||
} else {
|
||||
// toggle hidden
|
||||
helpPanel.classList.toggle("invisible");
|
||||
}
|
||||
}
|
||||
|
||||
function copyURLToClipboard() {
|
||||
const currentUrlElement = document.querySelector(".result[data-vim-selected] h3 a");
|
||||
if (currentUrlElement === null) return;
|
||||
|
||||
const url = currentUrlElement.getAttribute("href");
|
||||
navigator.clipboard.writeText(url);
|
||||
}
|
||||
|
||||
searxng.scrollPageToSelected = scrollPageToSelected;
|
||||
searxng.selectNext = highlightResult("down");
|
||||
searxng.selectPrevious = highlightResult("up");
|
||||
});
|
||||
@@ -1,488 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { listen, mutable, settings } from "../toolkit.ts";
|
||||
import { assertElement } from "../util/assertElement.ts";
|
||||
|
||||
export type KeyBindingLayout = "default" | "vim";
|
||||
|
||||
type KeyBinding = {
|
||||
key: string;
|
||||
fun: (event: KeyboardEvent) => void;
|
||||
des: string;
|
||||
cat: string;
|
||||
};
|
||||
|
||||
type HighlightResultElement = "down" | "up" | "visible" | "bottom" | "top";
|
||||
|
||||
/* common base for layouts */
|
||||
const baseKeyBinding: Record<string, KeyBinding> = {
|
||||
Escape: {
|
||||
key: "ESC",
|
||||
fun: (event: KeyboardEvent) => removeFocus(event),
|
||||
des: "remove focus from the focused input",
|
||||
cat: "Control"
|
||||
},
|
||||
c: {
|
||||
key: "c",
|
||||
fun: () => copyURLToClipboard(),
|
||||
des: "copy url of the selected result to the clipboard",
|
||||
cat: "Results"
|
||||
},
|
||||
h: {
|
||||
key: "h",
|
||||
fun: () => toggleHelp(keyBindings),
|
||||
des: "toggle help window",
|
||||
cat: "Other"
|
||||
},
|
||||
i: {
|
||||
key: "i",
|
||||
fun: () => searchInputFocus(),
|
||||
des: "focus on the search input",
|
||||
cat: "Control"
|
||||
},
|
||||
n: {
|
||||
key: "n",
|
||||
fun: () => GoToNextPage(),
|
||||
des: "go to next page",
|
||||
cat: "Results"
|
||||
},
|
||||
o: {
|
||||
key: "o",
|
||||
fun: () => openResult(false),
|
||||
des: "open search result",
|
||||
cat: "Results"
|
||||
},
|
||||
p: {
|
||||
key: "p",
|
||||
fun: () => GoToPreviousPage(),
|
||||
des: "go to previous page",
|
||||
cat: "Results"
|
||||
},
|
||||
r: {
|
||||
key: "r",
|
||||
fun: () => reloadPage(),
|
||||
des: "reload page from the server",
|
||||
cat: "Control"
|
||||
},
|
||||
t: {
|
||||
key: "t",
|
||||
fun: () => openResult(true),
|
||||
des: "open the result in a new tab",
|
||||
cat: "Results"
|
||||
}
|
||||
};
|
||||
|
||||
const keyBindingLayouts: Record<KeyBindingLayout, Record<string, KeyBinding>> = {
|
||||
// SearXNG layout
|
||||
default: {
|
||||
ArrowLeft: {
|
||||
key: "←",
|
||||
fun: () => highlightResult("up")(),
|
||||
des: "select previous search result",
|
||||
cat: "Results"
|
||||
},
|
||||
ArrowRight: {
|
||||
key: "→",
|
||||
fun: () => highlightResult("down")(),
|
||||
des: "select next search result",
|
||||
cat: "Results"
|
||||
},
|
||||
...baseKeyBinding
|
||||
},
|
||||
|
||||
// Vim-like keyboard layout
|
||||
vim: {
|
||||
b: {
|
||||
key: "b",
|
||||
fun: () => scrollPage(-window.innerHeight),
|
||||
des: "scroll one page up",
|
||||
cat: "Navigation"
|
||||
},
|
||||
d: {
|
||||
key: "d",
|
||||
fun: () => scrollPage(window.innerHeight / 2),
|
||||
des: "scroll half a page down",
|
||||
cat: "Navigation"
|
||||
},
|
||||
f: {
|
||||
key: "f",
|
||||
fun: () => scrollPage(window.innerHeight),
|
||||
des: "scroll one page down",
|
||||
cat: "Navigation"
|
||||
},
|
||||
g: {
|
||||
key: "g",
|
||||
fun: () => scrollPageTo(-document.body.scrollHeight, "top"),
|
||||
des: "scroll to the top of the page",
|
||||
cat: "Navigation"
|
||||
},
|
||||
j: {
|
||||
key: "j",
|
||||
fun: () => highlightResult("down")(),
|
||||
des: "select next search result",
|
||||
cat: "Results"
|
||||
},
|
||||
k: {
|
||||
key: "k",
|
||||
fun: () => highlightResult("up")(),
|
||||
des: "select previous search result",
|
||||
cat: "Results"
|
||||
},
|
||||
u: {
|
||||
key: "u",
|
||||
fun: () => scrollPage(-window.innerHeight / 2),
|
||||
des: "scroll half a page up",
|
||||
cat: "Navigation"
|
||||
},
|
||||
v: {
|
||||
key: "v",
|
||||
fun: () => scrollPageTo(document.body.scrollHeight, "bottom"),
|
||||
des: "scroll to the bottom of the page",
|
||||
cat: "Navigation"
|
||||
},
|
||||
y: {
|
||||
key: "y",
|
||||
fun: () => copyURLToClipboard(),
|
||||
des: "copy url of the selected result to the clipboard",
|
||||
cat: "Results"
|
||||
},
|
||||
...baseKeyBinding
|
||||
}
|
||||
};
|
||||
|
||||
const keyBindings: Record<string, KeyBinding> =
|
||||
settings.hotkeys && settings.hotkeys in keyBindingLayouts
|
||||
? keyBindingLayouts[settings.hotkeys]
|
||||
: keyBindingLayouts.default;
|
||||
|
||||
const isElementInDetail = (element?: HTMLElement): boolean => {
|
||||
const ancestor = element?.closest(".detail, .result");
|
||||
return ancestor?.classList.contains("detail") ?? false;
|
||||
};
|
||||
|
||||
const getResultElement = (element?: HTMLElement): HTMLElement | undefined => {
|
||||
return element?.closest(".result") ?? undefined;
|
||||
};
|
||||
|
||||
const isImageResult = (resultElement?: HTMLElement): boolean => {
|
||||
return resultElement?.classList.contains("result-images") ?? false;
|
||||
};
|
||||
|
||||
const highlightResult =
|
||||
(which: HighlightResultElement | HTMLElement) =>
|
||||
(noScroll?: boolean, keepFocus?: boolean): void => {
|
||||
let effectiveWhich = which;
|
||||
let current = document.querySelector<HTMLElement>(".result[data-vim-selected]");
|
||||
if (!current) {
|
||||
// no selection : choose the first one
|
||||
current = document.querySelector<HTMLElement>(".result");
|
||||
if (!current) {
|
||||
// no first one : there are no results
|
||||
return;
|
||||
}
|
||||
// replace up/down actions by selecting first one
|
||||
if (which === "down" || which === "up") {
|
||||
effectiveWhich = current;
|
||||
}
|
||||
}
|
||||
|
||||
const results = Array.from(document.querySelectorAll<HTMLElement>(".result"));
|
||||
|
||||
let next: HTMLElement | undefined;
|
||||
|
||||
if (typeof effectiveWhich === "string") {
|
||||
switch (effectiveWhich) {
|
||||
case "visible": {
|
||||
const top = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const bot = top + document.documentElement.clientHeight;
|
||||
|
||||
for (const element of results) {
|
||||
const etop = element.offsetTop;
|
||||
const ebot = etop + element.clientHeight;
|
||||
if (ebot <= bot && etop > top) {
|
||||
next = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "down":
|
||||
next = results[results.indexOf(current) + 1] || current;
|
||||
break;
|
||||
case "up":
|
||||
next = results[results.indexOf(current) - 1] || current;
|
||||
break;
|
||||
case "bottom":
|
||||
next = results.at(-1);
|
||||
break;
|
||||
// biome-ignore lint/complexity/noUselessSwitchCase: fallthrough is intended
|
||||
case "top":
|
||||
default:
|
||||
[next] = results;
|
||||
}
|
||||
} else {
|
||||
next = effectiveWhich;
|
||||
}
|
||||
|
||||
if (next) {
|
||||
current.removeAttribute("data-vim-selected");
|
||||
next.setAttribute("data-vim-selected", "true");
|
||||
|
||||
if (!keepFocus) {
|
||||
const link = next.querySelector<HTMLAnchorElement>("h3 a") || next.querySelector<HTMLAnchorElement>("a");
|
||||
link?.focus();
|
||||
}
|
||||
|
||||
if (!noScroll) {
|
||||
mutable.scrollPageToSelected?.();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const reloadPage = (): void => {
|
||||
document.location.reload();
|
||||
};
|
||||
|
||||
const removeFocus = (event: KeyboardEvent): void => {
|
||||
const target = event.target as HTMLElement;
|
||||
const tagName = target?.tagName?.toLowerCase();
|
||||
|
||||
if (document.activeElement && (tagName === "input" || tagName === "select" || tagName === "textarea")) {
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
} else {
|
||||
mutable.closeDetail?.();
|
||||
}
|
||||
};
|
||||
|
||||
const pageButtonClick = (css_selector: string): void => {
|
||||
const button = document.querySelector<HTMLButtonElement>(css_selector);
|
||||
if (button) {
|
||||
button.click();
|
||||
}
|
||||
};
|
||||
|
||||
const GoToNextPage = (): void => {
|
||||
pageButtonClick('nav#pagination .next_page button[type="submit"]');
|
||||
};
|
||||
|
||||
const GoToPreviousPage = (): void => {
|
||||
pageButtonClick('nav#pagination .previous_page button[type="submit"]');
|
||||
};
|
||||
|
||||
mutable.scrollPageToSelected = (): void => {
|
||||
const sel = document.querySelector<HTMLElement>(".result[data-vim-selected]");
|
||||
if (!sel) return;
|
||||
|
||||
const wtop = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const height = document.documentElement.clientHeight;
|
||||
const etop = sel.offsetTop;
|
||||
const ebot = etop + sel.clientHeight;
|
||||
const offset = 120;
|
||||
|
||||
// first element ?
|
||||
if (!sel.previousElementSibling && ebot < height) {
|
||||
// set to the top of page if the first element
|
||||
// is fully included in the viewport
|
||||
window.scroll(window.scrollX, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wtop > etop - offset) {
|
||||
window.scroll(window.scrollX, etop - offset);
|
||||
} else {
|
||||
const wbot = wtop + height;
|
||||
if (wbot < ebot + offset) {
|
||||
window.scroll(window.scrollX, ebot - height + offset);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const scrollPage = (amount: number): void => {
|
||||
window.scrollBy(0, amount);
|
||||
highlightResult("visible")();
|
||||
};
|
||||
|
||||
const scrollPageTo = (position: number, nav: HighlightResultElement): void => {
|
||||
window.scrollTo(0, position);
|
||||
highlightResult(nav)();
|
||||
};
|
||||
|
||||
const searchInputFocus = (): void => {
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
const q = document.querySelector<HTMLInputElement>("#q");
|
||||
if (q) {
|
||||
q.focus();
|
||||
|
||||
if (q.setSelectionRange) {
|
||||
const len = q.value.length;
|
||||
|
||||
q.setSelectionRange(len, len);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openResult = (newTab: boolean): void => {
|
||||
let link = document.querySelector<HTMLAnchorElement>(".result[data-vim-selected] h3 a");
|
||||
if (!link) {
|
||||
link = document.querySelector<HTMLAnchorElement>(".result[data-vim-selected] > a");
|
||||
}
|
||||
if (!link) return;
|
||||
|
||||
const url = link.getAttribute("href");
|
||||
if (url) {
|
||||
if (newTab) {
|
||||
window.open(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const initHelpContent = (divElement: HTMLElement, keyBindings: typeof baseKeyBinding): void => {
|
||||
const categories: Record<string, KeyBinding[]> = {};
|
||||
|
||||
for (const binding of Object.values(keyBindings)) {
|
||||
const { cat } = binding;
|
||||
categories[cat] ??= [];
|
||||
categories[cat].push(binding);
|
||||
}
|
||||
|
||||
const sortedCategoryKeys = Object.keys(categories).sort(
|
||||
(a, b) => (categories[b]?.length ?? 0) - (categories[a]?.length ?? 0)
|
||||
);
|
||||
|
||||
let html = '<a href="#" class="close" aria-label="close" title="close">×</a>';
|
||||
html += "<h3>How to navigate SearXNG with hotkeys</h3>";
|
||||
html += "<table>";
|
||||
|
||||
for (const [i, categoryKey] of sortedCategoryKeys.entries()) {
|
||||
const bindings = categories[categoryKey];
|
||||
if (!bindings || bindings.length === 0) continue;
|
||||
|
||||
const isFirst = i % 2 === 0;
|
||||
const isLast = i === sortedCategoryKeys.length - 1;
|
||||
|
||||
if (isFirst) {
|
||||
html += "<tr>";
|
||||
}
|
||||
|
||||
html += "<td>";
|
||||
html += `<h4>${categoryKey}</h4>`;
|
||||
html += '<ul class="list-unstyled">';
|
||||
|
||||
for (const binding of bindings) {
|
||||
html += `<li><kbd>${binding.key}</kbd> ${binding.des}</li>`;
|
||||
}
|
||||
|
||||
html += "</ul>";
|
||||
html += "</td>";
|
||||
|
||||
if (!isFirst || isLast) {
|
||||
html += "</tr>";
|
||||
}
|
||||
}
|
||||
|
||||
html += "</table>";
|
||||
|
||||
divElement.innerHTML = html;
|
||||
};
|
||||
|
||||
const toggleHelp = (keyBindings: typeof baseKeyBinding): void => {
|
||||
let helpPanel = document.querySelector<HTMLElement>("#vim-hotkeys-help");
|
||||
if (helpPanel) {
|
||||
// toggle hidden
|
||||
helpPanel.classList.toggle("invisible");
|
||||
} else {
|
||||
// first call
|
||||
helpPanel = Object.assign(document.createElement("div"), {
|
||||
id: "vim-hotkeys-help",
|
||||
className: "dialog-modal"
|
||||
});
|
||||
initHelpContent(helpPanel, keyBindings);
|
||||
const [body] = document.getElementsByTagName("body");
|
||||
if (body) {
|
||||
body.appendChild(helpPanel);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const copyURLToClipboard = async (): Promise<void> => {
|
||||
const selectedResult = document.querySelector<HTMLElement>(".result[data-vim-selected]");
|
||||
if (!selectedResult) return;
|
||||
|
||||
const resultAnchor = selectedResult.querySelector<HTMLAnchorElement>("a");
|
||||
assertElement(resultAnchor);
|
||||
|
||||
const url = resultAnchor.getAttribute("href");
|
||||
if (url) {
|
||||
if (window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(url);
|
||||
} else {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
const node = document.createElement("span");
|
||||
node.textContent = url;
|
||||
resultAnchor.appendChild(node);
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand("copy");
|
||||
node.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
listen("click", ".result", function (this: HTMLElement, event: PointerEvent) {
|
||||
if (!isElementInDetail(event.target as HTMLElement)) {
|
||||
highlightResult(this)(true, true);
|
||||
|
||||
const resultElement = getResultElement(event.target as HTMLElement);
|
||||
|
||||
if (resultElement && isImageResult(resultElement)) {
|
||||
event.preventDefault();
|
||||
mutable.selectImage?.(resultElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// FIXME: Focus might also trigger Pointer event ^^^
|
||||
listen(
|
||||
"focus",
|
||||
".result a",
|
||||
(event: FocusEvent) => {
|
||||
if (!isElementInDetail(event.target as HTMLElement)) {
|
||||
const resultElement = getResultElement(event.target as HTMLElement);
|
||||
|
||||
if (resultElement && !resultElement.hasAttribute("data-vim-selected")) {
|
||||
highlightResult(resultElement)(true);
|
||||
}
|
||||
|
||||
if (resultElement && isImageResult(resultElement)) {
|
||||
event.preventDefault();
|
||||
mutable.selectImage?.(resultElement);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
|
||||
listen("keydown", document, (event: KeyboardEvent) => {
|
||||
// check for modifiers so we don't break browser's hotkeys
|
||||
if (Object.hasOwn(keyBindings, event.key) && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) {
|
||||
const tagName = (event.target as HTMLElement)?.tagName?.toLowerCase();
|
||||
|
||||
if (event.key === "Escape") {
|
||||
keyBindings[event.key]?.fun(event);
|
||||
} else if (event.target === document.body || tagName === "a" || tagName === "button") {
|
||||
event.preventDefault();
|
||||
keyBindings[event.key]?.fun(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mutable.selectNext = highlightResult("down");
|
||||
mutable.selectPrevious = highlightResult("up");
|
||||
77
client/simple/src/js/main/mapresult.js
Normal file
77
client/simple/src/js/main/mapresult.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
/* global L */
|
||||
((_w, _d, searxng) => {
|
||||
searxng.ready(() => {
|
||||
searxng.on(".searxng_init_map", "click", function (event) {
|
||||
// no more request
|
||||
this.classList.remove("searxng_init_map");
|
||||
|
||||
//
|
||||
const leaflet_target = this.dataset.leafletTarget;
|
||||
const map_lon = parseFloat(this.dataset.mapLon);
|
||||
const map_lat = parseFloat(this.dataset.mapLat);
|
||||
const map_zoom = parseFloat(this.dataset.mapZoom);
|
||||
const map_boundingbox = JSON.parse(this.dataset.mapBoundingbox);
|
||||
const map_geojson = JSON.parse(this.dataset.mapGeojson);
|
||||
|
||||
searxng.loadStyle("css/leaflet.css");
|
||||
searxng.loadScript("js/leaflet.js", () => {
|
||||
let map_bounds = null;
|
||||
if (map_boundingbox) {
|
||||
const southWest = L.latLng(map_boundingbox[0], map_boundingbox[2]);
|
||||
const northEast = L.latLng(map_boundingbox[1], map_boundingbox[3]);
|
||||
map_bounds = L.latLngBounds(southWest, northEast);
|
||||
}
|
||||
|
||||
// init map
|
||||
const map = L.map(leaflet_target);
|
||||
// create the tile layer with correct attribution
|
||||
const osmMapnikUrl = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||
const osmMapnikAttrib = 'Map data © <a href="https://openstreetmap.org">OpenStreetMap</a> contributors';
|
||||
const osmMapnik = new L.TileLayer(osmMapnikUrl, { minZoom: 1, maxZoom: 19, attribution: osmMapnikAttrib });
|
||||
const osmWikimediaUrl = "https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png";
|
||||
const osmWikimediaAttrib =
|
||||
'Wikimedia maps | Maps data © <a href="https://openstreetmap.org">OpenStreetMap contributors</a>';
|
||||
const osmWikimedia = new L.TileLayer(osmWikimediaUrl, {
|
||||
minZoom: 1,
|
||||
maxZoom: 19,
|
||||
attribution: osmWikimediaAttrib
|
||||
});
|
||||
// init map view
|
||||
if (map_bounds) {
|
||||
// TODO hack: https://github.com/Leaflet/Leaflet/issues/2021
|
||||
// Still useful ?
|
||||
setTimeout(() => {
|
||||
map.fitBounds(map_bounds, {
|
||||
maxZoom: 17
|
||||
});
|
||||
}, 0);
|
||||
} else if (map_lon && map_lat) {
|
||||
if (map_zoom) {
|
||||
map.setView(new L.LatLng(map_lat, map_lon), map_zoom);
|
||||
} else {
|
||||
map.setView(new L.LatLng(map_lat, map_lon), 8);
|
||||
}
|
||||
}
|
||||
|
||||
map.addLayer(osmMapnik);
|
||||
|
||||
const baseLayers = {
|
||||
"OSM Mapnik": osmMapnik,
|
||||
"OSM Wikimedia": osmWikimedia
|
||||
};
|
||||
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
|
||||
if (map_geojson) {
|
||||
L.geoJson(map_geojson).addTo(map);
|
||||
} /* else if(map_bounds) {
|
||||
L.rectangle(map_bounds, {color: "#ff7800", weight: 3, fill:false}).addTo(map);
|
||||
} */
|
||||
});
|
||||
|
||||
// this event occur only once per element
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
})(window, document, window.searxng);
|
||||
52
client/simple/src/js/main/preferences.js
Normal file
52
client/simple/src/js/main/preferences.js
Normal file
@@ -0,0 +1,52 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
((_w, d, searxng) => {
|
||||
if (searxng.endpoint !== "preferences") {
|
||||
return;
|
||||
}
|
||||
|
||||
searxng.ready(() => {
|
||||
let engine_descriptions = null;
|
||||
|
||||
function load_engine_descriptions() {
|
||||
if (engine_descriptions == null) {
|
||||
searxng.http("GET", "engine_descriptions.json").then((content) => {
|
||||
engine_descriptions = JSON.parse(content);
|
||||
for (const [engine_name, description] of Object.entries(engine_descriptions)) {
|
||||
const elements = d.querySelectorAll(`[data-engine-name="${engine_name}"] .engine-description`);
|
||||
for (const element of elements) {
|
||||
const source = ` (<i>${searxng.settings.translations.Source}: ${description[1]}</i>)`;
|
||||
element.innerHTML = description[0] + source;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const el of d.querySelectorAll("[data-engine-name]")) {
|
||||
searxng.on(el, "mouseenter", load_engine_descriptions);
|
||||
}
|
||||
|
||||
const enableAllEngines = d.querySelectorAll(".enable-all-engines");
|
||||
const disableAllEngines = d.querySelectorAll(".disable-all-engines");
|
||||
const engineToggles = d.querySelectorAll("tbody input[type=checkbox][class~=checkbox-onoff]");
|
||||
const toggleEngines = (enable) => {
|
||||
for (const el of engineToggles) {
|
||||
// check if element visible, so that only engines of the current category are modified
|
||||
if (el.offsetParent !== null) el.checked = !enable;
|
||||
}
|
||||
};
|
||||
for (const el of enableAllEngines) {
|
||||
searxng.on(el, "click", () => toggleEngines(true));
|
||||
}
|
||||
for (const el of disableAllEngines) {
|
||||
searxng.on(el, "click", () => toggleEngines(false));
|
||||
}
|
||||
|
||||
const copyHashButton = d.querySelector("#copy-hash");
|
||||
searxng.on(copyHashButton, "click", (e) => {
|
||||
e.preventDefault();
|
||||
navigator.clipboard.writeText(copyHashButton.dataset.hash);
|
||||
copyHashButton.innerText = copyHashButton.dataset.copiedText;
|
||||
});
|
||||
});
|
||||
})(window, document, window.searxng);
|
||||
@@ -1,76 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { http, listen, settings } from "../toolkit.ts";
|
||||
import { assertElement } from "../util/assertElement.ts";
|
||||
|
||||
let engineDescriptions: Record<string, [string, string]> | undefined;
|
||||
|
||||
const loadEngineDescriptions = async (): Promise<void> => {
|
||||
if (engineDescriptions) return;
|
||||
try {
|
||||
const res = await http("GET", "engine_descriptions.json");
|
||||
engineDescriptions = await res.json();
|
||||
} catch (error) {
|
||||
console.error("Error fetching engineDescriptions:", error);
|
||||
}
|
||||
if (!engineDescriptions) return;
|
||||
|
||||
for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) {
|
||||
const elements = document.querySelectorAll<HTMLElement>(`[data-engine-name="${engine_name}"] .engine-description`);
|
||||
const sourceText = ` (<i>${settings.translations?.Source}: ${source}</i>)`;
|
||||
|
||||
for (const element of elements) {
|
||||
element.innerHTML = description + sourceText;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleEngines = (enable: boolean, engineToggles: NodeListOf<HTMLInputElement>): void => {
|
||||
for (const engineToggle of engineToggles) {
|
||||
// check if element visible, so that only engines of the current category are modified
|
||||
if (engineToggle.offsetParent) {
|
||||
engineToggle.checked = !enable;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engineElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>("[data-engine-name]");
|
||||
for (const engineElement of engineElements) {
|
||||
listen("mouseenter", engineElement, loadEngineDescriptions);
|
||||
}
|
||||
|
||||
const engineToggles: NodeListOf<HTMLInputElement> = document.querySelectorAll<HTMLInputElement>(
|
||||
"tbody input[type=checkbox][class~=checkbox-onoff]"
|
||||
);
|
||||
|
||||
const enableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".enable-all-engines");
|
||||
for (const engine of enableAllEngines) {
|
||||
listen("click", engine, () => toggleEngines(true, engineToggles));
|
||||
}
|
||||
|
||||
const disableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".disable-all-engines");
|
||||
for (const engine of disableAllEngines) {
|
||||
listen("click", engine, () => toggleEngines(false, engineToggles));
|
||||
}
|
||||
|
||||
listen("click", "#copy-hash", async function (this: HTMLElement) {
|
||||
const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
|
||||
assertElement(target);
|
||||
|
||||
if (window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(target.innerText);
|
||||
} else {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand("copy");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.dataset.copiedText) {
|
||||
this.innerText = this.dataset.copiedText;
|
||||
}
|
||||
});
|
||||
182
client/simple/src/js/main/results.js
Normal file
182
client/simple/src/js/main/results.js
Normal file
@@ -0,0 +1,182 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
import "../../../node_modules/swiped-events/src/swiped-events.js";
|
||||
|
||||
((w, d, searxng) => {
|
||||
if (searxng.endpoint !== "results") {
|
||||
return;
|
||||
}
|
||||
|
||||
searxng.ready(() => {
|
||||
d.querySelectorAll("#urls img").forEach((img) =>
|
||||
img.addEventListener(
|
||||
"error",
|
||||
() => {
|
||||
// console.log("ERROR can't load: " + img.src);
|
||||
img.src = `${window.searxng.settings.theme_static_path}/img/img_load_error.svg`;
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
);
|
||||
|
||||
if (d.querySelector("#search_url button#copy_url")) {
|
||||
d.querySelector("#search_url button#copy_url").style.display = "block";
|
||||
}
|
||||
|
||||
searxng.on(".btn-collapse", "click", function () {
|
||||
const btnLabelCollapsed = this.getAttribute("data-btn-text-collapsed");
|
||||
const btnLabelNotCollapsed = this.getAttribute("data-btn-text-not-collapsed");
|
||||
const target = this.getAttribute("data-target");
|
||||
const targetElement = d.querySelector(target);
|
||||
let html = this.innerHTML;
|
||||
if (this.classList.contains("collapsed")) {
|
||||
html = html.replace(btnLabelCollapsed, btnLabelNotCollapsed);
|
||||
} else {
|
||||
html = html.replace(btnLabelNotCollapsed, btnLabelCollapsed);
|
||||
}
|
||||
this.innerHTML = html;
|
||||
this.classList.toggle("collapsed");
|
||||
targetElement.classList.toggle("invisible");
|
||||
});
|
||||
|
||||
searxng.on(".media-loader", "click", function () {
|
||||
const target = this.getAttribute("data-target");
|
||||
const iframe_load = d.querySelector(`${target} > iframe`);
|
||||
const srctest = iframe_load.getAttribute("src");
|
||||
if (srctest === null || srctest === undefined || srctest === false) {
|
||||
iframe_load.setAttribute("src", iframe_load.getAttribute("data-src"));
|
||||
}
|
||||
});
|
||||
|
||||
searxng.on("#copy_url", "click", function () {
|
||||
const target = this.parentElement.querySelector("pre");
|
||||
navigator.clipboard.writeText(target.innerText);
|
||||
this.innerText = this.dataset.copiedText;
|
||||
});
|
||||
|
||||
// searxng.selectImage (gallery)
|
||||
// -----------------------------
|
||||
|
||||
// setTimeout() ID, needed to cancel *last* loadImage
|
||||
let imgTimeoutID;
|
||||
|
||||
// progress spinner, while an image is loading
|
||||
const imgLoaderSpinner = d.createElement("div");
|
||||
imgLoaderSpinner.classList.add("loader");
|
||||
|
||||
// singleton image object, which is used for all loading processes of a
|
||||
// detailed image
|
||||
const imgLoader = new Image();
|
||||
|
||||
const loadImage = (imgSrc, onSuccess) => {
|
||||
// if defered image load exists, stop defered task.
|
||||
if (imgTimeoutID) clearTimeout(imgTimeoutID);
|
||||
|
||||
// defer load of the detail image for 1 sec
|
||||
imgTimeoutID = setTimeout(() => {
|
||||
imgLoader.src = imgSrc;
|
||||
}, 1000);
|
||||
|
||||
// set handlers in the on-properties
|
||||
imgLoader.onload = () => {
|
||||
onSuccess();
|
||||
imgLoaderSpinner.remove();
|
||||
};
|
||||
imgLoader.onerror = () => {
|
||||
imgLoaderSpinner.remove();
|
||||
};
|
||||
};
|
||||
|
||||
searxng.selectImage = (resultElement) => {
|
||||
// add a class that can be evaluated in the CSS and indicates that the
|
||||
// detail view is open
|
||||
d.getElementById("results").classList.add("image-detail-open");
|
||||
|
||||
// add a hash to the browser history so that pressing back doesn't return
|
||||
// to the previous page this allows us to dismiss the image details on
|
||||
// pressing the back button on mobile devices
|
||||
window.location.hash = "#image-viewer";
|
||||
|
||||
searxng.scrollPageToSelected();
|
||||
|
||||
// if there is none element given by the caller, stop here
|
||||
if (!resultElement) return;
|
||||
|
||||
// find <img> object in the element, if there is none, stop here.
|
||||
const img = resultElement.querySelector(".result-images-source img");
|
||||
if (!img) return;
|
||||
|
||||
// <img src="" data-src="http://example.org/image.jpg">
|
||||
const src = img.getAttribute("data-src");
|
||||
|
||||
// already loaded high-res image or no high-res image available
|
||||
if (!src) return;
|
||||
|
||||
// use the image thumbnail until the image is fully loaded
|
||||
const thumbnail = resultElement.querySelector(".image_thumbnail");
|
||||
img.src = thumbnail.src;
|
||||
|
||||
// show a progress spinner
|
||||
const detailElement = resultElement.querySelector(".detail");
|
||||
detailElement.appendChild(imgLoaderSpinner);
|
||||
|
||||
// load full size image in background
|
||||
loadImage(src, () => {
|
||||
// after the singelton loadImage has loaded the detail image into the
|
||||
// cache, it can be used in the origin <img> as src property.
|
||||
img.src = src;
|
||||
img.removeAttribute("data-src");
|
||||
});
|
||||
};
|
||||
|
||||
searxng.closeDetail = () => {
|
||||
d.getElementById("results").classList.remove("image-detail-open");
|
||||
// remove #image-viewer hash from url by navigating back
|
||||
if (window.location.hash === "#image-viewer") window.history.back();
|
||||
searxng.scrollPageToSelected();
|
||||
};
|
||||
searxng.on(".result-detail-close", "click", (e) => {
|
||||
e.preventDefault();
|
||||
searxng.closeDetail();
|
||||
});
|
||||
searxng.on(".result-detail-previous", "click", (e) => {
|
||||
e.preventDefault();
|
||||
searxng.selectPrevious(false);
|
||||
});
|
||||
searxng.on(".result-detail-next", "click", (e) => {
|
||||
e.preventDefault();
|
||||
searxng.selectNext(false);
|
||||
});
|
||||
|
||||
// listen for the back button to be pressed and dismiss the image details when called
|
||||
window.addEventListener("hashchange", () => {
|
||||
if (window.location.hash !== "#image-viewer") searxng.closeDetail();
|
||||
});
|
||||
|
||||
d.querySelectorAll(".swipe-horizontal").forEach((obj) => {
|
||||
obj.addEventListener("swiped-left", () => {
|
||||
searxng.selectNext(false);
|
||||
});
|
||||
obj.addEventListener("swiped-right", () => {
|
||||
searxng.selectPrevious(false);
|
||||
});
|
||||
});
|
||||
|
||||
w.addEventListener(
|
||||
"scroll",
|
||||
() => {
|
||||
const e = d.getElementById("backToTop"),
|
||||
scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
|
||||
results = d.getElementById("results");
|
||||
if (e !== null) {
|
||||
if (scrollTop >= 100) {
|
||||
results.classList.add("scrolling");
|
||||
} else {
|
||||
results.classList.remove("scrolling");
|
||||
}
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
})(window, document, window.searxng);
|
||||
@@ -1,189 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import "../../../node_modules/swiped-events/src/swiped-events.js";
|
||||
import { listen, mutable, settings } from "../toolkit.ts";
|
||||
import { assertElement } from "../util/assertElement.ts";
|
||||
|
||||
let imgTimeoutID: number;
|
||||
|
||||
const imageLoader = (resultElement: HTMLElement): void => {
|
||||
if (imgTimeoutID) clearTimeout(imgTimeoutID);
|
||||
|
||||
const imgElement = resultElement.querySelector<HTMLImageElement>(".result-images-source img");
|
||||
if (!imgElement) return;
|
||||
|
||||
// use thumbnail until full image loads
|
||||
const thumbnail = resultElement.querySelector<HTMLImageElement>(".image_thumbnail");
|
||||
if (thumbnail) {
|
||||
if (thumbnail.src === `${settings.theme_static_path}/img/img_load_error.svg`) return;
|
||||
|
||||
imgElement.onerror = (): void => {
|
||||
imgElement.src = thumbnail.src;
|
||||
};
|
||||
|
||||
imgElement.src = thumbnail.src;
|
||||
}
|
||||
|
||||
const imgSource = imgElement.getAttribute("data-src");
|
||||
if (!imgSource) return;
|
||||
|
||||
// unsafe nodejs specific, cast to https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#return_value
|
||||
// https://github.com/searxng/searxng/pull/5073#discussion_r2265767231
|
||||
imgTimeoutID = setTimeout(() => {
|
||||
imgElement.src = imgSource;
|
||||
imgElement.removeAttribute("data-src");
|
||||
}, 1000) as unknown as number;
|
||||
};
|
||||
|
||||
const imageThumbnails: NodeListOf<HTMLImageElement> =
|
||||
document.querySelectorAll<HTMLImageElement>("#urls img.image_thumbnail");
|
||||
for (const thumbnail of imageThumbnails) {
|
||||
if (thumbnail.complete && thumbnail.naturalWidth === 0) {
|
||||
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
|
||||
}
|
||||
|
||||
thumbnail.onerror = (): void => {
|
||||
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
|
||||
};
|
||||
}
|
||||
|
||||
const copyUrlButton: HTMLButtonElement | null =
|
||||
document.querySelector<HTMLButtonElement>("#search_url button#copy_url");
|
||||
copyUrlButton?.style.setProperty("display", "block");
|
||||
|
||||
mutable.selectImage = (resultElement: HTMLElement): void => {
|
||||
// add a class that can be evaluated in the CSS and indicates that the
|
||||
// detail view is open
|
||||
const resultsElement = document.getElementById("results");
|
||||
resultsElement?.classList.add("image-detail-open");
|
||||
|
||||
// add a hash to the browser history so that pressing back doesn't return
|
||||
// to the previous page this allows us to dismiss the image details on
|
||||
// pressing the back button on mobile devices
|
||||
window.location.hash = "#image-viewer";
|
||||
|
||||
mutable.scrollPageToSelected?.();
|
||||
|
||||
// if there is no element given by the caller, stop here
|
||||
if (!resultElement) return;
|
||||
|
||||
imageLoader(resultElement);
|
||||
};
|
||||
|
||||
mutable.closeDetail = (): void => {
|
||||
const resultsElement = document.getElementById("results");
|
||||
resultsElement?.classList.remove("image-detail-open");
|
||||
|
||||
// remove #image-viewer hash from url by navigating back
|
||||
if (window.location.hash === "#image-viewer") {
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
mutable.scrollPageToSelected?.();
|
||||
};
|
||||
|
||||
listen("click", ".btn-collapse", function (this: HTMLElement) {
|
||||
const btnLabelCollapsed = this.getAttribute("data-btn-text-collapsed");
|
||||
const btnLabelNotCollapsed = this.getAttribute("data-btn-text-not-collapsed");
|
||||
const target = this.getAttribute("data-target");
|
||||
|
||||
if (!(target && btnLabelCollapsed && btnLabelNotCollapsed)) return;
|
||||
|
||||
const targetElement = document.querySelector<HTMLElement>(target);
|
||||
assertElement(targetElement);
|
||||
|
||||
const isCollapsed = this.classList.contains("collapsed");
|
||||
const newLabel = isCollapsed ? btnLabelNotCollapsed : btnLabelCollapsed;
|
||||
const oldLabel = isCollapsed ? btnLabelCollapsed : btnLabelNotCollapsed;
|
||||
|
||||
this.innerHTML = this.innerHTML.replace(oldLabel, newLabel);
|
||||
this.classList.toggle("collapsed");
|
||||
|
||||
targetElement.classList.toggle("invisible");
|
||||
});
|
||||
|
||||
listen("click", ".media-loader", function (this: HTMLElement) {
|
||||
const target = this.getAttribute("data-target");
|
||||
if (!target) return;
|
||||
|
||||
const iframeLoad = document.querySelector<HTMLIFrameElement>(`${target} > iframe`);
|
||||
assertElement(iframeLoad);
|
||||
|
||||
const srctest = iframeLoad.getAttribute("src");
|
||||
if (!srctest) {
|
||||
const dataSrc = iframeLoad.getAttribute("data-src");
|
||||
if (dataSrc) {
|
||||
iframeLoad.setAttribute("src", dataSrc);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
listen("click", "#copy_url", async function (this: HTMLElement) {
|
||||
const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
|
||||
assertElement(target);
|
||||
|
||||
if (window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(target.innerText);
|
||||
} else {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand("copy");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.dataset.copiedText) {
|
||||
this.innerText = this.dataset.copiedText;
|
||||
}
|
||||
});
|
||||
|
||||
listen("click", ".result-detail-close", (event: Event) => {
|
||||
event.preventDefault();
|
||||
mutable.closeDetail?.();
|
||||
});
|
||||
|
||||
listen("click", ".result-detail-previous", (event: Event) => {
|
||||
event.preventDefault();
|
||||
mutable.selectPrevious?.(false);
|
||||
});
|
||||
|
||||
listen("click", ".result-detail-next", (event: Event) => {
|
||||
event.preventDefault();
|
||||
mutable.selectNext?.(false);
|
||||
});
|
||||
|
||||
// listen for the back button to be pressed and dismiss the image details when called
|
||||
window.addEventListener("hashchange", () => {
|
||||
if (window.location.hash !== "#image-viewer") {
|
||||
mutable.closeDetail?.();
|
||||
}
|
||||
});
|
||||
|
||||
const swipeHorizontal: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".swipe-horizontal");
|
||||
for (const element of swipeHorizontal) {
|
||||
listen("swiped-left", element, () => {
|
||||
mutable.selectNext?.(false);
|
||||
});
|
||||
|
||||
listen("swiped-right", element, () => {
|
||||
mutable.selectPrevious?.(false);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener(
|
||||
"scroll",
|
||||
() => {
|
||||
const backToTopElement = document.getElementById("backToTop");
|
||||
const resultsElement = document.getElementById("results");
|
||||
|
||||
if (backToTopElement && resultsElement) {
|
||||
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const isScrolling = scrollTop >= 100;
|
||||
resultsElement.classList.toggle("scrolling", isScrolling);
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
197
client/simple/src/js/main/search.js
Normal file
197
client/simple/src/js/main/search.js
Normal file
@@ -0,0 +1,197 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
/* exported AutoComplete */
|
||||
|
||||
((_w, d, searxng) => {
|
||||
const qinput_id = "q";
|
||||
let qinput;
|
||||
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 50em)").matches;
|
||||
const isResultsPage = document.querySelector("main").id === "main_results";
|
||||
|
||||
function submitIfQuery() {
|
||||
if (qinput.value.length > 0) {
|
||||
const search = document.getElementById("search");
|
||||
setTimeout(search.submit.bind(search), 0);
|
||||
}
|
||||
}
|
||||
|
||||
function createClearButton(qinput) {
|
||||
const cs = document.getElementById("clear_search");
|
||||
const updateClearButton = () => {
|
||||
if (qinput.value.length === 0) {
|
||||
cs.classList.add("empty");
|
||||
} else {
|
||||
cs.classList.remove("empty");
|
||||
}
|
||||
};
|
||||
|
||||
// update status, event listener
|
||||
updateClearButton();
|
||||
cs.addEventListener("click", (ev) => {
|
||||
qinput.value = "";
|
||||
qinput.focus();
|
||||
updateClearButton();
|
||||
ev.preventDefault();
|
||||
});
|
||||
qinput.addEventListener("input", updateClearButton, false);
|
||||
}
|
||||
|
||||
const fetchResults = async (query) => {
|
||||
let request;
|
||||
if (searxng.settings.method === "GET") {
|
||||
const reqParams = new URLSearchParams();
|
||||
reqParams.append("q", query);
|
||||
request = fetch(`./autocompleter?${reqParams.toString()}`);
|
||||
} else {
|
||||
const formData = new FormData();
|
||||
formData.append("q", query);
|
||||
request = fetch("./autocompleter", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
}
|
||||
|
||||
request.then(async (response) => {
|
||||
const results = await response.json();
|
||||
|
||||
if (!results) return;
|
||||
|
||||
const autocomplete = d.querySelector(".autocomplete");
|
||||
const autocompleteList = d.querySelector(".autocomplete ul");
|
||||
autocomplete.classList.add("open");
|
||||
autocompleteList.innerHTML = "";
|
||||
|
||||
// show an error message that no result was found
|
||||
if (!results[1] || results[1].length === 0) {
|
||||
const noItemFoundMessage = document.createElement("li");
|
||||
noItemFoundMessage.classList.add("no-item-found");
|
||||
noItemFoundMessage.innerHTML = searxng.settings.translations.no_item_found;
|
||||
autocompleteList.appendChild(noItemFoundMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const result of results[1]) {
|
||||
const li = document.createElement("li");
|
||||
li.innerText = result;
|
||||
|
||||
searxng.on(li, "mousedown", () => {
|
||||
qinput.value = result;
|
||||
const form = d.querySelector("#search");
|
||||
form.submit();
|
||||
autocomplete.classList.remove("open");
|
||||
});
|
||||
autocompleteList.appendChild(li);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
searxng.ready(() => {
|
||||
// focus search input on large screens
|
||||
if (!isMobile && !isResultsPage) document.getElementById("q").focus();
|
||||
|
||||
qinput = d.getElementById(qinput_id);
|
||||
const autocomplete = d.querySelector(".autocomplete");
|
||||
const autocompleteList = d.querySelector(".autocomplete ul");
|
||||
|
||||
if (qinput !== null) {
|
||||
// clear button
|
||||
createClearButton(qinput);
|
||||
|
||||
// autocompleter
|
||||
if (searxng.settings.autocomplete) {
|
||||
searxng.on(qinput, "input", () => {
|
||||
const query = qinput.value;
|
||||
if (query.length < searxng.settings.autocomplete_min) return;
|
||||
|
||||
setTimeout(() => {
|
||||
if (query === qinput.value) fetchResults(query);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
searxng.on(qinput, "keyup", (e) => {
|
||||
let currentIndex = -1;
|
||||
const listItems = autocompleteList.children;
|
||||
for (let i = 0; i < listItems.length; i++) {
|
||||
if (listItems[i].classList.contains("active")) {
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let newCurrentIndex = -1;
|
||||
if (e.key === "ArrowUp") {
|
||||
if (currentIndex >= 0) listItems[currentIndex].classList.remove("active");
|
||||
// we need to add listItems.length to the index calculation here because the JavaScript modulos
|
||||
// operator doesn't work with negative numbers
|
||||
newCurrentIndex = (currentIndex - 1 + listItems.length) % listItems.length;
|
||||
} else if (e.key === "ArrowDown") {
|
||||
if (currentIndex >= 0) listItems[currentIndex].classList.remove("active");
|
||||
newCurrentIndex = (currentIndex + 1) % listItems.length;
|
||||
} else if (e.key === "Tab" || e.key === "Enter") {
|
||||
autocomplete.classList.remove("open");
|
||||
}
|
||||
|
||||
if (newCurrentIndex !== -1) {
|
||||
const selectedItem = listItems[newCurrentIndex];
|
||||
selectedItem.classList.add("active");
|
||||
|
||||
if (!selectedItem.classList.contains("no-item-found")) qinput.value = selectedItem.innerText;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Additionally to searching when selecting a new category, we also
|
||||
// automatically start a new search request when the user changes a search
|
||||
// filter (safesearch, time range or language) (this requires JavaScript
|
||||
// though)
|
||||
if (
|
||||
qinput !== null &&
|
||||
searxng.settings.search_on_category_select &&
|
||||
// If .search_filters is undefined (invisible) we are on the homepage and
|
||||
// hence don't have to set any listeners
|
||||
d.querySelector(".search_filters") != null
|
||||
) {
|
||||
searxng.on(d.getElementById("safesearch"), "change", submitIfQuery);
|
||||
searxng.on(d.getElementById("time_range"), "change", submitIfQuery);
|
||||
searxng.on(d.getElementById("language"), "change", submitIfQuery);
|
||||
}
|
||||
|
||||
const categoryButtons = d.querySelectorAll("button.category_button");
|
||||
for (const button of categoryButtons) {
|
||||
searxng.on(button, "click", (event) => {
|
||||
if (event.shiftKey) {
|
||||
event.preventDefault();
|
||||
button.classList.toggle("selected");
|
||||
return;
|
||||
}
|
||||
|
||||
// manually deselect the old selection when a new category is selected
|
||||
const selectedCategories = d.querySelectorAll("button.category_button.selected");
|
||||
for (const categoryButton of selectedCategories) {
|
||||
categoryButton.classList.remove("selected");
|
||||
}
|
||||
button.classList.add("selected");
|
||||
});
|
||||
}
|
||||
|
||||
// override form submit action to update the actually selected categories
|
||||
const form = d.querySelector("#search");
|
||||
if (form != null) {
|
||||
searxng.on(form, "submit", (event) => {
|
||||
event.preventDefault();
|
||||
const categoryValuesInput = d.querySelector("#selected-categories");
|
||||
if (categoryValuesInput) {
|
||||
const categoryValues = [];
|
||||
for (const categoryButton of categoryButtons) {
|
||||
if (categoryButton.classList.contains("selected")) {
|
||||
categoryValues.push(categoryButton.name.replace("category_", ""));
|
||||
}
|
||||
}
|
||||
categoryValuesInput.value = categoryValues.join(",");
|
||||
}
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
});
|
||||
})(window, document, window.searxng);
|
||||
@@ -1,94 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { listen } from "../toolkit.ts";
|
||||
import { getElement } from "../util/getElement.ts";
|
||||
|
||||
const searchForm: HTMLFormElement = getElement<HTMLFormElement>("search");
|
||||
const searchInput: HTMLInputElement = getElement<HTMLInputElement>("q");
|
||||
const searchReset: HTMLButtonElement = getElement<HTMLButtonElement>("clear_search");
|
||||
|
||||
const isMobile: boolean = window.matchMedia("(max-width: 50em)").matches;
|
||||
const isResultsPage: boolean = document.querySelector("main")?.id === "main_results";
|
||||
|
||||
const categoryButtons: HTMLButtonElement[] = Array.from(
|
||||
document.querySelectorAll<HTMLButtonElement>("#categories_container button.category")
|
||||
);
|
||||
|
||||
if (searchInput.value.length === 0) {
|
||||
searchReset.classList.add("empty");
|
||||
}
|
||||
|
||||
// focus search input on large screens
|
||||
if (!(isMobile || isResultsPage)) {
|
||||
searchInput.focus();
|
||||
}
|
||||
|
||||
// On mobile, move cursor to the end of the input on focus
|
||||
if (isMobile) {
|
||||
listen("focus", searchInput, () => {
|
||||
// Defer cursor move until the next frame to prevent a visual jump
|
||||
requestAnimationFrame(() => {
|
||||
const end = searchInput.value.length;
|
||||
searchInput.setSelectionRange(end, end);
|
||||
searchInput.scrollLeft = searchInput.scrollWidth;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
listen("input", searchInput, () => {
|
||||
searchReset.classList.toggle("empty", searchInput.value.length === 0);
|
||||
});
|
||||
|
||||
listen("click", searchReset, (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
searchInput.value = "";
|
||||
searchInput.focus();
|
||||
searchReset.classList.add("empty");
|
||||
});
|
||||
|
||||
for (const button of categoryButtons) {
|
||||
listen("click", button, (event: MouseEvent) => {
|
||||
if (event.shiftKey) {
|
||||
event.preventDefault();
|
||||
button.classList.toggle("selected");
|
||||
return;
|
||||
}
|
||||
|
||||
// deselect all other categories
|
||||
for (const categoryButton of categoryButtons) {
|
||||
categoryButton.classList.toggle("selected", categoryButton === button);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.querySelector("div.search_filters")) {
|
||||
const safesearchElement = document.getElementById("safesearch");
|
||||
if (safesearchElement) {
|
||||
listen("change", safesearchElement, () => searchForm.submit());
|
||||
}
|
||||
|
||||
const timeRangeElement = document.getElementById("time_range");
|
||||
if (timeRangeElement) {
|
||||
listen("change", timeRangeElement, () => searchForm.submit());
|
||||
}
|
||||
|
||||
const languageElement = document.getElementById("language");
|
||||
if (languageElement) {
|
||||
listen("change", languageElement, () => searchForm.submit());
|
||||
}
|
||||
}
|
||||
|
||||
// override searchForm submit event
|
||||
listen("submit", searchForm, (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (categoryButtons.length > 0) {
|
||||
const searchCategories = getElement<HTMLInputElement>("selected-categories");
|
||||
searchCategories.value = categoryButtons
|
||||
.filter((button) => button.classList.contains("selected"))
|
||||
.map((button) => button.name.replace("category_", ""))
|
||||
.join(",");
|
||||
}
|
||||
|
||||
searchForm.submit();
|
||||
});
|
||||
@@ -1,93 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
absDependencies,
|
||||
addDependencies,
|
||||
create,
|
||||
divideDependencies,
|
||||
eDependencies,
|
||||
evaluateDependencies,
|
||||
expDependencies,
|
||||
factorialDependencies,
|
||||
gcdDependencies,
|
||||
lcmDependencies,
|
||||
log1pDependencies,
|
||||
log2Dependencies,
|
||||
log10Dependencies,
|
||||
logDependencies,
|
||||
modDependencies,
|
||||
multiplyDependencies,
|
||||
nthRootDependencies,
|
||||
piDependencies,
|
||||
powDependencies,
|
||||
roundDependencies,
|
||||
signDependencies,
|
||||
sqrtDependencies,
|
||||
subtractDependencies
|
||||
} from "mathjs/number";
|
||||
import { Plugin } from "../Plugin.ts";
|
||||
import { appendAnswerElement } from "../util/appendAnswerElement.ts";
|
||||
import { getElement } from "../util/getElement.ts";
|
||||
|
||||
/**
|
||||
* Parses and solves mathematical expressions. Can do basic arithmetic and
|
||||
* evaluate some functions.
|
||||
*
|
||||
* @example
|
||||
* "(3 + 5) / 2" = "4"
|
||||
* "e ^ 2 + pi" = "10.530648752520442"
|
||||
* "gcd(48, 18) + lcm(4, 5)" = "26"
|
||||
*
|
||||
* @remarks
|
||||
* Depends on `mathjs` library.
|
||||
*/
|
||||
export default class Calculator extends Plugin {
|
||||
public constructor() {
|
||||
super("calculator");
|
||||
}
|
||||
|
||||
/**
|
||||
* @remarks
|
||||
* Compare bundle size after adding or removing features.
|
||||
*/
|
||||
private static readonly math = create({
|
||||
...absDependencies,
|
||||
...addDependencies,
|
||||
...divideDependencies,
|
||||
...eDependencies,
|
||||
...evaluateDependencies,
|
||||
...expDependencies,
|
||||
...factorialDependencies,
|
||||
...gcdDependencies,
|
||||
...lcmDependencies,
|
||||
...log10Dependencies,
|
||||
...log1pDependencies,
|
||||
...log2Dependencies,
|
||||
...logDependencies,
|
||||
...modDependencies,
|
||||
...multiplyDependencies,
|
||||
...nthRootDependencies,
|
||||
...piDependencies,
|
||||
...powDependencies,
|
||||
...roundDependencies,
|
||||
...signDependencies,
|
||||
...sqrtDependencies,
|
||||
...subtractDependencies
|
||||
});
|
||||
|
||||
protected async run(): Promise<string | undefined> {
|
||||
const searchInput = getElement<HTMLInputElement>("q");
|
||||
|
||||
try {
|
||||
const node = Calculator.math.parse(searchInput.value);
|
||||
return `${node.toString()} = ${node.evaluate()}`;
|
||||
} catch {
|
||||
// not a compatible math expression
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected async post(result: string): Promise<void> {
|
||||
appendAnswerElement(result);
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { Plugin } from "../Plugin.ts";
|
||||
import { http, settings } from "../toolkit.ts";
|
||||
import { assertElement } from "../util/assertElement.ts";
|
||||
import { getElement } from "../util/getElement.ts";
|
||||
|
||||
/**
|
||||
* Automatically loads the next page when scrolling to bottom of the current page.
|
||||
*/
|
||||
export default class InfiniteScroll extends Plugin {
|
||||
public constructor() {
|
||||
super("infiniteScroll");
|
||||
}
|
||||
|
||||
protected async run(): Promise<void> {
|
||||
const resultsElement = getElement<HTMLElement>("results");
|
||||
|
||||
const onlyImages: boolean = resultsElement.classList.contains("only_template_images");
|
||||
const observedSelector = "article.result:last-child";
|
||||
|
||||
const spinnerElement = document.createElement("div");
|
||||
spinnerElement.className = "loader";
|
||||
|
||||
const loadNextPage = async (callback: () => void): Promise<void> => {
|
||||
const searchForm = document.querySelector<HTMLFormElement>("#search");
|
||||
assertElement(searchForm);
|
||||
|
||||
const form = document.querySelector<HTMLFormElement>("#pagination form.next_page");
|
||||
assertElement(form);
|
||||
|
||||
const action = searchForm.getAttribute("action");
|
||||
if (!action) {
|
||||
throw new Error("Form action not defined");
|
||||
}
|
||||
|
||||
const paginationElement = document.querySelector<HTMLElement>("#pagination");
|
||||
assertElement(paginationElement);
|
||||
|
||||
paginationElement.replaceChildren(spinnerElement);
|
||||
|
||||
try {
|
||||
const res = await http("POST", action, { body: new FormData(form) });
|
||||
const nextPage = await res.text();
|
||||
if (!nextPage) return;
|
||||
|
||||
const nextPageDoc = new DOMParser().parseFromString(nextPage, "text/html");
|
||||
const articleList = nextPageDoc.querySelectorAll<HTMLElement>("#urls article");
|
||||
const nextPaginationElement = nextPageDoc.querySelector<HTMLElement>("#pagination");
|
||||
|
||||
document.querySelector("#pagination")?.remove();
|
||||
|
||||
const urlsElement = document.querySelector<HTMLElement>("#urls");
|
||||
if (!urlsElement) {
|
||||
throw new Error("URLs element not found");
|
||||
}
|
||||
|
||||
if (articleList.length > 0 && !onlyImages) {
|
||||
// do not add <hr> element when there are only images
|
||||
urlsElement.appendChild(document.createElement("hr"));
|
||||
}
|
||||
|
||||
urlsElement.append(...articleList);
|
||||
|
||||
if (nextPaginationElement) {
|
||||
const results = document.querySelector<HTMLElement>("#results");
|
||||
results?.appendChild(nextPaginationElement);
|
||||
callback();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading next page:", error);
|
||||
|
||||
const errorElement = Object.assign(document.createElement("div"), {
|
||||
textContent: settings.translations?.error_loading_next_page ?? "Error loading next page",
|
||||
className: "dialog-error"
|
||||
});
|
||||
errorElement.setAttribute("role", "alert");
|
||||
document.querySelector("#pagination")?.replaceChildren(errorElement);
|
||||
}
|
||||
};
|
||||
|
||||
const intersectionObserveOptions: IntersectionObserverInit = {
|
||||
rootMargin: "320px"
|
||||
};
|
||||
|
||||
const observer: IntersectionObserver = new IntersectionObserver(async (entries: IntersectionObserverEntry[]) => {
|
||||
const [paginationEntry] = entries;
|
||||
|
||||
if (paginationEntry?.isIntersecting) {
|
||||
observer.unobserve(paginationEntry.target);
|
||||
|
||||
await loadNextPage(() => {
|
||||
const nextObservedElement = document.querySelector<HTMLElement>(observedSelector);
|
||||
if (nextObservedElement) {
|
||||
observer.observe(nextObservedElement);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, intersectionObserveOptions);
|
||||
|
||||
const initialObservedElement: HTMLElement | null = document.querySelector<HTMLElement>(observedSelector);
|
||||
if (initialObservedElement) {
|
||||
observer.observe(initialObservedElement);
|
||||
}
|
||||
}
|
||||
|
||||
protected async post(): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import "ol/ol.css";
|
||||
import { Feature, Map as OlMap, View } from "ol";
|
||||
import { GeoJSON } from "ol/format";
|
||||
import { Point } from "ol/geom";
|
||||
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
|
||||
import { fromLonLat } from "ol/proj";
|
||||
import { OSM, Vector as VectorSource } from "ol/source";
|
||||
import { Circle, Fill, Stroke, Style } from "ol/style";
|
||||
import { Plugin } from "../Plugin.ts";
|
||||
|
||||
/**
|
||||
* MapView
|
||||
*/
|
||||
export default class MapView extends Plugin {
|
||||
private readonly map: HTMLElement;
|
||||
|
||||
public constructor(map: HTMLElement) {
|
||||
super("mapView");
|
||||
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
protected async run(): Promise<void> {
|
||||
const { leafletTarget: target, mapLon = "0", mapLat = "0", mapGeojson } = this.map.dataset;
|
||||
|
||||
const lon = Number.parseFloat(mapLon);
|
||||
const lat = Number.parseFloat(mapLat);
|
||||
const view = new View({ maxZoom: 16, enableRotation: false });
|
||||
const map = new OlMap({
|
||||
target: target,
|
||||
layers: [new TileLayer({ source: new OSM({ maxZoom: 16 }) })],
|
||||
view: view
|
||||
});
|
||||
|
||||
try {
|
||||
const markerSource = new VectorSource({
|
||||
features: [
|
||||
new Feature({
|
||||
geometry: new Point(fromLonLat([lon, lat]))
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
const markerLayer = new VectorLayer({
|
||||
source: markerSource,
|
||||
style: new Style({
|
||||
image: new Circle({
|
||||
radius: 6,
|
||||
fill: new Fill({ color: "#3050ff" })
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
map.addLayer(markerLayer);
|
||||
} catch (error) {
|
||||
console.error("Failed to create marker layer:", error);
|
||||
}
|
||||
|
||||
if (mapGeojson) {
|
||||
try {
|
||||
const geoSource = new VectorSource({
|
||||
features: new GeoJSON().readFeatures(JSON.parse(mapGeojson), {
|
||||
dataProjection: "EPSG:4326",
|
||||
featureProjection: "EPSG:3857"
|
||||
})
|
||||
});
|
||||
|
||||
const geoLayer = new VectorLayer({
|
||||
source: geoSource,
|
||||
style: new Style({
|
||||
stroke: new Stroke({ color: "#3050ff", width: 2 }),
|
||||
fill: new Fill({ color: "#3050ff33" })
|
||||
})
|
||||
});
|
||||
|
||||
map.addLayer(geoLayer);
|
||||
|
||||
const geoSourceExtent = geoSource.getExtent();
|
||||
if (geoSourceExtent) {
|
||||
view.fit(geoSourceExtent, { padding: [20, 20, 20, 20] });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create GeoJSON layer:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async post(): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { load } from "./loader.ts";
|
||||
import { Endpoints, endpoint, listen, ready, settings } from "./toolkit.ts";
|
||||
|
||||
ready(() => {
|
||||
document.documentElement.classList.remove("no-js");
|
||||
document.documentElement.classList.add("js");
|
||||
|
||||
listen("click", ".close", function (this: HTMLElement) {
|
||||
(this.parentNode as HTMLElement)?.classList.add("invisible");
|
||||
});
|
||||
|
||||
listen("click", ".searxng_init_map", async function (this: HTMLElement, event: Event) {
|
||||
event.preventDefault();
|
||||
this.classList.remove("searxng_init_map");
|
||||
|
||||
load(() => import("./plugin/MapView.ts").then(({ default: Plugin }) => new Plugin(this)), {
|
||||
on: "endpoint",
|
||||
where: [Endpoints.results]
|
||||
});
|
||||
});
|
||||
|
||||
if (settings.plugins?.includes("infiniteScroll")) {
|
||||
load(() => import("./plugin/InfiniteScroll.ts").then(({ default: Plugin }) => new Plugin()), {
|
||||
on: "endpoint",
|
||||
where: [Endpoints.results]
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.plugins?.includes("calculator")) {
|
||||
load(() => import("./plugin/Calculator.ts").then(({ default: Plugin }) => new Plugin()), {
|
||||
on: "endpoint",
|
||||
where: [Endpoints.results]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ready(
|
||||
() => {
|
||||
void import("./main/keyboard.ts");
|
||||
void import("./main/search.ts");
|
||||
|
||||
if (settings.autocomplete) {
|
||||
void import("./main/autocomplete.ts");
|
||||
}
|
||||
},
|
||||
{ on: [endpoint === Endpoints.index] }
|
||||
);
|
||||
|
||||
ready(
|
||||
() => {
|
||||
void import("./main/keyboard.ts");
|
||||
void import("./main/results.ts");
|
||||
void import("./main/search.ts");
|
||||
|
||||
if (settings.autocomplete) {
|
||||
void import("./main/autocomplete.ts");
|
||||
}
|
||||
},
|
||||
{ on: [endpoint === Endpoints.results] }
|
||||
);
|
||||
|
||||
ready(
|
||||
() => {
|
||||
void import("./main/preferences.ts");
|
||||
},
|
||||
{ on: [endpoint === Endpoints.preferences] }
|
||||
);
|
||||
1
client/simple/src/js/searxng.head.js
Normal file
1
client/simple/src/js/searxng.head.js
Normal file
@@ -0,0 +1 @@
|
||||
import "./head/00_init.js";
|
||||
7
client/simple/src/js/searxng.js
Normal file
7
client/simple/src/js/searxng.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import "./main/00_toolkit.js";
|
||||
import "./main/infinite_scroll.js";
|
||||
import "./main/keyboard.js";
|
||||
import "./main/mapresult.js";
|
||||
import "./main/preferences.js";
|
||||
import "./main/results.js";
|
||||
import "./main/search.js";
|
||||
@@ -1,133 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type { KeyBindingLayout } from "./main/keyboard.ts";
|
||||
|
||||
// synced with searx/webapp.py get_client_settings
|
||||
type Settings = {
|
||||
plugins?: string[];
|
||||
autocomplete?: string;
|
||||
autocomplete_min?: number;
|
||||
doi_resolver?: string;
|
||||
favicon_resolver?: string;
|
||||
hotkeys?: KeyBindingLayout;
|
||||
method?: "GET" | "POST";
|
||||
query_in_title?: boolean;
|
||||
results_on_new_tab?: boolean;
|
||||
safesearch?: 0 | 1 | 2;
|
||||
search_on_category_select?: boolean;
|
||||
theme?: string;
|
||||
theme_static_path?: string;
|
||||
translations?: Record<string, string>;
|
||||
url_formatting?: "pretty" | "full" | "host";
|
||||
};
|
||||
|
||||
type HTTPOptions = {
|
||||
body?: BodyInit;
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
type ReadyOptions = {
|
||||
// all values must be truthy for the callback to be executed
|
||||
on?: (boolean | undefined)[];
|
||||
};
|
||||
|
||||
export type EndpointsKeys = keyof typeof Endpoints;
|
||||
|
||||
export const Endpoints = {
|
||||
index: "index",
|
||||
results: "results",
|
||||
preferences: "preferences",
|
||||
unknown: "unknown"
|
||||
} as const;
|
||||
|
||||
export const mutable = {
|
||||
closeDetail: undefined as (() => void) | undefined,
|
||||
scrollPageToSelected: undefined as (() => void) | undefined,
|
||||
selectImage: undefined as ((resultElement: HTMLElement) => void) | undefined,
|
||||
selectNext: undefined as ((openDetailView?: boolean) => void) | undefined,
|
||||
selectPrevious: undefined as ((openDetailView?: boolean) => void) | undefined
|
||||
};
|
||||
|
||||
const getEndpoint = (): EndpointsKeys => {
|
||||
const metaEndpoint = document.querySelector('meta[name="endpoint"]')?.getAttribute("content");
|
||||
|
||||
if (metaEndpoint && metaEndpoint in Endpoints) {
|
||||
return metaEndpoint as EndpointsKeys;
|
||||
}
|
||||
|
||||
return Endpoints.unknown;
|
||||
};
|
||||
|
||||
const getSettings = (): Settings => {
|
||||
const settings = document.querySelector("script[client_settings]")?.getAttribute("client_settings");
|
||||
if (!settings) return {};
|
||||
|
||||
try {
|
||||
return JSON.parse(atob(settings));
|
||||
} catch (error) {
|
||||
console.error("Failed to load client_settings:", error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const http = async (method: string, url: string | URL, options?: HTTPOptions): Promise<Response> => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), options?.timeout ?? 30_000);
|
||||
|
||||
const res = await fetch(url, {
|
||||
body: options?.body,
|
||||
method: method,
|
||||
signal: controller.signal
|
||||
}).finally(() => clearTimeout(timeoutId));
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const listen = <K extends keyof DocumentEventMap, E extends HTMLElement>(
|
||||
type: string | K,
|
||||
target: string | Document | E,
|
||||
listener: (this: E, event: DocumentEventMap[K]) => void | Promise<void>,
|
||||
options?: AddEventListenerOptions
|
||||
): void => {
|
||||
if (typeof target !== "string") {
|
||||
target.addEventListener(type, listener as EventListener, options);
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener(
|
||||
type,
|
||||
(event: Event) => {
|
||||
for (const node of event.composedPath()) {
|
||||
if (node instanceof HTMLElement && node.matches(target)) {
|
||||
try {
|
||||
listener.call(node as E, event as DocumentEventMap[K]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
export const ready = (callback: () => void, options?: ReadyOptions): void => {
|
||||
for (const condition of options?.on ?? []) {
|
||||
if (!condition) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
listen("DOMContentLoaded", document, callback, { once: true });
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
export const endpoint: EndpointsKeys = getEndpoint();
|
||||
export const settings: Settings = getSettings();
|
||||
@@ -1,34 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { getElement } from "./getElement.ts";
|
||||
|
||||
export const appendAnswerElement = (element: HTMLElement | string | number): void => {
|
||||
const results = getElement<HTMLDivElement>("results");
|
||||
|
||||
// ./searx/templates/elements/answers.html
|
||||
let answers = getElement<HTMLDivElement>("answers", { assert: false });
|
||||
if (!answers) {
|
||||
// what is this?
|
||||
const answersTitle = document.createElement("h4");
|
||||
answersTitle.setAttribute("class", "title");
|
||||
answersTitle.setAttribute("id", "answers-title");
|
||||
answersTitle.textContent = "Answers : ";
|
||||
|
||||
answers = document.createElement("div");
|
||||
answers.setAttribute("id", "answers");
|
||||
answers.setAttribute("role", "complementary");
|
||||
answers.setAttribute("aria-labelledby", "answers-title");
|
||||
answers.appendChild(answersTitle);
|
||||
}
|
||||
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
const span = document.createElement("span");
|
||||
span.innerHTML = element.toString();
|
||||
// biome-ignore lint/style/noParameterAssign: TODO
|
||||
element = span;
|
||||
}
|
||||
|
||||
answers.appendChild(element);
|
||||
|
||||
results.insertAdjacentElement("afterbegin", answers);
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
type AssertElement = <T>(element?: T | null) => asserts element is T;
|
||||
export const assertElement: AssertElement = <T>(element?: T | null): asserts element is T => {
|
||||
if (!element) {
|
||||
throw new Error("DOM element not found");
|
||||
}
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { assertElement } from "./assertElement.ts";
|
||||
|
||||
type Options = {
|
||||
assert?: boolean;
|
||||
};
|
||||
|
||||
export function getElement<T>(id: string, options?: { assert: true }): T;
|
||||
export function getElement<T>(id: string, options?: { assert: false }): T | null;
|
||||
export function getElement<T>(id: string, options: Options = {}): T | null {
|
||||
options.assert ??= true;
|
||||
|
||||
const element = document.getElementById(id) as T | null;
|
||||
|
||||
if (options.assert) {
|
||||
assertElement(element);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.dialog-modal {
|
||||
animation-name: dialogmodal;
|
||||
animation-duration: 0.13s;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
/*! Autocomplete.js v2.6.3 | license MIT | (c) 2017, Baptiste Donaux | http://autocomplete-js.com */
|
||||
|
||||
.autocomplete {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
@import "../../../generated/pygments.less";
|
||||
@import "../../generated/pygments.less";
|
||||
|
||||
.codelines {
|
||||
margin: @results-margin 0 0 0;
|
||||
@@ -20,9 +18,12 @@
|
||||
cursor: default;
|
||||
|
||||
&::selection {
|
||||
background: transparent;
|
||||
background: transparent; /* WebKit/Blink Browsers */
|
||||
}
|
||||
|
||||
&::-moz-selection {
|
||||
background: transparent; /* Gecko Browsers */
|
||||
}
|
||||
margin-right: 8px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -30,12 +31,6 @@
|
||||
span.linenos {
|
||||
color: var(--color-line-number);
|
||||
}
|
||||
|
||||
.err {
|
||||
// The code view in the result list should not act as a code-checker.
|
||||
border: none;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
* SearXNG, A privacy-respecting, hackable metasearch engine
|
||||
*
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#main_results #results.image-detail-open.only_template_images {
|
||||
width: min(98%, 59.25rem) !important;
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
iframe[src^="https://w.soundcloud.com"] {
|
||||
iframe[src^="https://w.soundcloud.com"]
|
||||
{
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
iframe[src^="https://www.deezer.com"] {
|
||||
iframe[src^="https://www.deezer.com"]
|
||||
{
|
||||
// The real size is 92px, but 94px are needed to avoid an inner scrollbar of
|
||||
// the embedded HTML.
|
||||
height: 94px;
|
||||
}
|
||||
|
||||
iframe[src^="https://www.mixcloud.com"] {
|
||||
iframe[src^="https://www.mixcloud.com"]
|
||||
{
|
||||
// the embedded player from mixcloud has some quirks: initial there is an
|
||||
// issue with an image URL that is blocked since it is an a Cross-Origin
|
||||
// request. The alternative text (<img alt='Mixcloud Logo'> then cause an
|
||||
@@ -20,16 +21,19 @@ iframe[src^="https://www.mixcloud.com"] {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
iframe[src^="https://bandcamp.com/EmbeddedPlayer"] {
|
||||
iframe[src^="https://bandcamp.com/EmbeddedPlayer"]
|
||||
{
|
||||
// show playlist
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
iframe[src^="https://bandcamp.com/EmbeddedPlayer/track"] {
|
||||
iframe[src^="https://bandcamp.com/EmbeddedPlayer/track"]
|
||||
{
|
||||
// hide playlist
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
iframe[src^="https://genius.com/songs"] {
|
||||
iframe[src^="https://genius.com/songs"]
|
||||
{
|
||||
height: 65px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#main_index {
|
||||
margin-top: 26vh;
|
||||
}
|
||||
@@ -8,7 +6,7 @@
|
||||
text-align: center;
|
||||
|
||||
.title {
|
||||
background: url("./img/searxng.png") no-repeat;
|
||||
background: url("../img/searxng.png") no-repeat;
|
||||
min-height: 4rem;
|
||||
margin: 4rem auto;
|
||||
background-position: center;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.info-page {
|
||||
code {
|
||||
font-family: monospace;
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
// Mixins
|
||||
.text-size-adjust (@property: 100%) {
|
||||
-webkit-text-size-adjust: @property;
|
||||
-ms-text-size-adjust: @property;
|
||||
-moz-text-size-adjust: @property;
|
||||
text-size-adjust: @property;
|
||||
}
|
||||
|
||||
@@ -19,6 +22,7 @@
|
||||
|
||||
// disable user selection
|
||||
.disable-user-select () {
|
||||
-webkit-touch-callout: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
37
client/simple/src/less/new_issue.less
Normal file
37
client/simple/src/less/new_issue.less
Normal file
@@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
.stats_endpoint {
|
||||
.github-issue-button {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.issue-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="checked"] {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 1rem 1rem 1rem 0;
|
||||
}
|
||||
|
||||
.step_content {
|
||||
margin: 1rem 1rem 1rem 2rem;
|
||||
}
|
||||
|
||||
.step1,
|
||||
.step2 {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.step1_delay {
|
||||
transition: visibility 0s linear 4s;
|
||||
}
|
||||
|
||||
#step1:checked ~ .step1,
|
||||
#step2:checked ~ .step2 {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
Layout of the Files result class
|
||||
*/
|
||||
|
||||
#main_results .result-file {
|
||||
border: 1px solid var(--color-result-border);
|
||||
margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important;
|
||||
.rounded-corners;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
|
||||
audio {
|
||||
width: 100%;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
Layout of the KeyValue result class
|
||||
*/
|
||||
|
||||
#main_results .result-keyvalue {
|
||||
caption {
|
||||
padding: 0.8rem 0.5rem;
|
||||
@@ -17,7 +14,7 @@
|
||||
}
|
||||
|
||||
table {
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
background-color: var(--color-result-keyvalue-table);
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
Layout of the Paper result class
|
||||
*/
|
||||
|
||||
.result-paper {
|
||||
.attributes {
|
||||
display: table;
|
||||
border-spacing: 0.125rem;
|
||||
|
||||
div {
|
||||
display: table-row;
|
||||
|
||||
span {
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.25rem;
|
||||
display: table-cell;
|
||||
|
||||
time {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
span:first-child {
|
||||
color: var(--color-base-font);
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
color: var(--color-result-publishdate-font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.comments {
|
||||
font-size: 0.9rem;
|
||||
margin: 0.25rem 0 0 0;
|
||||
padding: 0;
|
||||
overflow-wrap: break-word;
|
||||
line-height: 1.24;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @phone) {
|
||||
.result-paper {
|
||||
.attributes {
|
||||
display: block;
|
||||
|
||||
div {
|
||||
display: block;
|
||||
|
||||
span {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
span:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
.ltr-margin-left(0.5rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
@import (inline) "../../node_modules/normalize.css/normalize.css";
|
||||
@import "definitions.less";
|
||||
@import "mixins.less";
|
||||
|
||||
.text-size-adjust (@property: 100%) {
|
||||
-webkit-text-size-adjust: @property;
|
||||
-ms-text-size-adjust: @property;
|
||||
-moz-text-size-adjust: @property;
|
||||
text-size-adjust: @property;
|
||||
}
|
||||
|
||||
// Reset padding and margin
|
||||
html,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
/*
|
||||
* SearXNG, A privacy-respecting, hackable metasearch engine
|
||||
*/
|
||||
|
||||
#search {
|
||||
padding: 0;
|
||||
@@ -178,6 +180,7 @@ html.no-js #clear_search.hide_if_nojs {
|
||||
#send_search {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0.8rem;
|
||||
background: none repeat scroll 0 0 var(--color-search-background);
|
||||
border: none;
|
||||
outline: none;
|
||||
@@ -193,9 +196,13 @@ html.no-js #clear_search.hide_if_nojs {
|
||||
.ltr-rounded-left-corners(0.8rem);
|
||||
}
|
||||
|
||||
#q::-ms-clear,
|
||||
#q::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#send_search {
|
||||
.ltr-rounded-right-corners(0.8rem);
|
||||
padding: 0.8rem;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
@@ -264,6 +271,7 @@ html.no-js #clear_search.hide_if_nojs {
|
||||
width: 100%;
|
||||
.ltr-text-align-left();
|
||||
overflow: scroll hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,6 +374,11 @@ html.no-js #clear_search.hide_if_nojs {
|
||||
|
||||
#categories {
|
||||
.disable-user-select;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#categories_container {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
--center-page-width overrides the less variable @results-width when the results are centered
|
||||
see the CSS rules for #results in style.less ( grid-template-columns and gap).
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.ltr-left(@offset) {
|
||||
left: @offset;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.ltr-left(@offset) {
|
||||
right: @offset;
|
||||
}
|
||||
@@ -131,7 +129,13 @@
|
||||
}
|
||||
|
||||
// select HTML element
|
||||
@supports ((background-position-x: 100%) and ((appearance: none))) {
|
||||
@supports (
|
||||
(background-position-x: 100%) and
|
||||
(
|
||||
(appearance: none) or (-webkit-appearance: none) or
|
||||
(-moz-appearance: none)
|
||||
)
|
||||
) {
|
||||
select {
|
||||
border-width: 0 0 0 2rem;
|
||||
background-position-x: -2rem;
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
/*
|
||||
* SearXNG, A privacy-respecting, hackable metasearch engine
|
||||
*
|
||||
* To convert "style.less" to "style.css" run: $make styles
|
||||
*/
|
||||
|
||||
// stylelint-disable no-descending-specificity
|
||||
|
||||
@import (inline) "../../node_modules/normalize.css/normalize.css";
|
||||
@import "definitions.less";
|
||||
@import "mixins.less";
|
||||
@import "code.less";
|
||||
@import "toolkit.less";
|
||||
@import "autocomplete.less";
|
||||
@import "detail.less";
|
||||
@import "animations.less";
|
||||
@import "embedded.less";
|
||||
@import "info.less";
|
||||
@import "new_issue.less";
|
||||
@import "stats.less";
|
||||
@import "result_templates.less";
|
||||
@import "weather.less";
|
||||
@@ -161,22 +168,12 @@ article[data-vim-selected].category-videos,
|
||||
article[data-vim-selected].category-news,
|
||||
article[data-vim-selected].category-map,
|
||||
article[data-vim-selected].category-music,
|
||||
article[data-vim-selected].category-files,
|
||||
article[data-vim-selected].category-social {
|
||||
border: 1px solid var(--color-result-vim-arrow);
|
||||
.rounded-corners;
|
||||
}
|
||||
|
||||
.image-label-bottom-right() {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--color-image-resolution-background);
|
||||
padding: 0.3rem 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-image-resolution-font);
|
||||
border-top-left-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin: @results-margin 0;
|
||||
padding: @result-padding;
|
||||
@@ -186,7 +183,7 @@ article[data-vim-selected].category-social {
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
margin: 0.4rem 0 0.4rem 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -220,7 +217,7 @@ article[data-vim-selected].category-social {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: 54em;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
line-height: 1.24;
|
||||
|
||||
.highlight {
|
||||
@@ -303,22 +300,12 @@ article[data-vim-selected].category-social {
|
||||
color: var(--color-result-description-highlight-font);
|
||||
}
|
||||
|
||||
a.thumbnail_link {
|
||||
position: relative;
|
||||
margin-top: 0.6rem;
|
||||
.ltr-margin-right(1rem);
|
||||
img.thumbnail {
|
||||
.ltr-float-left();
|
||||
|
||||
img.thumbnail {
|
||||
width: 7rem;
|
||||
height: unset; // remove height value that was needed for lazy loading
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumbnail_length {
|
||||
.image-label-bottom-right();
|
||||
right: 6px;
|
||||
}
|
||||
padding-top: 0.6rem;
|
||||
.ltr-padding-right(1rem);
|
||||
width: 7rem;
|
||||
height: unset; // remove height value that was needed for lazy loading
|
||||
}
|
||||
|
||||
.break {
|
||||
@@ -326,11 +313,11 @@ article[data-vim-selected].category-social {
|
||||
}
|
||||
}
|
||||
|
||||
.result-paper,
|
||||
.result-packages {
|
||||
.attributes {
|
||||
display: table;
|
||||
border-spacing: 0.125rem;
|
||||
margin-top: 0.3rem;
|
||||
|
||||
div {
|
||||
display: table-row;
|
||||
@@ -364,12 +351,18 @@ article[data-vim-selected].category-social {
|
||||
font-size: 0.9rem;
|
||||
margin: 0.25rem 0 0 0;
|
||||
padding: 0;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
line-height: 1.24;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.result-packages {
|
||||
.attributes {
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.template_group_images {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -384,6 +377,7 @@ article[data-vim-selected].category-social {
|
||||
.category-news,
|
||||
.category-map,
|
||||
.category-music,
|
||||
.category-files,
|
||||
.category-social {
|
||||
border: 1px solid var(--color-result-border);
|
||||
margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important;
|
||||
@@ -408,19 +402,23 @@ article[data-vim-selected].category-social {
|
||||
}
|
||||
|
||||
.result-videos {
|
||||
a.thumbnail_link img.thumbnail {
|
||||
img.thumbnail {
|
||||
.ltr-float-left();
|
||||
padding-top: 0.6rem;
|
||||
.ltr-padding-right(1rem);
|
||||
width: 20rem;
|
||||
height: unset; // remove height value that was needed for lazy loading
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: hidden;
|
||||
}
|
||||
.result-videos .content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.embedded-video iframe {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
.result-videos .embedded-video iframe {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
|
||||
@supports not (aspect-ratio: 1 / 1) {
|
||||
@@ -485,7 +483,14 @@ article[data-vim-selected].category-social {
|
||||
}
|
||||
|
||||
.image_resolution {
|
||||
.image-label-bottom-right();
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--color-image-resolution-background);
|
||||
padding: 0.3rem 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-image-resolution-font);
|
||||
border-top-left-radius: 0.3rem;
|
||||
}
|
||||
|
||||
span.title,
|
||||
@@ -569,16 +574,13 @@ article[data-vim-selected].category-social {
|
||||
|
||||
#suggestions {
|
||||
.wrapper {
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
list-style-position: inside;
|
||||
|
||||
li::marker {
|
||||
color: var(--color-result-link-font);
|
||||
}
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
form {
|
||||
display: inline-block;
|
||||
flex: 1 1 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -586,8 +588,8 @@ article[data-vim-selected].category-social {
|
||||
#suggestions,
|
||||
#infoboxes {
|
||||
input {
|
||||
padding: 3px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin: 3px;
|
||||
font-size: 0.9em;
|
||||
display: inline-block;
|
||||
background: transparent;
|
||||
@@ -689,7 +691,7 @@ summary.title {
|
||||
|
||||
#sidebar {
|
||||
grid-area: sidebar;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
color: var(--color-sidebar-font);
|
||||
|
||||
.infobox {
|
||||
@@ -1030,6 +1032,10 @@ summary.title {
|
||||
/ 100%;
|
||||
gap: 0;
|
||||
|
||||
#sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#urls {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
@@ -1116,6 +1122,7 @@ summary.title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.result-paper,
|
||||
.result-packages {
|
||||
.attributes {
|
||||
display: block;
|
||||
@@ -1161,7 +1168,3 @@ pre code {
|
||||
|
||||
// import layouts of the Result types
|
||||
@import "result_types/keyvalue.less";
|
||||
@import "result_types/code.less";
|
||||
@import "result_types/paper.less";
|
||||
@import "result_types/file.less";
|
||||
@import "result_types/image.less";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
// other solution : http://stackoverflow.com/questions/1577598/how-to-hide-parts-of-html-when-javascript-is-disabled/13857783#13857783
|
||||
|
||||
// stylelint-disable no-descending-specificity
|
||||
@@ -158,7 +156,9 @@ div.selectable_url {
|
||||
|
||||
td {
|
||||
padding: 0 1em 0 0;
|
||||
padding-top: 0;
|
||||
.ltr-padding-right(1rem);
|
||||
padding-bottom: 0;
|
||||
.ltr-padding-left(0);
|
||||
}
|
||||
|
||||
@@ -193,15 +193,6 @@ div.selectable_url {
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.dialog-warning-block {
|
||||
.dialog();
|
||||
|
||||
display: block;
|
||||
color: var(--color-warning);
|
||||
background: var(--color-warning-background);
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.dialog-modal {
|
||||
.dialog();
|
||||
|
||||
@@ -316,7 +307,7 @@ html body .tabs > input:checked {
|
||||
}
|
||||
|
||||
~ label {
|
||||
position: inherit;
|
||||
position: inherited;
|
||||
background: inherit;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-weight: normal;
|
||||
@@ -356,9 +347,17 @@ select {
|
||||
}
|
||||
}
|
||||
|
||||
@supports ((background-position-x: 100%) and ((appearance: none))) {
|
||||
@supports (
|
||||
(background-position-x: 100%) and
|
||||
(
|
||||
(appearance: none) or (-webkit-appearance: none) or
|
||||
(-moz-appearance: none)
|
||||
)
|
||||
) {
|
||||
select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
border-width: 0 2rem 0 0;
|
||||
border-color: transparent;
|
||||
background: data-uri("image/svg+xml;charset=UTF-8", @select-light-svg-path)
|
||||
@@ -401,6 +400,8 @@ select {
|
||||
|
||||
/* -- checkbox-onoff -- */
|
||||
input.checkbox-onoff[type="checkbox"] {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@@ -474,6 +475,8 @@ input.checkbox-onoff.reversed-checkbox[type="checkbox"] {
|
||||
/* -- checkbox -- */
|
||||
@supports (transform: rotate(-45deg)) {
|
||||
input[type="checkbox"]:not(.checkbox-onoff) {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
width: 20px;
|
||||
@@ -546,16 +549,33 @@ input.checkbox-onoff.reversed-checkbox[type="checkbox"] {
|
||||
border-right: 0.5em solid var(--color-toolkit-loader-border);
|
||||
border-bottom: 0.5em solid var(--color-toolkit-loader-border);
|
||||
border-left: 0.5em solid var(--color-toolkit-loader-borderleft);
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-animation: load8 1.2s infinite linear;
|
||||
animation: load8 1.2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes load8 {
|
||||
@-webkit-keyframes load8 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes load8 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -586,6 +606,9 @@ td:hover .engine-tooltip,
|
||||
margin: 0;
|
||||
padding: 0 0.125rem 0 4rem;
|
||||
width: 100%;
|
||||
width: -moz-available;
|
||||
width: -webkit-fill-available;
|
||||
width: fill;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
@@ -610,7 +633,7 @@ td:hover .engine-tooltip,
|
||||
.stacked-bar-chart-base();
|
||||
|
||||
background: var(--color-base-font);
|
||||
border: 1px solid rgb(var(--color-base-font-rgb), 0.9);
|
||||
border: 1px solid rgba(var(--color-base-font-rgb), 0.9);
|
||||
padding: 0.3rem 0;
|
||||
}
|
||||
|
||||
@@ -618,7 +641,7 @@ td:hover .engine-tooltip,
|
||||
.stacked-bar-chart-base();
|
||||
|
||||
background: transparent;
|
||||
border: 1px solid rgb(var(--color-base-font-rgb), 0.3);
|
||||
border: 1px solid rgba(var(--color-base-font-rgb), 0.3);
|
||||
padding: 0.3rem 0;
|
||||
}
|
||||
|
||||
@@ -626,7 +649,7 @@ td:hover .engine-tooltip,
|
||||
.stacked-bar-chart-base();
|
||||
|
||||
background: transparent;
|
||||
border-bottom: 1px dotted rgb(var(--color-base-font-rgb), 0.5);
|
||||
border-bottom: 1px dotted rgba(var(--color-base-font-rgb), 0.5);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -634,7 +657,7 @@ td:hover .engine-tooltip,
|
||||
.stacked-bar-chart-base();
|
||||
|
||||
background: transparent;
|
||||
border-left: 1px solid rgb(var(--color-base-font-rgb), 0.9);
|
||||
border-left: 1px solid rgba(var(--color-base-font-rgb), 0.9);
|
||||
padding: 0.4rem 0;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.loader,
|
||||
.loader::after {
|
||||
border-radius: 50%;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#answers .weather {
|
||||
summary {
|
||||
display: block;
|
||||
|
||||
2
client/simple/static/.gitattributes
vendored
Normal file
2
client/simple/static/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
leaflet.css -diff
|
||||
leaflet.js -diff
|
||||
@@ -1,38 +1,43 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* Generate icons.html for the jinja templates of the simple theme.
|
||||
*/
|
||||
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { argv } from "node:process";
|
||||
import type { Config as SvgoConfig } from "svgo";
|
||||
import { type IconSet, type JinjaMacro, jinja_svg_sets } from "./tools/jinja_svg_catalog.ts";
|
||||
import { jinja_svg_sets } from "./tools/jinja_svg_catalog.js";
|
||||
|
||||
const HERE = `${dirname(argv[1] || "")}/`;
|
||||
const dest: string = resolve(HERE, "../../searx/templates/simple/icons.html");
|
||||
const HERE = `${dirname(argv[1])}/`;
|
||||
const dest = resolve(HERE, "../../searx/templates/simple/icons.html");
|
||||
|
||||
const searxng_jinja_macros: JinjaMacro[] = [
|
||||
/** @type import("./tools/jinja_svg_catalog.js").JinjaMacro[] */
|
||||
const searxng_jinja_macros = [
|
||||
{ name: "icon", class: "sxng-icon-set" },
|
||||
{ name: "icon_small", class: "sxng-icon-set-small" },
|
||||
{ name: "icon_big", class: "sxng-icon-set-big" }
|
||||
];
|
||||
|
||||
const sxng_icon_opts: SvgoConfig = {
|
||||
const sxng_icon_opts = {
|
||||
multipass: true,
|
||||
plugins: [
|
||||
"removeTitle",
|
||||
"removeXMLNS",
|
||||
{ name: "removeTitle" },
|
||||
{ name: "removeXMLNS" },
|
||||
{
|
||||
name: "addAttributesToSVGElement",
|
||||
params: {
|
||||
attributes: [{ "aria-hidden": "true" }]
|
||||
attributes: [
|
||||
{
|
||||
"aria-hidden": "true"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const simple_icons: IconSet[] = [
|
||||
/**
|
||||
* @type import("./tools/jinja_svg_catalog.js").IconSet[]
|
||||
*/
|
||||
const simple_icons = [
|
||||
{
|
||||
base: resolve(HERE, "node_modules/ionicons/dist/svg"),
|
||||
set: {
|
||||
64
client/simple/tools/img.js
Normal file
64
client/simple/tools/img.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import sharp from "sharp";
|
||||
import { optimize as svgo } from "svgo";
|
||||
|
||||
/**
|
||||
* @typedef {object} Src2Dest - Mapping of src to dest
|
||||
* @property {string} src - Name of the source file.
|
||||
* @property {string} dest - Name of the destination file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert a list of SVG files to PNG.
|
||||
*
|
||||
* @param {Src2Dest[]} items - Array of SVG files (src: SVG, dest:PNG) to convert.
|
||||
*/
|
||||
async function svg2png(items) {
|
||||
for (const item of items) {
|
||||
try {
|
||||
fs.mkdir(path.dirname(item.dest), { recursive: true }, (err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
|
||||
const info = await sharp(item.src)
|
||||
.png({
|
||||
force: true,
|
||||
compressionLevel: 9,
|
||||
palette: true
|
||||
})
|
||||
.toFile(item.dest);
|
||||
|
||||
console.log(`[svg2png] created ${item.dest} -- bytes: ${info.size}, w:${info.width}px, h:${info.height}px`);
|
||||
} catch (err) {
|
||||
console.error(`ERROR: ${item.dest} -- ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize SVG images for WEB.
|
||||
*
|
||||
* @param {Src2Dest[]} items - Array of SVG files (src:SVG, dest:SVG) to optimize.
|
||||
* @param {import('svgo').Config} svgo_opts - Options passed to svgo.
|
||||
*/
|
||||
async function svg2svg(items, svgo_opts) {
|
||||
for (const item of items) {
|
||||
try {
|
||||
fs.mkdir(path.dirname(item.dest), { recursive: true }, (err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
|
||||
const raw = fs.readFileSync(item.src, "utf8");
|
||||
const opt = svgo(raw, svgo_opts);
|
||||
fs.writeFileSync(item.dest, opt.data);
|
||||
console.log(`[svg2svg] optimized: ${item.dest} -- src: ${item.src}`);
|
||||
} catch (err) {
|
||||
console.error(`ERROR: optimize src: ${item.src} -- ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { svg2png, svg2svg };
|
||||
@@ -1,69 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import sharp from "sharp";
|
||||
import type { Config } from "svgo";
|
||||
import { optimize as svgo } from "svgo";
|
||||
|
||||
// Mapping of src to dest
|
||||
export type Src2Dest = {
|
||||
// Name of the source file.
|
||||
src: string;
|
||||
// Name of the destination file.
|
||||
dest: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a list of SVG files to PNG.
|
||||
*
|
||||
* @param items - Array of SVG files (src: SVG, dest:PNG) to convert.
|
||||
* @param width - (optional) width of the PNG pictures
|
||||
* @param height - (optional) height of the PNG pictures.
|
||||
*/
|
||||
export const svg2png = (items: Src2Dest[], width?: number, height?: number): void => {
|
||||
for (const item of items) {
|
||||
fs.mkdirSync(path.dirname(item.dest), { recursive: true });
|
||||
|
||||
sharp(item.src)
|
||||
.png({
|
||||
force: true,
|
||||
compressionLevel: 9,
|
||||
palette: true
|
||||
})
|
||||
.resize(width, height, {
|
||||
fit: "contain"
|
||||
})
|
||||
.toFile(item.dest)
|
||||
.then((info) => {
|
||||
console.log(`[svg2png] created ${item.dest} -- bytes: ${info.size}, w:${info.width}px, h:${info.height}px`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`ERROR: ${item.dest} -- ${error}`);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Optimize SVG images for WEB.
|
||||
*
|
||||
* @param items - Array of SVG files (src:SVG, dest:SVG) to optimize.
|
||||
* @param svgo_opts - Options passed to svgo.
|
||||
*/
|
||||
export const svg2svg = (items: Src2Dest[], svgo_opts: Config): void => {
|
||||
for (const item of items) {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(item.dest), { recursive: true });
|
||||
|
||||
const raw = fs.readFileSync(item.src, "utf8");
|
||||
const opt = svgo(raw, svgo_opts);
|
||||
|
||||
fs.writeFileSync(item.dest, opt.data);
|
||||
console.log(`[svg2svg] optimized: ${item.dest} -- src: ${item.src}`);
|
||||
} catch (error) {
|
||||
console.error(`ERROR: optimize src: ${item.src} -- ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,4 @@
|
||||
{{--
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
This is a EDGE https://edgejs.dev/ template to generate a HTML Jinja template
|
||||
for the backend. Example output of this EDGE template:
|
||||
- https://github.com/searxng/searxng/blob/master/searx/templates/simple/icons.html
|
||||
|
||||
119
client/simple/tools/jinja_svg_catalog.js
Normal file
119
client/simple/tools/jinja_svg_catalog.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import fs from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Edge } from "edge.js";
|
||||
import { optimize as svgo } from "svgo";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const __jinja_class_placeholder__ = "__jinja_class_placeholder__";
|
||||
|
||||
// -- types
|
||||
|
||||
/**
|
||||
* @typedef {object} IconSet - A set of icons
|
||||
* @property {object} set - Object of SVG icons, where property name is the
|
||||
* name of the icon and value is the src of the SVG (relative to base).
|
||||
* @property {string} base - Folder in which the SVG src files are located.
|
||||
* @property {import("svgo").Config} svgo_opts - svgo options for this set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} IconSVG - Mapping of icon name to SVG source file.
|
||||
* @property {string} name - Name of the icon isource file.
|
||||
* @property {string} src - Name of the destination file.
|
||||
* @property {import("svgo").Config} svgo_opts - Options passed to svgo.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} JinjaMacro - Arguments to create a jinja macro
|
||||
* @property {string} name - Name of the jinja macro.
|
||||
* @property {string} class - SVG's class name (value of XML class attribute)
|
||||
*/
|
||||
|
||||
// -- functions
|
||||
|
||||
/**
|
||||
* Generate a jinja template with a catalog of SVG icons that can be
|
||||
* used in in other HTML jinja templates.
|
||||
*
|
||||
* @param {string} dest - filename of the generate jinja template.
|
||||
* @param {JinjaMacro} macros - Jinja macros to create.
|
||||
* @param {IconSVG[]} items - Array of SVG items.
|
||||
*/
|
||||
|
||||
function jinja_svg_catalog(dest, macros, items) {
|
||||
const svg_catalog = {};
|
||||
const edge_template = resolve(__dirname, "jinja_svg_catalog.html.edge");
|
||||
|
||||
items.forEach((item) => {
|
||||
/** @type {import("svgo").Config} */
|
||||
// JSON.stringify & JSON.parse are used to create a deep copy of the
|
||||
// item.svgo_opts object
|
||||
const svgo_opts = JSON.parse(JSON.stringify(item.svgo_opts));
|
||||
svgo_opts.plugins.push({
|
||||
name: "addClassesToSVGElement",
|
||||
params: {
|
||||
classNames: [__jinja_class_placeholder__]
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(item.src, "utf8");
|
||||
const opt = svgo(raw, svgo_opts);
|
||||
svg_catalog[item.name] = opt.data;
|
||||
} catch (err) {
|
||||
console.error(`ERROR: jinja_svg_catalog processing ${item.name} src: ${item.src} -- ${err}`);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
fs.mkdir(dirname(dest), { recursive: true }, (err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
|
||||
const ctx = {
|
||||
svg_catalog: svg_catalog,
|
||||
macros: macros,
|
||||
edge_template: edge_template,
|
||||
__jinja_class_placeholder__: __jinja_class_placeholder__,
|
||||
// see https://github.com/edge-js/edge/issues/162
|
||||
open_curly_brace: "{{",
|
||||
close_curly_brace: "}}"
|
||||
};
|
||||
|
||||
const jinjatmpl = Edge.create().renderRawSync(fs.readFileSync(edge_template, "utf-8"), ctx);
|
||||
|
||||
fs.writeFileSync(dest, jinjatmpl);
|
||||
console.log(`[jinja_svg_catalog] created: ${dest}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls jinja_svg_catalog for a collection of icon sets where each set has its
|
||||
* own parameters.
|
||||
*
|
||||
* @param {string} dest - filename of the generate jinja template.
|
||||
* @param {JinjaMacro} macros - Jinja macros to create.
|
||||
* @param {IconSet[]} sets - Array of SVG sets.
|
||||
*/
|
||||
function jinja_svg_sets(dest, macros, sets) {
|
||||
/** @type IconSVG[] */
|
||||
const items = [];
|
||||
const all = [];
|
||||
for (const obj of sets) {
|
||||
for (const [name, file] of Object.entries(obj.set)) {
|
||||
if (all.includes(name)) {
|
||||
throw new Error(`ERROR: ${name} has already been defined`);
|
||||
}
|
||||
items.push({
|
||||
name: name,
|
||||
src: resolve(obj.base, file),
|
||||
svgo_opts: obj.svgo_opts
|
||||
});
|
||||
}
|
||||
jinja_svg_catalog(dest, macros, items);
|
||||
}
|
||||
}
|
||||
|
||||
// -- exports
|
||||
|
||||
export { jinja_svg_sets, jinja_svg_catalog };
|
||||
@@ -1,118 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import fs from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { Edge } from "edge.js";
|
||||
import { type Config as SvgoConfig, optimize as svgo } from "svgo";
|
||||
|
||||
const __jinja_class_placeholder__ = "__jinja_class_placeholder__";
|
||||
|
||||
// A set of icons
|
||||
export type IconSet = {
|
||||
// Object of SVG icons, where property name is the name of the icon and value is the src of the SVG (relative to base)
|
||||
set: Record<string, string>;
|
||||
// Folder in which the SVG src files are located
|
||||
base: string;
|
||||
// svgo options for this set
|
||||
svgo_opts: SvgoConfig;
|
||||
};
|
||||
|
||||
// Mapping of icon name to SVG source file
|
||||
type IconSVG = {
|
||||
// Name of the icon isource file
|
||||
name: string;
|
||||
// Name of the destination file
|
||||
src: string;
|
||||
// Options passed to svgo
|
||||
svgo_opts: SvgoConfig;
|
||||
};
|
||||
|
||||
// Arguments to create a jinja macro
|
||||
export type JinjaMacro = {
|
||||
// Name of the jinja macro
|
||||
name: string;
|
||||
// SVG's class name (value of XML class attribute)
|
||||
class: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a jinja template with a catalog of SVG icons that can be
|
||||
* used in other HTML jinja templates.
|
||||
*
|
||||
* @param dest - filename of the generate jinja template.
|
||||
* @param macros - Jinja macros to create.
|
||||
* @param items - Array of SVG items.
|
||||
*/
|
||||
export const jinja_svg_catalog = (dest: string, macros: JinjaMacro[], items: IconSVG[]): void => {
|
||||
const svg_catalog: Record<string, string> = {};
|
||||
const edge_template = resolve(import.meta.dirname, "jinja_svg_catalog.html.edge");
|
||||
|
||||
for (const item of items) {
|
||||
// JSON.stringify & JSON.parse are used to create a deep copy of the item.svgo_opts object
|
||||
const svgo_opts: SvgoConfig = JSON.parse(JSON.stringify(item.svgo_opts));
|
||||
|
||||
svgo_opts.plugins?.push({
|
||||
name: "addClassesToSVGElement",
|
||||
params: {
|
||||
classNames: [__jinja_class_placeholder__]
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(item.src, "utf8");
|
||||
const opt = svgo(raw, svgo_opts);
|
||||
|
||||
svg_catalog[item.name] = opt.data;
|
||||
} catch (error) {
|
||||
console.error(`ERROR: jinja_svg_catalog processing ${item.name} src: ${item.src} -- ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(dirname(dest), { recursive: true });
|
||||
|
||||
const ctx = {
|
||||
svg_catalog: svg_catalog,
|
||||
macros: macros,
|
||||
edge_template: edge_template,
|
||||
__jinja_class_placeholder__: __jinja_class_placeholder__,
|
||||
// see https://github.com/edge-js/edge/issues/162
|
||||
open_curly_brace: "{{",
|
||||
close_curly_brace: "}}"
|
||||
};
|
||||
|
||||
const jinjatmpl = Edge.create().renderRawSync(fs.readFileSync(edge_template, "utf-8"), ctx);
|
||||
|
||||
fs.writeFileSync(dest, jinjatmpl);
|
||||
console.log(`[jinja_svg_catalog] created: ${dest}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calls jinja_svg_catalog for a collection of icon sets where each set has its
|
||||
* own parameters.
|
||||
*
|
||||
* @param dest - filename of the generate jinja template.
|
||||
* @param macros - Jinja macros to create.
|
||||
* @param sets - Array of SVG sets.
|
||||
*/
|
||||
export const jinja_svg_sets = (dest: string, macros: JinjaMacro[], sets: IconSet[]): void => {
|
||||
const items: IconSVG[] = [];
|
||||
const all: string[] = [];
|
||||
|
||||
for (const obj of sets) {
|
||||
for (const [name, file] of Object.entries(obj.set)) {
|
||||
if (all.includes(name)) {
|
||||
throw new Error(`ERROR: ${name} has already been defined`);
|
||||
}
|
||||
|
||||
all.push(name);
|
||||
items.push({
|
||||
name: name,
|
||||
src: resolve(obj.base, file),
|
||||
svgo_opts: obj.svgo_opts
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
jinja_svg_catalog(dest, macros, items);
|
||||
};
|
||||
44
client/simple/tools/plg.js
Normal file
44
client/simple/tools/plg.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Custom vite plugins to build the web-client components of the simple theme.
|
||||
*
|
||||
* HINT:
|
||||
*
|
||||
* This is an inital implementation for the migration of the build process
|
||||
* from grunt to vite. For fully support (vite: build & serve) more work is
|
||||
* needed.
|
||||
*/
|
||||
|
||||
import { svg2png, svg2svg } from "./img.js";
|
||||
|
||||
/**
|
||||
* Vite plugin to convert a list of SVG files to PNG.
|
||||
*
|
||||
* @param {import('./img.js').Src2Dest[]} items - Array of SVG files (src: SVG, dest:PNG) to convert.
|
||||
*/
|
||||
function plg_svg2png(items) {
|
||||
return {
|
||||
name: "searxng-simple-svg2png",
|
||||
apply: "build", // or 'serve'
|
||||
async writeBundle() {
|
||||
await svg2png(items);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite plugin to optimize SVG images for WEB.
|
||||
*
|
||||
* @param {import('./img.js').Src2Dest[]} items - Array of SVG files (src:SVG, dest:SVG) to optimize.
|
||||
* @param {import('svgo').Config} svgo_opts - Options passed to svgo.
|
||||
*/
|
||||
function plg_svg2svg(items, svgo_opts) {
|
||||
return {
|
||||
name: "searxng-simple-svg2png",
|
||||
apply: "build", // or 'serve'
|
||||
async writeBundle() {
|
||||
await svg2svg(items, svgo_opts);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { plg_svg2png, plg_svg2svg };
|
||||
@@ -1,47 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* Custom vite plugins to build the web-client components of the simple theme.
|
||||
*
|
||||
* HINT:
|
||||
* This is an initial implementation for the migration of the build process
|
||||
* from grunt to vite. For fully support (vite: build & serve) more work is
|
||||
* needed.
|
||||
*/
|
||||
|
||||
import type { Config } from "svgo";
|
||||
import type { Plugin } from "vite";
|
||||
import { type Src2Dest, svg2png, svg2svg } from "./img.ts";
|
||||
|
||||
/**
|
||||
* Vite plugin to convert a list of SVG files to PNG.
|
||||
*
|
||||
* @param items - Array of SVG files (src: SVG, dest:PNG) to convert.
|
||||
* @param width - (optional) width of the PNG picture
|
||||
* @param height - (optional) height of the PNG picture
|
||||
*/
|
||||
export const plg_svg2png = (items: Src2Dest[], width?: number, height?: number): Plugin => {
|
||||
return {
|
||||
name: "searxng-simple-svg2png",
|
||||
apply: "build",
|
||||
writeBundle: () => {
|
||||
svg2png(items, width, height);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Vite plugin to optimize SVG images for WEB.
|
||||
*
|
||||
* @param items - Array of SVG files (src:SVG, dest:SVG) to optimize.
|
||||
* @param svgo_opts - Options passed to svgo.
|
||||
*/
|
||||
export const plg_svg2svg = (items: Src2Dest[], svgo_opts: Config): Plugin => {
|
||||
return {
|
||||
name: "searxng-simple-svg2svg",
|
||||
apply: "build",
|
||||
writeBundle: () => {
|
||||
svg2svg(items, svgo_opts);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"target": "ES2022",
|
||||
|
||||
"allowImportingTsExtensions": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"incremental": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
"strict": true,
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["./"],
|
||||
"exclude": ["./node_modules/"]
|
||||
}
|
||||
141
client/simple/vite.config.js
Normal file
141
client/simple/vite.config.js
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* CONFIG: https://vite.dev/config/
|
||||
*/
|
||||
|
||||
import { resolve } from "node:path";
|
||||
import { defineConfig } from "vite";
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
import { plg_svg2png, plg_svg2svg } from "./tools/plg.js";
|
||||
|
||||
const ROOT = "../.."; // root of the git reposetory
|
||||
|
||||
const PATH = {
|
||||
dist: resolve(ROOT, "searx/static/themes/simple"),
|
||||
// dist: resolve(ROOT, "client/simple/dist"),
|
||||
|
||||
src: "src",
|
||||
modules: "node_modules",
|
||||
brand: "src/brand",
|
||||
static: resolve(ROOT, "client/simple/static"),
|
||||
leaflet: resolve(ROOT, "client/simple/node_modules/leaflet/dist"),
|
||||
templates: resolve(ROOT, "searx/templates/simple")
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import('svgo').Config}
|
||||
*/
|
||||
const svg2svg_opts = {
|
||||
plugins: [{ name: "preset-default" }, "sortAttrs", "convertStyleToAttrs"]
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import('svgo').Config}
|
||||
*/
|
||||
const svg2svg_favicon_opts = {
|
||||
plugins: [{ name: "preset-default" }, "sortAttrs"]
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
root: PATH.src,
|
||||
mode: "production",
|
||||
// mode: "development",
|
||||
|
||||
// FIXME: missing CCS sourcemaps!!
|
||||
// see: https://github.com/vitejs/vite/discussions/13845#discussioncomment-11992084
|
||||
//
|
||||
// what I have tried so far (see config below):
|
||||
//
|
||||
// - build.sourcemap
|
||||
// - esbuild.sourcemap
|
||||
// - css.preprocessorOptions.less.sourceMap
|
||||
css: {
|
||||
devSourcemap: true
|
||||
}, // end: css
|
||||
|
||||
build: {
|
||||
target: ["chrome87", "edge88", "firefox78", "safari14"],
|
||||
manifest: "manifest.json",
|
||||
emptyOutDir: true,
|
||||
assetsDir: "",
|
||||
outDir: PATH.dist,
|
||||
|
||||
sourcemap: true,
|
||||
minify: "esbuild",
|
||||
cssMinify: "esbuild",
|
||||
|
||||
rollupOptions: {
|
||||
input: {
|
||||
// build CSS files
|
||||
"css/searxng.min.css": `${PATH.src}/less/style-ltr.less`,
|
||||
"css/searxng-rtl.min.css": `${PATH.src}/less/style-rtl.less`,
|
||||
"css/rss.min.css": `${PATH.src}/less/rss.less`,
|
||||
|
||||
// build JS files
|
||||
"js/searxng.head.min": `${PATH.src}/js/searxng.head.js`,
|
||||
"js/searxng.min": `${PATH.src}/js/searxng.js`
|
||||
},
|
||||
|
||||
// file naming conventions / pathnames are relative to outDir (PATH.dist)
|
||||
output: {
|
||||
entryFileNames: "[name].js",
|
||||
chunkFileNames: "[name].js",
|
||||
assetFileNames: "[name].[ext]"
|
||||
// Vite does not support "rollupOptions.output.sourcemap".
|
||||
// Please use "build.sourcemap" instead.
|
||||
// sourcemap: true,
|
||||
}
|
||||
}
|
||||
}, // end: build
|
||||
|
||||
plugins: [
|
||||
// Leaflet
|
||||
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{ src: `${PATH.leaflet}/leaflet.{js,js.map}`, dest: `${PATH.dist}/js` },
|
||||
{ src: `${PATH.leaflet}/images/*.png`, dest: `${PATH.dist}/css/images/` },
|
||||
{ src: `${PATH.leaflet}/*.{css,css.map}`, dest: `${PATH.dist}/css` },
|
||||
{ src: `${PATH.static}/**/*`, dest: PATH.dist }
|
||||
]
|
||||
}),
|
||||
|
||||
// -- svg images
|
||||
|
||||
plg_svg2svg(
|
||||
[
|
||||
{ src: `${PATH.src}/svg/empty_favicon.svg`, dest: `${PATH.dist}/img/empty_favicon.svg` },
|
||||
{ src: `${PATH.src}/svg/select-dark.svg`, dest: `${PATH.dist}/img/select-dark.svg` },
|
||||
{ src: `${PATH.src}/svg/select-light.svg`, dest: `${PATH.dist}/img/select-light.svg` }
|
||||
],
|
||||
svg2svg_opts
|
||||
),
|
||||
|
||||
// SearXNG brand (static)
|
||||
|
||||
plg_svg2png([
|
||||
{ src: `${PATH.brand}/searxng-wordmark.svg`, dest: `${PATH.dist}/img/favicon.png` },
|
||||
{ src: `${PATH.brand}/searxng.svg`, dest: `${PATH.dist}/img/searxng.png` }
|
||||
]),
|
||||
|
||||
// -- svg
|
||||
plg_svg2svg(
|
||||
[
|
||||
{ src: `${PATH.brand}/searxng.svg`, dest: `${PATH.dist}/img/searxng.svg` },
|
||||
{ src: `${PATH.brand}/img_load_error.svg`, dest: `${PATH.dist}/img/img_load_error.svg` }
|
||||
],
|
||||
svg2svg_opts
|
||||
),
|
||||
|
||||
// -- favicon
|
||||
plg_svg2svg(
|
||||
[{ src: `${PATH.brand}/searxng-wordmark.svg`, dest: `${PATH.dist}/img/favicon.svg` }],
|
||||
svg2svg_favicon_opts
|
||||
),
|
||||
|
||||
// -- simple templates
|
||||
plg_svg2svg(
|
||||
[{ src: `${PATH.brand}/searxng-wordmark.svg`, dest: `${PATH.templates}/searxng-wordmark.min.svg` }],
|
||||
svg2svg_opts
|
||||
)
|
||||
] // end: plugins
|
||||
});
|
||||
@@ -1,210 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* CONFIG: https://vite.dev/config/
|
||||
*/
|
||||
|
||||
import { resolve } from "node:path";
|
||||
import { constants as zlibConstants } from "node:zlib";
|
||||
import browserslistToEsbuild from "browserslist-to-esbuild";
|
||||
import { browserslistToTargets } from "lightningcss";
|
||||
import type { PreRenderedAsset } from "rolldown";
|
||||
import type { Config } from "svgo";
|
||||
import type { UserConfig } from "vite";
|
||||
import analyzer from "vite-bundle-analyzer";
|
||||
import manifest from "./package.json" with { type: "json" };
|
||||
import { plg_svg2png, plg_svg2svg } from "./tools/plg.ts";
|
||||
|
||||
const ROOT = "../../"; // root of the git repository
|
||||
|
||||
const PATH = {
|
||||
brand: "src/brand/",
|
||||
dist: resolve(ROOT, "searx/static/themes/simple/"),
|
||||
modules: "node_modules/",
|
||||
src: "src/",
|
||||
templates: resolve(ROOT, "searx/templates/simple/")
|
||||
} as const;
|
||||
|
||||
const svg2svg_opts: Config = {
|
||||
plugins: [{ name: "preset-default" }, "sortAttrs", "convertStyleToAttrs"]
|
||||
};
|
||||
|
||||
const svg2svg_favicon_opts: Config = {
|
||||
plugins: [{ name: "preset-default" }, "sortAttrs"]
|
||||
};
|
||||
|
||||
export default {
|
||||
base: "./",
|
||||
publicDir: "static/",
|
||||
|
||||
build: {
|
||||
target: browserslistToEsbuild(manifest.browserslist),
|
||||
assetsDir: "",
|
||||
outDir: PATH.dist,
|
||||
manifest: "manifest.json",
|
||||
emptyOutDir: true,
|
||||
sourcemap: true,
|
||||
rolldownOptions: {
|
||||
input: {
|
||||
// entrypoint
|
||||
core: `${PATH.src}/js/index.ts`,
|
||||
|
||||
// stylesheets
|
||||
ltr: `${PATH.src}/less/style-ltr.less`,
|
||||
rtl: `${PATH.src}/less/style-rtl.less`,
|
||||
rss: `${PATH.src}/less/rss.less`
|
||||
},
|
||||
|
||||
// file naming conventions / pathnames are relative to outDir (PATH.dist)
|
||||
output: {
|
||||
entryFileNames: "sxng-[name].min.js",
|
||||
chunkFileNames: "chunk/[hash].min.js",
|
||||
assetFileNames: ({ names }: PreRenderedAsset): string => {
|
||||
const [name] = names;
|
||||
|
||||
switch (name?.split(".").pop()) {
|
||||
case "css":
|
||||
return "sxng-[name].min[extname]";
|
||||
default:
|
||||
return "sxng-[name][extname]";
|
||||
}
|
||||
},
|
||||
sanitizeFileName: (name: string): string => {
|
||||
return name
|
||||
.normalize("NFD")
|
||||
.replace(/[^a-zA-Z0-9.-]/g, "_")
|
||||
.toLowerCase();
|
||||
},
|
||||
comments: {
|
||||
legal: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}, // end: build
|
||||
|
||||
plugins: [
|
||||
// -- bundle analyzer
|
||||
analyzer({
|
||||
enabled: process.env.VITE_BUNDLE_ANALYZE === "true",
|
||||
analyzerPort: "auto",
|
||||
summary: true,
|
||||
reportTitle: manifest.name,
|
||||
|
||||
// sidecars with max compression
|
||||
gzipOptions: {
|
||||
level: zlibConstants.Z_BEST_COMPRESSION
|
||||
},
|
||||
brotliOptions: {
|
||||
params: {
|
||||
[zlibConstants.BROTLI_PARAM_QUALITY]: zlibConstants.BROTLI_MAX_QUALITY
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
// -- svg images
|
||||
plg_svg2svg(
|
||||
[
|
||||
{
|
||||
src: `${PATH.src}/svg/empty_favicon.svg`,
|
||||
dest: `${PATH.dist}/img/empty_favicon.svg`
|
||||
},
|
||||
{
|
||||
src: `${PATH.src}/svg/select-dark.svg`,
|
||||
dest: `${PATH.dist}/img/select-dark.svg`
|
||||
},
|
||||
{
|
||||
src: `${PATH.src}/svg/select-light.svg`,
|
||||
dest: `${PATH.dist}/img/select-light.svg`
|
||||
}
|
||||
],
|
||||
svg2svg_opts
|
||||
),
|
||||
|
||||
// SearXNG brand (static)
|
||||
plg_svg2png([
|
||||
{
|
||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
||||
dest: `${PATH.dist}/img/favicon.png`
|
||||
},
|
||||
{
|
||||
src: `${PATH.brand}/searxng.svg`,
|
||||
dest: `${PATH.dist}/img/searxng.png`
|
||||
}
|
||||
]),
|
||||
|
||||
// SearXNG PWA Icons (static)
|
||||
plg_svg2png(
|
||||
[
|
||||
{
|
||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
||||
dest: `${PATH.dist}/img/512.png`
|
||||
}
|
||||
],
|
||||
512,
|
||||
512
|
||||
),
|
||||
plg_svg2png(
|
||||
[
|
||||
{
|
||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
||||
dest: `${PATH.dist}/img/192.png`
|
||||
}
|
||||
],
|
||||
192,
|
||||
192
|
||||
),
|
||||
|
||||
// -- svg
|
||||
plg_svg2svg(
|
||||
[
|
||||
{
|
||||
src: `${PATH.brand}/searxng.svg`,
|
||||
dest: `${PATH.dist}/img/searxng.svg`
|
||||
},
|
||||
{
|
||||
src: `${PATH.brand}/img_load_error.svg`,
|
||||
dest: `${PATH.dist}/img/img_load_error.svg`
|
||||
}
|
||||
],
|
||||
svg2svg_opts
|
||||
),
|
||||
|
||||
// -- favicon
|
||||
plg_svg2svg(
|
||||
[
|
||||
{
|
||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
||||
dest: `${PATH.dist}/img/favicon.svg`
|
||||
}
|
||||
],
|
||||
svg2svg_favicon_opts
|
||||
),
|
||||
|
||||
// -- simple templates
|
||||
plg_svg2svg(
|
||||
[
|
||||
{
|
||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
||||
dest: `${PATH.templates}/searxng-wordmark.min.svg`
|
||||
}
|
||||
],
|
||||
svg2svg_opts
|
||||
)
|
||||
], // end: plugins
|
||||
|
||||
// FIXME: missing CCS sourcemaps!!
|
||||
// see: https://github.com/vitejs/vite/discussions/13845#discussioncomment-11992084
|
||||
//
|
||||
// what I have tried so far (see config below):
|
||||
//
|
||||
// - build.sourcemap
|
||||
// - esbuild.sourcemap
|
||||
// - css.preprocessorOptions.less.sourceMap
|
||||
css: {
|
||||
transformer: "lightningcss",
|
||||
lightningcss: {
|
||||
targets: browserslistToTargets(manifest.browserslist)
|
||||
},
|
||||
devSourcemap: true
|
||||
} // end: css
|
||||
} satisfies UserConfig;
|
||||
@@ -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
|
||||
25
container/base-builder.yml
Normal file
25
container/base-builder.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
contents:
|
||||
repositories:
|
||||
- https://mirrors.edge.kernel.org/alpine/edge/main
|
||||
- https://mirrors.edge.kernel.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
58
container/base.yml
Normal file
@@ -0,0 +1,58 @@
|
||||
contents:
|
||||
repositories:
|
||||
- https://mirrors.edge.kernel.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
|
||||
@@ -1,31 +1,24 @@
|
||||
FROM ghcr.io/searxng/base:searxng-builder AS builder
|
||||
|
||||
COPY ./requirements.txt ./requirements-server.txt ./
|
||||
COPY ./requirements*.txt ./
|
||||
|
||||
ENV UV_NO_MANAGED_PYTHON="true"
|
||||
ENV UV_NATIVE_TLS="true"
|
||||
RUN --mount=type=cache,id=pip,target=/root/.cache/pip set -eux; \
|
||||
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; \
|
||||
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" {} +
|
||||
ARG TIMESTAMP_SETTINGS="0"
|
||||
|
||||
COPY --exclude=./searx/version_frozen.py ./searx/ ./searx/
|
||||
|
||||
RUN set -eux -o pipefail; \
|
||||
python -m compileall -q -f -j 0 --invalidation-mode=unchecked-hash ./searx/; \
|
||||
RUN set -eux; \
|
||||
python -m compileall -q ./searx/; \
|
||||
touch -c --date=@$TIMESTAMP_SETTINGS ./searx/settings.yml; \
|
||||
find ./searx/static/ -type f \
|
||||
\( -name "*.html" -o -name "*.css" -o -name "*.js" -o -name "*.svg" \) \
|
||||
-exec gzip -9 -k {} + \
|
||||
-exec brotli -9 -k {} + \
|
||||
-exec gzip --test {}.gz + \
|
||||
-exec brotli --test {}.br +
|
||||
\( -name "*.html" -o -name "*.css" -o -name "*.js" -o -name "*.svg" \) \
|
||||
-exec gzip -9 -k {} + \
|
||||
-exec brotli -9 -k {} + \
|
||||
-exec gzip --test {}.gz + \
|
||||
-exec brotli --test {}.br +; \
|
||||
# Move always changing files to /usr/local/searxng/
|
||||
mv ./searx/version_frozen.py ./
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
FROM ghcr.io/searxng/base:searxng AS dist
|
||||
|
||||
ARG CONTAINER_IMAGE_ORGANIZATION="searxng"
|
||||
ARG CONTAINER_IMAGE_NAME="searxng"
|
||||
|
||||
FROM localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder AS builder
|
||||
FROM ghcr.io/searxng/base:searxng AS dist
|
||||
|
||||
COPY --chown=977:977 --from=builder /usr/local/searxng/.venv/ ./.venv/
|
||||
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/
|
||||
COPY --chown=searxng:searxng --from=localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder /usr/local/searxng/.venv/ ./.venv/
|
||||
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=searxng:searxng --from=localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder /usr/local/searxng/version_frozen.py ./searx/
|
||||
|
||||
ARG CREATED="0001-01-01T00:00:00Z"
|
||||
ARG VERSION="unknown"
|
||||
@@ -15,29 +14,30 @@ ARG VCS_URL="unknown"
|
||||
ARG VCS_REVISION="unknown"
|
||||
|
||||
LABEL org.opencontainers.image.created="$CREATED" \
|
||||
org.opencontainers.image.description="SearXNG is a metasearch engine. Users are neither tracked nor profiled." \
|
||||
org.opencontainers.image.documentation="https://docs.searxng.org/admin/installation-docker" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
|
||||
org.opencontainers.image.revision="$VCS_REVISION" \
|
||||
org.opencontainers.image.source="$VCS_URL" \
|
||||
org.opencontainers.image.title="SearXNG" \
|
||||
org.opencontainers.image.url="https://searxng.org" \
|
||||
org.opencontainers.image.version="$VERSION"
|
||||
org.opencontainers.image.description="SearXNG is a metasearch engine. Users are neither tracked nor profiled." \
|
||||
org.opencontainers.image.documentation="https://docs.searxng.org/admin/installation-docker" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
|
||||
org.opencontainers.image.revision="$VCS_REVISION" \
|
||||
org.opencontainers.image.source="$VCS_URL" \
|
||||
org.opencontainers.image.title="SearXNG" \
|
||||
org.opencontainers.image.url="https://searxng.org" \
|
||||
org.opencontainers.image.version="$VERSION"
|
||||
|
||||
ENV __SEARXNG_VERSION="$VERSION" \
|
||||
__SEARXNG_SETTINGS_PATH="$__SEARXNG_CONFIG_PATH/settings.yml" \
|
||||
ENV SEARXNG_VERSION="$VERSION" \
|
||||
SEARXNG_SETTINGS_PATH="$CONFIG_PATH/settings.yml" \
|
||||
GRANIAN_PROCESS_NAME="searxng" \
|
||||
GRANIAN_INTERFACE="wsgi" \
|
||||
GRANIAN_HOST="::" \
|
||||
GRANIAN_PORT="8080" \
|
||||
GRANIAN_WEBSOCKETS="false" \
|
||||
GRANIAN_LOOP="uvloop" \
|
||||
GRANIAN_BLOCKING_THREADS="4" \
|
||||
GRANIAN_WORKERS_KILL_TIMEOUT="30s" \
|
||||
GRANIAN_BLOCKING_THREADS_IDLE_TIMEOUT="5m"
|
||||
|
||||
# "*_PATH" ENVs are defined in base images
|
||||
VOLUME $__SEARXNG_CONFIG_PATH
|
||||
VOLUME $__SEARXNG_DATA_PATH
|
||||
VOLUME $CONFIG_PATH
|
||||
VOLUME $DATA_PATH
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
|
||||
@@ -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:
|
||||
@@ -77,41 +77,54 @@ volume_handler() {
|
||||
setup_ownership "$target" "directory"
|
||||
}
|
||||
|
||||
setup() {
|
||||
local template_settings="/usr/local/searxng/settings.template.yml"
|
||||
local target_settings="$__SEARXNG_CONFIG_PATH/settings.yml"
|
||||
# Handle configuration file updates
|
||||
config_handler() {
|
||||
local target="$1"
|
||||
local template="$2"
|
||||
local new_template_target="$target.new"
|
||||
|
||||
if [ ! -f "$target_settings" ]; then
|
||||
# Create/Update the configuration file
|
||||
if [ -f "$target" ]; then
|
||||
setup_ownership "$target" "file"
|
||||
|
||||
if [ "$template" -nt "$target" ]; then
|
||||
cp -pfT "$template" "$new_template_target"
|
||||
|
||||
cat <<EOF
|
||||
...
|
||||
... INFORMATION
|
||||
... 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
|
||||
fi
|
||||
else
|
||||
cat <<EOF
|
||||
...
|
||||
... INFORMATION
|
||||
... "$target_settings" does not exist, creating from template...
|
||||
... "$target" does not exist, creating from template...
|
||||
...
|
||||
EOF
|
||||
cp -pfT "$template_settings" "$target_settings"
|
||||
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
|
||||
|
||||
check_file "$target_settings"
|
||||
check_file "$target"
|
||||
}
|
||||
|
||||
cat <<EOF
|
||||
SearXNG $__SEARXNG_VERSION
|
||||
SearXNG $SEARXNG_VERSION
|
||||
EOF
|
||||
|
||||
# Check for volume mounts
|
||||
volume_handler "$__SEARXNG_CONFIG_PATH"
|
||||
volume_handler "$__SEARXNG_DATA_PATH"
|
||||
volume_handler "$CONFIG_PATH"
|
||||
volume_handler "$DATA_PATH"
|
||||
|
||||
setup
|
||||
|
||||
# root only features
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
update-ca-certificates
|
||||
fi
|
||||
|
||||
# ENVs aliases
|
||||
export GRANIAN_PORT="${SEARXNG_PORT:-$GRANIAN_PORT}"
|
||||
# Check for files
|
||||
config_handler "$SEARXNG_SETTINGS_PATH" "/usr/local/searxng/searx/settings.yml"
|
||||
|
||||
exec /usr/local/searxng/.venv/bin/granian searx.webapp:app
|
||||
|
||||
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user