Merge pull request from return42/merge-user-doc2

Integrate the user documentation into the application
This commit is contained in:
Markus Heiser 2022-03-13 23:12:46 +01:00 committed by GitHub
commit cd92a7eacd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 513 additions and 173 deletions

View File

@ -14,7 +14,7 @@ p.version-warning {
background-color: #004b6b;
}
div.sidebar {
aside.sidebar {
background-color: whitesmoke;
border-color: lightsteelblue;
border-radius: 3pt;

View File

@ -35,7 +35,7 @@ master_doc = "index"
source_suffix = '.rst'
numfig = True
exclude_patterns = ['build-templates/*.rst']
exclude_patterns = ['build-templates/*.rst', 'user/*.md']
import searx.engines
import searx.plugins
@ -94,7 +94,6 @@ extlinks['pull-searx'] = ('https://github.com/searx/searx/pull/%s', 'PR ')
# links to custom brand
extlinks['origin'] = (GIT_URL + '/blob/' + GIT_BRANCH + '/%s', 'git://')
extlinks['patch'] = (GIT_URL + '/commit/%s', '#')
extlinks['search'] = (SEARXNG_URL + '/%s', '#')
extlinks['docs'] = (DOCS_URL + '/%s', 'docs: ')
extlinks['pypi'] = ('https://pypi.org/project/%s', 'PyPi: ')
extlinks['man'] = ('https://manpages.debian.org/jump?q=%s', '')
@ -117,14 +116,17 @@ extensions = [
"sphinx.ext.intersphinx",
"pallets_sphinx_themes",
"sphinx_issues", # https://github.com/sloria/sphinx-issues/blob/master/README.rst
"sphinxcontrib.jinja", # https://github.com/tardyp/sphinx-jinja
"sphinx_jinja", # https://github.com/tardyp/sphinx-jinja
"sphinxcontrib.programoutput", # https://github.com/NextThought/sphinxcontrib-programoutput
'linuxdoc.kernel_include', # Implementation of the 'kernel-include' reST-directive.
'linuxdoc.rstFlatTable', # Implementation of the 'flat-table' reST-directive.
'linuxdoc.kfigure', # Sphinx extension which implements scalable image handling.
"sphinx_tabs.tabs", # https://github.com/djungelorm/sphinx-tabs
'myst_parser', # https://www.sphinx-doc.org/en/master/usage/markdown.html
]
suppress_warnings = ['myst.domains']
intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
"flask": ("https://flask.palletsprojects.com/", None),

View File

@ -31,6 +31,7 @@ If you don't trust anyone, you can set up your own, see :ref:`installation`.
:caption: Contents
user/index
own-instance
admin/index
dev/index
utils/index

View File

@ -0,0 +1,8 @@
.. _searx.infopage:
================
Online ``/info``
================
.. automodule:: searx.infopage
:members:

1
docs/user/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.md

View File

@ -2,9 +2,14 @@
User documentation
==================
.. toctree::
:maxdepth: 2
:caption: Contents
.. contents:: Contents
:depth: 3
:local:
:backlinks: entry
.. _search-syntax:
.. include:: search-syntax.md
:parser: myst_parser.sphinx_
search_syntax
own-instance

View File

@ -1,39 +0,0 @@
.. _search-syntax:
=============
Search syntax
=============
SearXNG allows you to modify the default categories, engines and search language
via the search query.
Prefix ``!``
to set Category/engine
Prefix: ``:``
to set language
Abbrevations of the engines and languages are also accepted. Engine/category
modifiers are chainable and inclusive (e.g. with :search:`!it !ddg !wp qwer
<?q=%21it%20%21ddg%20%21wp%20qwer>` search in IT category **and** duckduckgo
**and** wikipedia for ``qwer``).
See the :search:`/preferences page <preferences>` for the list of engines,
categories and languages.
Examples
========
Search in wikipedia for ``qwer``:
- :search:`!wp qwer <?q=%21wp%20qwer>` or
- :search:`!wikipedia qwer :search:<?q=%21wikipedia%20qwer>`
Image search:
- :search:`!images Cthulhu <?q=%21images%20Cthulhu>`
Custom language in wikipedia:
- :search:`:hu !wp hackerspace <?q=%3Ahu%20%21wp%20hackerspace>`

1
manage
View File

@ -419,6 +419,7 @@ docs.prebuild() {
./utils/searx.sh doc | cat > "${DOCS_BUILD}/includes/searx.rst"
./utils/filtron.sh doc | cat > "${DOCS_BUILD}/includes/filtron.rst"
./utils/morty.sh doc | cat > "${DOCS_BUILD}/includes/morty.rst"
pyenv.cmd searxng_extra/docs_prebuild
)
dump_return $?
}

View File

@ -10,10 +10,11 @@ twine==3.8.0
Pallets-Sphinx-Themes==2.0.2
Sphinx==4.4.0
sphinx-issues==3.0.1
sphinx-jinja==1.4.0
sphinx-tabs==3.2.0
sphinx-jinja==2.0.1
sphinx-tabs @ git+https://github.com/return42/sphinx-tabs.git@fix-152#egg=fix-152
sphinxcontrib-programoutput==0.17
sphinx-autobuild==2021.3.14
myst-parser==0.17.0
linuxdoc==20211220
aiounittest==1.4.1
yamllint==1.26.3

70
searx/compat.py Normal file
View File

@ -0,0 +1,70 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# lint: pylint
# pyright: basic
"""Module for backward compatibility.
"""
# pylint: disable=C,R
try:
from functools import cached_property # pylint: disable=unused-import
except ImportError:
# cache_property has been added in py3.8 [1]
#
# To support cache_property in py3.7 the implementation from 3.8 has been
# copied here. This code can be cleanup with EOL of py3.7.
#
# [1] https://docs.python.org/3/library/functools.html#functools.cached_property
from threading import RLock
_NOT_FOUND = object()
class cached_property:
def __init__(self, func):
self.func = func
self.attrname = None
self.__doc__ = func.__doc__
self.lock = RLock()
def __set_name__(self, owner, name):
if self.attrname is None:
self.attrname = name
elif name != self.attrname:
raise TypeError(
"Cannot assign the same cached_property to two different names "
f"({self.attrname!r} and {name!r})."
)
def __get__(self, instance, owner=None):
if instance is None:
return self
if self.attrname is None:
raise TypeError("Cannot use cached_property instance without calling __set_name__ on it.")
try:
cache = instance.__dict__
except AttributeError: # not all objects have __dict__ (e.g. class defines slots)
msg = (
f"No '__dict__' attribute on {type(instance).__name__!r} "
f"instance to cache {self.attrname!r} property."
)
raise TypeError(msg) from None
val = cache.get(self.attrname, _NOT_FOUND)
if val is _NOT_FOUND:
with self.lock:
# check if another thread filled cache while we awaited lock
val = cache.get(self.attrname, _NOT_FOUND)
if val is _NOT_FOUND:
val = self.func(instance)
try:
cache[self.attrname] = val
except TypeError:
msg = (
f"The '__dict__' attribute on {type(instance).__name__!r} instance "
f"does not support item assignment for caching {self.attrname!r} property."
)
raise TypeError(msg) from None
return val

179
searx/infopage/__init__.py Normal file
View File

@ -0,0 +1,179 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# lint: pylint
# pyright: basic
"""Render SearXNG instance documentation.
Usage in a Flask app route:
.. code:: python
from searx import infopage
_INFO_PAGES = infopage.InfoPageSet(infopage.MistletoePage)
@app.route('/info/<pagename>', methods=['GET'])
def info(pagename):
locale = request.preferences.get_value('locale')
page = _INFO_PAGES.get_page(pagename, locale)
"""
__all__ = ['InfoPage', 'InfoPageSet']
import os
import os.path
import logging
import typing
import urllib.parse
import jinja2
from flask.helpers import url_for
import mistletoe
from .. import get_setting
from ..compat import cached_property
from ..version import GIT_URL
from ..locales import LOCALE_NAMES
logger = logging.getLogger('searx.infopage')
_INFO_FOLDER = os.path.abspath(os.path.dirname(__file__))
class InfoPage:
"""A page of the :py:obj:`online documentation <InfoPageSet>`."""
def __init__(self, fname):
self.fname = fname
@cached_property
def raw_content(self):
"""Raw content of the page (without any jinja rendering)"""
with open(self.fname, 'r', encoding='utf-8') as f:
return f.read()
@cached_property
def content(self):
"""Content of the page (rendered in a Jinja conntext)"""
ctx = self.get_ctx()
template = jinja2.Environment().from_string(self.raw_content)
return template.render(**ctx)
@cached_property
def title(self):
"""Title of the content (without any markup)"""
t = ""
for l in self.raw_content.split('\n'):
if l.startswith('# '):
t = l.strip('# ')
return t
@cached_property
def html(self):
"""Render Markdown (CommonMark_) to HTML by using mistletoe_.
.. _CommonMark: https://commonmark.org/
.. _mistletoe: https://github.com/miyuchina/mistletoe
"""
return mistletoe.markdown(self.content)
def get_ctx(self): # pylint: disable=no-self-use
"""Jinja context to render :py:obj:`InfoPage.content`"""
def _md_link(name, url):
url = url_for(url, _external=True)
return "[%s](%s)" % (name, url)
def _md_search(query):
url = '%s?q=%s' % (url_for('search', _external=True), urllib.parse.quote(query))
return '[%s](%s)' % (query, url)
ctx = {}
ctx['GIT_URL'] = GIT_URL
ctx['get_setting'] = get_setting
ctx['link'] = _md_link
ctx['search'] = _md_search
return ctx
def __repr__(self):
return f'<{self.__class__.__name__} fname={self.fname!r}>'
class InfoPageSet: # pylint: disable=too-few-public-methods
"""Cached rendering of the online documentation a SearXNG instance has.
:param page_class: render online documentation by :py:obj:`InfoPage` parser.
:type page_class: :py:obj:`InfoPage`
:param info_folder: information directory
:type info_folder: str
"""
def __init__(
self, page_class: typing.Optional[typing.Type[InfoPage]] = None, info_folder: typing.Optional[str] = None
):
self.page_class = page_class or InfoPage
self.CACHE: typing.Dict[tuple, typing.Optional[InfoPage]] = {}
# future: could be set from settings.xml
self.folder: str = info_folder or _INFO_FOLDER
"""location of the Markdwon files"""
self.locale_default: str = 'en'
"""default language"""
self.locales: typing.List = [locale for locale in os.listdir(_INFO_FOLDER) if locale in LOCALE_NAMES]
"""list of supported languages (aka locales)"""
self.toc: typing.List = [
'search-syntax',
'about',
]
"""list of articles in the online documentation"""
def get_page(self, pagename: str, locale: typing.Optional[str] = None):
"""Return ``pagename`` instance of :py:obj:`InfoPage`
:param pagename: name of the page, a value from :py:obj:`InfoPageSet.toc`
:type pagename: str
:param locale: language of the page, e.g. ``en``, ``zh_Hans_CN``
(default: :py:obj:`InfoPageSet.i18n_origin`)
:type locale: str
"""
locale = locale or self.locale_default
if pagename not in self.toc:
return None
if locale not in self.locales:
return None
cache_key = (pagename, locale)
page = self.CACHE.get(cache_key)
if page is not None:
return page
# not yet instantiated
fname = os.path.join(self.folder, locale, pagename) + '.md'
if not os.path.exists(fname):
logger.info('file %s does not exists', fname)
self.CACHE[cache_key] = None
return None
page = self.page_class(fname)
self.CACHE[cache_key] = page
return page
def all_pages(self, locale: typing.Optional[str] = None):
"""Iterate over all pages of the TOC"""
locale = locale or self.locale_default
for pagename in self.toc:
page = self.get_page(pagename, locale)
yield pagename, page

View File

@ -1,30 +1,29 @@
# About SearXNG
SearXNG is a fork from the well-known [searx] [metasearch engine], aggregating
the results of other [search engines][url_for:preferences] while not storing
information about its users.
the results of other {{link('search engines', 'preferences')}} while not
storing information about its users.
More about SearXNG ...
* [SearXNG sources][brand.git_url]
* [SearXNG sources]({{GIT_URL}})
* [weblate]
---
## Why use it?
* SearXNG may not offer you as personalised results as Google,
but it doesn't generate a profile about you.
* SearXNG may not offer you as personalised results as Google, but it doesn't
generate a profile about you.
* SearXNG doesn't care about what you search for, never shares anything
with a third party, and it can't be used to compromise you.
* SearXNG doesn't care about what you search for, never shares anything with a
third party, and it can't be used to compromise you.
* SearXNG is free software, the code is 100% open and you can help
to make it better. See more on [SearXNG sources][brand.git_url].
* SearXNG is free software, the code is 100% open and you can help to make it
better. See more on [SearXNG sources]({{GIT_URL}}).
If you do care about privacy, want to be a conscious user, or otherwise
believe in digital freedom, make SearXNG your default search engine or run
it on your own server
If you do care about privacy, want to be a conscious user, or otherwise believe
in digital freedom, make SearXNG your default search engine or run it on your
own server
## Technical details - How does it work?
@ -37,35 +36,40 @@ exception: searx uses the search bar to perform GET requests. SearXNG can be
added to your browser's search bar; moreover, it can be set as the default
search engine.
<span id='add to browser'></span>
## How to set as the default search engine?
SearXNG supports [OpenSearch]. For more information on changing your default
search engine, see your browser's documentation:
* [Firefox](https://support.mozilla.org/en-US/kb/add-or-remove-search-engine-firefox)
* [Microsoft Edge](https://support.microsoft.com/en-us/help/4028574/microsoft-edge-change-the-default-search-engine)
* Chromium-based browsers [only add websites that the user navigates to without a path.](https://www.chromium.org/tab-to-search)
* [Firefox]
* [Microsoft Edge]
* Chromium-based browsers [only add websites that the user navigates to without
a path.](https://www.chromium.org/tab-to-search)
## Where to find anonymous usage statistics of this instance ?
[Stats page][url_for:stats] contains some useful data about the engines used.
{{link('Stats page', 'stats')}} contains some useful data about the engines
used.
## How can I make it my own?
SearXNG appreciates your concern regarding logs, so take the code from
the [SearXNG project][brand.git_url] and run it yourself!
SearXNG appreciates your concern regarding logs, so take the code from the
[SearXNG project]({{GIT_URL}}) and run it yourself!
Add your instance to this [list of public instances][brand.public_instances] to
help other people reclaim their privacy and make the Internet freer! The more
decentralized the Internet is, the more freedom we have!
Add your instance to this [list of public
instances]({{get_setting('brand.public_instances')}}) to help other people
reclaim their privacy and make the Internet freer! The more decentralized the
Internet is, the more freedom we have!
## Where are the docs & code of this instance?
See the [SearXNG docs][brand.docs_url] and [SearXNG sources][brand.git_url]
See the [SearXNG docs]({{get_setting('brand.docs_url')}}) and [SearXNG
sources]({{GIT_URL}})
[searx]: https://github.com/searx/searx
[metasearch engine]: https://en.wikipedia.org/wiki/Metasearch_engine
[weblate]: https://weblate.bubu1.eu/projects/searxng/
[seeks project]: https://beniz.github.io/seeks/
[OpenSearch]: https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md
[Firefox]: https://support.mozilla.org/en-US/kb/add-or-remove-search-engine-firefox
[Microsoft Edge]: https://support.microsoft.com/en-us/help/4028574/microsoft-edge-change-the-default-search-engine

View File

@ -0,0 +1,35 @@
# Search syntax
SearXNG allows you to modify the default categories, engines and search language
via the search query.
Prefix `!` to set category and engine names.
Prefix: `:` to set the language.
Abbrevations of the engines and languages are also accepted. Engine/category
modifiers are chainable and inclusive. E.g. with {{search('!map !ddg !wp paris')}}
search in map category **and** duckduckgo **and** wikipedia for
`paris`.
See the {{link('preferences', 'preferences')}} for the list of engines,
categories and languages.
## Examples
Search in wikipedia for `paris`:
* {{search('!wp paris')}}
* {{search('!wikipedia paris')}}
Search in category `map` for `paris`:
* {{search('!map paris')}}
Image search:
* {{search('!images Wau Holland')}}
Custom language in wikipedia:
* {{search(':fr !wp Wau Holland')}}

Binary file not shown.

Binary file not shown.

View File

@ -109,6 +109,8 @@
--color-toolkit-engine-tooltip-background: #fff;
--color-toolkit-loader-border: rgba(0, 0, 0, 0.2);
--color-toolkit-loader-borderleft: rgba(255, 255, 255, 0);
--color-doc-code: #300;
--color-doc-code-background: #fdd;
}
.dark-themes() {
@ -215,6 +217,8 @@
--color-toolkit-engine-tooltip-background: #222;
--color-toolkit-loader-border: rgba(255, 255, 255, 0.2);
--color-toolkit-loader-borderleft: rgba(0, 0, 0, 0);
--color-doc-code: #fdd;
--color-doc-code-background: #300;
}
/// Dark Theme (autoswitch based on device pref)

View File

@ -0,0 +1,13 @@
.info-page {
font-family: sans-serif;
font-size: 1.3em;
code {
font-family: monospace;
font-size: 1.3em;
color: var(--color-doc-code);
background-color: var(--color-doc-code-background);
padding: 2px 5px;
.rounded-corners(5px);
}
}

View File

@ -18,6 +18,7 @@
@import "detail.less";
@import "animations.less";
@import "embedded.less";
@import "info.less";
// for index.html template
@import "index.less";

View File

@ -1,12 +0,0 @@
{% extends "oscar/base.html" %}
{% block title %}{{ page.title }} - {% endblock %}
{% block content %}
<ul class="nav nav-tabs">
{% for name, page in all_pages %}
<li {% if name == page_filename %}class="active"{% endif %}>
<a href="{{name}}">{{page.title}}</a>
</li>
{% endfor %}
</ul>
{{ page.content | safe }}
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends "oscar/base.html" %}
{% block title %}{{ active_page.title }} - {% endblock %}
{% block content %}
<ul class="nav nav-tabs">
{% for pagename, page, locale in all_pages %}
<li>
<a href="{{ url_for('info', pagename=pagename, locale=locale) }}" {% if pagename == active_pagename %}class="active"{% endif %}>{{page.title}}</a>
</li>
{% endfor %}
</ul>
{{ active_page.html | safe }}
{% endblock %}

View File

@ -3,7 +3,7 @@
<a href="{{ url_for('index') }}">{{ instance_name }}</a>{{- "" -}}
</span>{{- "" -}}
<span class="{% if rtl %}pull-left{% else %}pull-right{% endif %}">{{- "" -}}
<a href="{{ url_for('help_page', pagename='about') }}">{{ _('about') }}</a>{{- "" -}}
<a href="{{ url_for('info', pagename='about') }}">{{ _('about') }}</a>{{- "" -}}
<a href="{{ url_for('preferences') }}">{{ _('preferences') }}</a>{{- "" -}}
</span>{{- "" -}}
</div>

View File

@ -58,7 +58,7 @@
</main>
<footer>
<p>
{{ _('Powered by') }} <a href="{{ url_for('help_page', pagename='about') }}">searxng</a> - {{ searx_version }} — {{ _('a privacy-respecting, hackable metasearch engine') }}<br/>
{{ _('Powered by') }} <a href="{{ url_for('info', pagename='about') }}">searxng</a> - {{ searx_version }} — {{ _('a privacy-respecting, hackable metasearch engine') }}<br/>
<a href="{{ searx_git_url }}">{{ _('Source code') }}</a> |
<a href="{{ get_setting('brand.issue_url') }}">{{ _('Issue tracker') }}</a> |
<a href="{{ url_for('stats') }}">{{ _('Engine stats') }}</a> |

View File

@ -1,12 +0,0 @@
{% extends 'simple/page_with_header.html' %}
{% block title %}{{ page.title }} - {% endblock %}
{% block content %}
<ul class="tabs">
{% for name, page in all_pages %}
<li>
<a href="{{name}}" {% if name == page_filename %}class="active"{% endif %}>{{page.title}}</a>
</li>
{% endfor %}
</ul>
{{ page.content | safe }}
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends 'simple/page_with_header.html' %}
{% block title %}{{ active_page.title }} - {% endblock %}
{% block content %}
<ul class="tabs">
{% for pagename, page, locale in all_pages %}
<li>
<a href="{{ url_for('info', pagename=pagename, locale=locale) }}" {% if pagename == active_pagename %}class="active"{% endif %}>{{page.title}}</a>
</li>
{% endfor %}
</ul>
<div class="info-page {{pagename}}">
{{- active_page.html | safe -}}
</div>
{% endblock %}

View File

@ -1,61 +0,0 @@
# pyright: basic
from typing import Dict, NamedTuple
import pkg_resources
import flask
from flask.helpers import url_for
import mistletoe
from . import get_setting
from .version import GIT_URL
class HelpPage(NamedTuple):
title: str
content: str
# Whenever a new .md file is added to help/ it needs to be added here
_TOC = ('about',)
PAGES: Dict[str, HelpPage] = {}
""" Maps a filename under help/ without the file extension to the rendered page. """
def render(app: flask.Flask):
"""
Renders the user documentation. Must be called after all Flask routes have been
registered, because the documentation might try to link to them with Flask's `url_for`.
We render the user documentation once on startup to improve performance.
"""
link_targets = {
'brand.git_url': GIT_URL,
'brand.public_instances': get_setting('brand.public_instances'),
'brand.docs_url': get_setting('brand.docs_url'),
}
base_url = get_setting('server.base_url') or None
# we specify base_url so that url_for works for base_urls that have a non-root path
with app.test_request_context(base_url=base_url):
link_targets['url_for:index'] = url_for('index')
link_targets['url_for:preferences'] = url_for('preferences')
link_targets['url_for:stats'] = url_for('stats')
define_link_targets = ''.join(f'[{name}]: {url}\n' for name, url in link_targets.items())
for pagename in _TOC:
file_content = pkg_resources.resource_string(__name__, 'help/' + pagename + '.md').decode()
markdown = define_link_targets + file_content
assert file_content.startswith('# ')
title = file_content.split('\n', maxsplit=1)[0].strip('# ')
content: str = mistletoe.markdown(markdown)
if pagename == 'about':
try:
content += pkg_resources.resource_string(__name__, 'templates/__common__/aboutextend.html').decode()
except FileNotFoundError:
pass
PAGES[pagename] = HelpPage(title=title, content=content)

View File

@ -56,8 +56,9 @@ from searx import (
get_setting,
settings,
searx_debug,
user_help,
)
from searx import infopage
from searx.data import ENGINE_DESCRIPTIONS
from searx.results import Timing, UnresponsiveEngine
from searx.settings_defaults import OUTPUT_FORMATS
@ -382,6 +383,11 @@ def url_for_theme(endpoint: str, override_theme: Optional[str] = None, **values)
if file_hash:
values['filename'] = filename_with_theme
suffix = "?" + file_hash
if endpoint == 'info' and 'locale' not in values:
locale = request.preferences.get_value('locale')
if _INFO_PAGES.get_page(values['pagename'], locale) is None:
locale = _INFO_PAGES.locale_default
values['locale'] = locale
return url_for(endpoint, **values) + suffix
@ -660,6 +666,7 @@ def index():
# fmt: off
'index.html',
selected_categories=get_selected_categories(request.preferences, request.form),
current_locale = request.preferences.get_value("locale"),
# fmt: on
)
@ -864,6 +871,7 @@ def search():
unresponsive_engines = __get_translated_errors(
result_container.unresponsive_engines
),
current_locale = request.preferences.get_value("locale"),
current_language = match_language(
search_query.lang,
settings['search']['languages'],
@ -898,19 +906,36 @@ def __get_translated_errors(unresponsive_engines: Iterable[UnresponsiveEngine]):
@app.route('/about', methods=['GET'])
def about():
"""Redirect to about page"""
return redirect(url_for('help_page', pagename='about'))
locale = request.preferences.get_value('locale')
return redirect(url_for('info', pagename='about', locale=locale))
@app.route('/help/en/<pagename>', methods=['GET'])
def help_page(pagename):
"""Render help page"""
page = user_help.PAGES.get(pagename)
_INFO_PAGES = infopage.InfoPageSet()
@app.route('/info/<locale>/<pagename>', methods=['GET'])
def info(pagename, locale):
"""Render page of online user documentation"""
page = _INFO_PAGES.get_page(pagename, locale)
if page is None:
flask.abort(404)
def all_pages():
user_locale = request.preferences.get_value('locale')
for for_pagename, for_page in _INFO_PAGES.all_pages(user_locale):
for_locale = locale
if for_page is None:
# we are sure that for_pagename != pagename
for_page = _INFO_PAGES.get_page(for_pagename, _INFO_PAGES.locale_default)
for_locale = _INFO_PAGES.locale_default
yield for_pagename, for_page, for_locale
return render(
'help.html', page=user_help.PAGES[pagename], all_pages=user_help.PAGES.items(), page_filename=pagename
'info.html',
all_pages=all_pages(),
active_page=page,
active_pagename=pagename,
)
@ -1411,7 +1436,6 @@ werkzeug_reloader = flask_run_development or (searx_debug and __name__ == "__mai
if not werkzeug_reloader or (werkzeug_reloader and os.environ.get("WERKZEUG_RUN_MAIN") == "true"):
plugin_initialize(app)
search_initialize(enable_checker=True, check_network=True, enable_metrics=settings['general']['enable_metrics'])
user_help.render(app)
def run():

84
searxng_extra/docs_prebuild Executable file
View File

@ -0,0 +1,84 @@
#!/usr/bin/env python
# lint: pylint
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Script that implements some prebuild tasks needed by target docs.prebuild
"""
import sys
import os.path
import time
from contextlib import contextmanager
from searx import settings, get_setting
from searx.infopage import InfoPageSet, InfoPage
_doc_user = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'docs', 'user'))
def main():
base_url = get_setting('server.base_url', None)
if base_url:
infopageset_ctx = _instance_infosetset_ctx(base_url)
else:
infopageset_ctx = _offline_infosetset_ctx()
with infopageset_ctx as infopageset:
for _, page in infopageset.all_pages('en'):
fname = os.path.join(_doc_user, os.path.basename(page.fname))
with open(fname, 'w') as f:
f.write(page.content)
class OfflinePage(InfoPage):
def get_ctx(self): # pylint: disable=no-self-use
"""Jinja context to render :py:obj:`DocPage.content` for offline purpose (no
links to SearXNG instance)"""
ctx = super().get_ctx()
ctx['link'] = lambda name, url: '`%s`' % name
ctx['search'] = lambda query: '`%s`' % query
return ctx
@contextmanager
def _offline_infosetset_ctx():
yield InfoPageSet(OfflinePage)
@contextmanager
def _instance_infosetset_ctx(base_url):
# The url_for functions in the jinja templates need all routes to be
# registered in the Flask app.
settings['server']['secret_key'] = ''
from searx.webapp import app
# Specify base_url so that url_for() works for base_urls. If base_url is
# specified, then these values from are given preference over any Flask's
# generics (see flaskfix.py).
with app.test_request_context(base_url=base_url):
yield InfoPageSet()
# The searx.webapp import from above fires some HTTP requests, thats
# why we get a RuntimeError::
#
# RuntimeError: The connection pool was closed while 1 HTTP \
# requests/responses were still in-flight.
#
# Closing network won't help ..
# from searx.network import network
# network.done()
# waiting some seconds before ending the comand line was the only solution I
# found ..
time.sleep(3)
return DOC
if __name__ == '__main__':
sys.exit(main())

View File

@ -58,7 +58,8 @@ setup(
'../requirements.txt',
'../requirements-dev.txt',
'data/*',
'help/*',
'info/*',
'info/*/*',
'plugins/*/*',
'static/*.*',
'static/*/*.*',

View File

@ -177,10 +177,14 @@ class ViewsTestCase(SearxTestCase):
self.assertIn(b'<description>first test content</description>', result.data)
def test_about(self):
result = self.app.get('/help/en/about')
def test_redirect_about(self):
result = self.app.get('/about')
self.assertEqual(result.status_code, 302)
def test_info_page(self):
result = self.app.get('/info/en/search-syntax')
self.assertEqual(result.status_code, 200)
self.assertIn(b'<h1>About SearXNG</h1>', result.data)
self.assertIn(b'<h1>Search syntax</h1>', result.data)
def test_health(self):
result = self.app.get('/healthz')