summaryrefslogtreecommitdiff
path: root/searx/infopage/__init__.py
blob: 6b8fd913379a0305966e92abe85cfb376eaaf872 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# 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
from markdown_it import MarkdownIt

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 markdown-it-py_.

        .. _CommonMark: https://commonmark.org/
        .. _markdown-it-py: https://github.com/executablebooks/markdown-it-py

        """
        return (
            MarkdownIt("commonmark", {"typographer": True}).enable(["replacements", "smartquotes"]).render(self.content)
        )

    def get_ctx(self):
        """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.folder: str = info_folder or _INFO_FOLDER
        """location of the Markdwon files"""

        self.CACHE: typing.Dict[tuple, typing.Optional[InfoPage]] = {}

        self.locale_default: str = 'en'
        """default language"""

        self.locales: typing.List[str] = [
            locale.replace('_', '-') for locale in os.listdir(_INFO_FOLDER) if locale.replace('_', '-') in LOCALE_NAMES
        ]
        """list of supported languages (aka locales)"""

        self.toc: typing.List[str] = [
            'search-syntax',
            'about',
            'donate',
        ]
        """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)

        if cache_key in self.CACHE:
            return self.CACHE[cache_key]

        # not yet instantiated

        fname = os.path.join(self.folder, locale.replace('-', '_'), 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 iter_pages(self, locale: typing.Optional[str] = None, fallback_to_default=False):
        """Iterate over all pages of the TOC"""
        locale = locale or self.locale_default
        for page_name in self.toc:
            page_locale = locale
            page = self.get_page(page_name, locale)
            if fallback_to_default and page is None:
                page_locale = self.locale_default
                page = self.get_page(page_name, self.locale_default)
            if page is not None:
                # page is None if the page was deleted by the administrator
                yield page_name, page_locale, page