Просмотр исходного кода

Improve layout of documentation page and add search feature (#8247)

* Improve layout of documentation page and add search feature

Closes https://github.com/FreshRSS/FreshRSS/issues/7915, https://github.com/FreshRSS/FreshRSS/issues/5325

Also: anchor headings and fix building site locally

* Further improvements

* Set color of hyperlinks
* Consistent styling of close aside button across devices
* Mobile layout 600px -> 1200px
* Add suffix to docs `<title>`
    * Note: titles of pages probably need to be improved, since currently they are just derived from the names of the first heading on every page
* Add favicon

* Improve font

* Try to fix favicon not loading correctly on GH pages

* Use local font

* Attempt to fix GH pages

* Final improvements

* Copy to clipboard button
* Support for nojs search
* Dark mode
* Load search.json (200KB json) only on search input focus
* Keep scroll state of sidebar across navigations

* Clickable images and CSP

CSP so we avoid hotlinking resources and clickable images are useful for zooming on mobile for example

* Fix typos

* Disable Dark Reader extension if dark mode CSS is loaded

* Support internationalisation (via language dropdown)

* Add Gemfile.lock

* Make CI build work with the custom plugin

* Make menus closable with Esc

* Fix typos CI

* Suggestions

* Use `ruby/setup-ruby` action in workflow for installing and caching gems.

* Run build only when there are changes to `docs/`

See: https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows?versionId=free-pro-team%40latest&productId=actions#running-your-workflow-only-when-a-push-to-specific-branches-occurs

* Change font to `Open Sans`

* Increase line height

* Fix Liquid syntax error
Inverle 3 месяцев назад
Родитель
Сommit
5e9c3617ca

+ 13 - 4
.github/workflows/jekyll-gh-pages.yml

@@ -5,6 +5,8 @@ on:
   # Runs on pushes targeting the default branch
   # Runs on pushes targeting the default branch
   push:
   push:
     branches: ["edge"]
     branches: ["edge"]
+    paths:
+      - 'docs/**'
 
 
   # Allows you to run this workflow manually from the Actions tab
   # Allows you to run this workflow manually from the Actions tab
   workflow_dispatch:
   workflow_dispatch:
@@ -28,13 +30,20 @@ jobs:
     steps:
     steps:
       - name: Checkout
       - name: Checkout
         uses: actions/checkout@v6
         uses: actions/checkout@v6
+      - name: Setup Ruby
+        # https://github.com/ruby/setup-ruby/releases/tag/v1.268.0
+        uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71
+        with:
+          ruby-version: '3.2.3' # Not needed with a .ruby-version file
+          bundler-cache: true # runs 'bundle install' and caches installed gems automatically
+          cache-version: 0 # Increment this number if you need to re-download cached gems
+          working-directory: docs
       - name: Setup Pages
       - name: Setup Pages
         uses: actions/configure-pages@v5
         uses: actions/configure-pages@v5
       - name: Build with Jekyll
       - name: Build with Jekyll
-        uses: actions/jekyll-build-pages@v1
-        with:
-          source: ./docs/
-          destination: ./_site
+        run: |
+          cd docs
+          bundle exec jekyll build --destination ../_site
       - name: Upload artifact
       - name: Upload artifact
         uses: actions/upload-pages-artifact@v4
         uses: actions/upload-pages-artifact@v4
 
 

+ 3 - 0
.gitignore

@@ -10,6 +10,9 @@
 
 
 .vscode/
 .vscode/
 
 
+/docs/_site/
+/docs/.sass-cache/
+
 # Temp files
 # Temp files
 *~
 *~
 *.bak
 *.bak

+ 1 - 1
.markdownlint.json

@@ -6,7 +6,7 @@
 	"line-length": false,
 	"line-length": false,
 	"no-hard-tabs": false,
 	"no-hard-tabs": false,
 	"no-inline-html": {
 	"no-inline-html": {
-		"allowed_elements": ["br", "img", "kbd", "translations"]
+		"allowed_elements": ["br", "img", "kbd", "translations", "meta"]
 	},
 	},
 	"no-multiple-blanks": {
 	"no-multiple-blanks": {
 		"maximum": 2
 		"maximum": 2

+ 3 - 0
.stylelintignore

@@ -5,3 +5,6 @@ p/scripts/vendor/
 vendor/
 vendor/
 # Ignore RTLCSS auto-generated CSS
 # Ignore RTLCSS auto-generated CSS
 *.rtl.css
 *.rtl.css
+
+# actual location is docs/_includes/docs.css
+docs/assets/css/docs.css

+ 3 - 0
.typos.toml

@@ -44,6 +44,9 @@ extend-exclude = [
 	"composer.lock",
 	"composer.lock",
 	"data/",
 	"data/",
 	"docs/CHANGELOG-old.md",
 	"docs/CHANGELOG-old.md",
+	"docs/_config.yml",
+	"docs/_includes/",
+	"docs/assets/",
 	"docs/fr/",
 	"docs/fr/",
 	"lib/marienfressinaud/",
 	"lib/marienfressinaud/",
 	"lib/phpgt/",
 	"lib/phpgt/",

+ 17 - 0
docs/Gemfile

@@ -0,0 +1,17 @@
+source "https://rubygems.org"
+
+gem "kramdown-parser-gfm"
+
+group :jekyll_plugins do
+  gem 'jekyll-coffeescript'
+  gem 'jekyll-commonmark-ghpages'
+  gem 'jekyll-gist'
+  gem 'jekyll-github-metadata'
+  gem 'jekyll-paginate'
+  gem 'jekyll-relative-links'
+  gem 'jekyll-optional-front-matter'
+  gem 'jekyll-readme-index'
+  gem 'jekyll-default-layout'
+  gem 'jekyll-titles-from-headings'
+  gem 'jekyll-i18n_tags'
+end

+ 150 - 0
docs/Gemfile.lock

@@ -0,0 +1,150 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    addressable (2.8.8)
+      public_suffix (>= 2.0.2, < 8.0)
+    coffee-script (2.4.1)
+      coffee-script-source
+      execjs
+    coffee-script-source (1.12.2)
+    colorator (1.1.0)
+    commonmarker (0.23.12)
+    concurrent-ruby (1.3.5)
+    csv (3.3.5)
+    em-websocket (0.5.3)
+      eventmachine (>= 0.12.9)
+      http_parser.rb (~> 0)
+    eventmachine (1.2.7)
+    execjs (2.10.0)
+    faraday (2.14.0)
+      faraday-net_http (>= 2.0, < 3.5)
+      json
+      logger
+    faraday-net_http (3.4.2)
+      net-http (~> 0.5)
+    ffi (1.17.2)
+    ffi (1.17.2-aarch64-linux-gnu)
+    ffi (1.17.2-aarch64-linux-musl)
+    ffi (1.17.2-arm-linux-gnu)
+    ffi (1.17.2-arm-linux-musl)
+    ffi (1.17.2-arm64-darwin)
+    ffi (1.17.2-x86-linux-gnu)
+    ffi (1.17.2-x86-linux-musl)
+    ffi (1.17.2-x86_64-darwin)
+    ffi (1.17.2-x86_64-linux-gnu)
+    ffi (1.17.2-x86_64-linux-musl)
+    forwardable-extended (2.6.0)
+    http_parser.rb (0.8.0)
+    i18n (1.14.7)
+      concurrent-ruby (~> 1.0)
+    jekyll (3.10.0)
+      addressable (~> 2.4)
+      colorator (~> 1.0)
+      csv (~> 3.0)
+      em-websocket (~> 0.5)
+      i18n (>= 0.7, < 2)
+      jekyll-sass-converter (~> 1.0)
+      jekyll-watch (~> 2.0)
+      kramdown (>= 1.17, < 3)
+      liquid (~> 4.0)
+      mercenary (~> 0.3.3)
+      pathutil (~> 0.9)
+      rouge (>= 1.7, < 4)
+      safe_yaml (~> 1.0)
+      webrick (>= 1.0)
+    jekyll-coffeescript (2.0.0)
+      coffee-script (~> 2.2)
+      coffee-script-source (~> 1.12)
+    jekyll-commonmark (1.4.0)
+      commonmarker (~> 0.22)
+    jekyll-commonmark-ghpages (0.5.1)
+      commonmarker (>= 0.23.7, < 1.1.0)
+      jekyll (>= 3.9, < 4.0)
+      jekyll-commonmark (~> 1.4.0)
+      rouge (>= 2.0, < 5.0)
+    jekyll-default-layout (0.1.5)
+      jekyll (>= 3.0, < 5.0)
+    jekyll-gist (1.5.0)
+      octokit (~> 4.2)
+    jekyll-github-metadata (2.16.1)
+      jekyll (>= 3.4, < 5.0)
+      octokit (>= 4, < 7, != 4.4.0)
+    jekyll-i18n_tags (1.0.0)
+    jekyll-optional-front-matter (0.3.2)
+      jekyll (>= 3.0, < 5.0)
+    jekyll-paginate (1.1.0)
+    jekyll-readme-index (0.3.0)
+      jekyll (>= 3.0, < 5.0)
+    jekyll-relative-links (0.7.0)
+      jekyll (>= 3.3, < 5.0)
+    jekyll-sass-converter (1.5.2)
+      sass (~> 3.4)
+    jekyll-titles-from-headings (0.5.3)
+      jekyll (>= 3.3, < 5.0)
+    jekyll-watch (2.2.1)
+      listen (~> 3.0)
+    json (2.16.0)
+    kramdown (2.5.1)
+      rexml (>= 3.3.9)
+    kramdown-parser-gfm (1.1.0)
+      kramdown (~> 2.0)
+    liquid (4.0.4)
+    listen (3.9.0)
+      rb-fsevent (~> 0.10, >= 0.10.3)
+      rb-inotify (~> 0.9, >= 0.9.10)
+    logger (1.7.0)
+    mercenary (0.3.6)
+    net-http (0.8.0)
+      uri (>= 0.11.1)
+    octokit (4.25.1)
+      faraday (>= 1, < 3)
+      sawyer (~> 0.9)
+    pathutil (0.16.2)
+      forwardable-extended (~> 2.6)
+    public_suffix (7.0.0)
+    rb-fsevent (0.11.2)
+    rb-inotify (0.11.1)
+      ffi (~> 1.0)
+    rexml (3.4.4)
+    rouge (3.30.0)
+    safe_yaml (1.0.5)
+    sass (3.7.4)
+      sass-listen (~> 4.0.0)
+    sass-listen (4.0.0)
+      rb-fsevent (~> 0.9, >= 0.9.4)
+      rb-inotify (~> 0.9, >= 0.9.7)
+    sawyer (0.9.3)
+      addressable (>= 2.3.5)
+      faraday (>= 0.17.3, < 3)
+    uri (1.1.1)
+    webrick (1.9.2)
+
+PLATFORMS
+  aarch64-linux-gnu
+  aarch64-linux-musl
+  arm-linux-gnu
+  arm-linux-musl
+  arm64-darwin
+  ruby
+  x86-linux-gnu
+  x86-linux-musl
+  x86_64-darwin
+  x86_64-linux-gnu
+  x86_64-linux-musl
+
+DEPENDENCIES
+  jekyll-coffeescript
+  jekyll-commonmark-ghpages
+  jekyll-default-layout
+  jekyll-gist
+  jekyll-github-metadata
+  jekyll-i18n_tags
+  jekyll-optional-front-matter
+  jekyll-paginate
+  jekyll-readme-index
+  jekyll-relative-links
+  jekyll-titles-from-headings
+  kramdown-parser-gfm
+
+BUNDLED WITH
+   2.7.2

+ 13 - 0
docs/README.md

@@ -0,0 +1,13 @@
+This is the documentation deployed by GitHub Pages.
+
+To build and serve locally, first install the necessary packages:
+```sh
+bundle install
+```
+
+Then serve:
+```sh
+bundle exec jekyll serve -H 127.0.0.1 --watch --incremental
+```
+
+The documentation should be reachable at <http://127.0.0.1:4000/FreshRSS/>.

+ 47 - 3
docs/_config.yml

@@ -1,9 +1,53 @@
-theme: jekyll-theme-cayman
 title: FreshRSS
 title: FreshRSS
 description: Documentation center
 description: Documentation center
+baseurl: /FreshRSS
 
 
 logo: /img/FreshRSS-logo.png
 logo: /img/FreshRSS-logo.png
-show_downloads: true
 
 
 include: [contributing.md]
 include: [contributing.md]
-exclude: [CHANGELOG*.md]
+exclude: [CHANGELOG*.md, README.md, vendor]
+
+defaults:
+  -
+    scope:
+      path: "/en/*"
+    values:
+      lang: "en"
+  -
+    scope:
+      path: "/fr/*"
+    values:
+      lang: "fr"
+
+translations:
+  en:
+    back_to_freshrss: Back to freshrss.org
+    search_docs: Search docs…
+    search: Search
+    toggle_aside: Toggle sidebar
+    close: Close
+    choose_language: Choose language
+    copy_to_clipboard: Copy to clipboard
+  fr:
+    back_to_freshrss: Retour à freshrss.org
+    search_docs: Rechercher dans la documentation…
+    search: Rechercher
+    toggle_aside: Afficher/masquer le panneau
+    close: Fermer
+    choose_language: Choisir la langue
+    copy_to_clipboard: Copier dans le presse-papiers
+
+plugins:
+  # gh
+  - jekyll-coffeescript
+  - jekyll-commonmark-ghpages
+  - jekyll-gist
+  - jekyll-github-metadata
+  - jekyll-paginate
+  - jekyll-relative-links
+  - jekyll-optional-front-matter
+  - jekyll-readme-index
+  - jekyll-default-layout
+  - jekyll-titles-from-headings
+  # custom
+  - jekyll-i18n_tags

+ 174 - 0
docs/_includes/anchor_headings.html

@@ -0,0 +1,174 @@
+{% capture headingsWorkspace %}
+  {% comment %}
+    Copyright (c) 2018 Vladimir "allejo" Jimenez
+
+    Permission is hereby granted, free of charge, to any person
+    obtaining a copy of this software and associated documentation
+    files (the "Software"), to deal in the Software without
+    restriction, including without limitation the rights to use,
+    copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following
+    conditions:
+
+    The above copyright notice and this permission notice shall be
+    included in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+    OTHER DEALINGS IN THE SOFTWARE.
+  {% endcomment %}
+  {% comment %}
+    Version 1.0.13
+      https://github.com/allejo/jekyll-anchor-headings
+
+    "Be the pull request you wish to see in the world." ~Ben Balter
+
+    Usage:
+      {% include anchor_headings.html html=content anchorBody="#" %}
+
+    Parameters:
+      * html          (string) - the HTML of compiled markdown generated by kramdown in Jekyll
+
+    Optional Parameters:
+      * beforeHeading (bool)   : false  - Set to true if the anchor should be placed _before_ the heading's content
+      * headerAttrs   (string) :  ''    - Any custom HTML attributes that will be added to the heading tag; you may NOT use `id`;
+                                          the `%heading%` and `%html_id%` placeholders are available
+      * anchorAttrs   (string) :  ''    - Any custom HTML attributes that will be added to the `<a>` tag; you may NOT use `href`, `class` or `title`;
+                                          the `%heading%` and `%html_id%` placeholders are available
+      * anchorBody    (string) :  ''    - The content that will be placed inside the anchor; the `%heading%` placeholder is available
+      * anchorClass   (string) :  ''    - The class(es) that will be used for each anchor. Separate multiple classes with a space
+      * anchorTitle   (string) :  ''    - The `title` attribute that will be used for anchors
+      * h_min         (int)    :  1     - The minimum header level to build an anchor for; any header lower than this value will be ignored
+      * h_max         (int)    :  6     - The maximum header level to build an anchor for; any header greater than this value will be ignored
+      * bodyPrefix    (string) :  ''    - Anything that should be inserted inside of the heading tag _before_ its anchor and content
+      * bodySuffix    (string) :  ''    - Anything that should be inserted inside of the heading tag _after_ its anchor and content
+      * generateId    (true)   :  false - Set to true if a header without id should generate an id to use.
+
+    Output:
+      The original HTML with the addition of anchors inside of all of the h1-h6 headings.
+  {% endcomment %}
+
+  {% assign minHeader = include.h_min | default: 1 %}
+  {% assign maxHeader = include.h_max | default: 6 %}
+  {% assign beforeHeading = include.beforeHeading %}
+  {% assign headerAttrs = include.headerAttrs %}
+  {% assign nodes = include.html | split: '<h' %}
+
+  {% capture edited_headings %}{% endcapture %}
+
+  {% for _node in nodes %}
+    {% capture node %}{{ _node | strip }}{% endcapture %}
+
+    {% if node == "" %}
+      {% continue %}
+    {% endif %}
+
+    {% assign nextChar = node | replace: '"', '' | strip | slice: 0, 1 %}
+    {% assign headerLevel = nextChar | times: 1 %}
+
+    <!-- If the level is cast to 0, it means it's not a h1-h6 tag, so let's see if we need to fix it -->
+    {% if headerLevel == 0 %}
+      <!-- Split up the node based on closing angle brackets and get the first one. -->
+      {% assign firstChunk = node | split: '>' | first %}
+
+      <!-- If the first chunk does NOT contain a '<', that means we've broken another HTML tag that starts with 'h' -->
+      {% unless firstChunk contains '<' %}
+        {% capture node %}<h{{ node }}{% endcapture %}
+      {% endunless %}
+
+      {% capture edited_headings %}{{ edited_headings }}{{ node }}{% endcapture %}
+      {% continue %}
+    {% endif %}
+
+    {% capture _closingTag %}</h{{ headerLevel }}>{% endcapture %}
+    {% assign _workspace = node | split: _closingTag %}
+    {% capture _hAttrToStrip %}{{ _workspace[0] | split: '>' | first }}>{% endcapture %}
+    {% assign header = _workspace[0] | replace: _hAttrToStrip, '' %}
+    {% assign escaped_header = header | strip_html | strip %}
+
+    {% assign _classWorkspace = _workspace[0] | split: 'class="' %}
+    {% assign _classWorkspace = _classWorkspace[1] | split: '"' %}
+    {% assign _html_class = _classWorkspace[0] %}
+
+    {% if _html_class contains "no_anchor" %}
+      {% assign skip_anchor = true %}
+    {% else %}
+      {% assign skip_anchor = false %}
+    {% endif %}
+
+    {% assign _idWorkspace = _workspace[0] | split: 'id="' %}
+    {% if _idWorkspace[1] %}
+      {% assign _idWorkspace = _idWorkspace[1] | split: '"' %}
+      {% assign html_id = _idWorkspace[0] %}
+      {% assign h_attrs = headerAttrs %}
+    {% elsif include.generateId %}
+      <!-- If the header did not have an id we create one. -->
+      {% assign html_id = escaped_header | slugify %}
+      {% if html_id == "" %}
+        {% assign html_id = false %}
+      {% endif %}
+      <!-- Append the generated id to other potential header attributes. -->
+      {% capture h_attrs %}{{ headerAttrs }} id="%html_id%"{% endcapture %}
+    {% endif %}
+
+    <!-- Build the anchor to inject for our heading -->
+    {% capture anchor %}{% endcapture %}
+
+    {% if skip_anchor == false and html_id and headerLevel >= minHeader and headerLevel <= maxHeader %}
+      {% if h_attrs %}
+        {% capture _hAttrToStrip %}{{ _hAttrToStrip | split: '>' | first }} {{ h_attrs | strip | replace: '%heading%', escaped_header | replace: '%html_id%', html_id }}>{% endcapture %}
+      {% endif %}
+
+      {% capture anchor %}href="#{{ html_id }}"{% endcapture %}
+
+      {% if include.anchorClass %}
+        {% capture anchor %}{{ anchor }} class="{{ include.anchorClass }}"{% endcapture %}
+      {% endif %}
+
+      {% if include.anchorTitle %}
+        {% capture anchor %}{{ anchor }} title="{{ include.anchorTitle | replace: '%heading%', escaped_header }}"{% endcapture %}
+      {% endif %}
+
+      {% if include.anchorAttrs %}
+        {% capture anchor %}{{ anchor }} {{ include.anchorAttrs | replace: '%heading%', escaped_header | replace: '%html_id%', html_id }}{% endcapture %}
+      {% endif %}
+
+      {% capture anchor %}<a {{ anchor }}>{{ include.anchorBody | replace: '%heading%', escaped_header | default: '' }}</a>{% endcapture %}
+
+      <!-- In order to prevent adding extra space after a heading, we'll let the 'anchor' value contain it -->
+      {% if beforeHeading %}
+        {% capture anchor %}{{ anchor }} {% endcapture %}
+      {% else %}
+        {% capture anchor %} {{ anchor }}{% endcapture %}
+      {% endif %}
+    {% endif %}
+
+    {% capture new_heading %}
+<h{{ _hAttrToStrip }}
+  {{ include.bodyPrefix }}
+  {% if beforeHeading %}
+    {{ anchor }}{{ header }}
+  {% else %}
+    {{ header }}{{ anchor }}
+  {% endif %}
+  {{ include.bodySuffix }}
+</h{{ headerLevel }}>
+    {% endcapture %}
+
+    <!--
+    If we have content after the `</hX>` tag, then we'll want to append that here so we don't lost any content.
+    -->
+    {% assign chunkCount = _workspace | size %}
+    {% if chunkCount > 1 %}
+      {% capture new_heading %}{{ new_heading }}{{ _workspace | last }}{% endcapture %}
+    {% endif %}
+
+    {% capture edited_headings %}{{ edited_headings }}{{ new_heading }}{% endcapture %}
+  {% endfor %}
+{% endcapture %}{% assign headingsWorkspace = '' %}{{ edited_headings | strip }}

+ 210 - 0
docs/_includes/docs.css

@@ -0,0 +1,210 @@
+:root {
+	--aside-width: 300px;
+}
+
+@font-face {
+	font-family: 'Open Sans';
+	font-style: normal;
+	font-weight: 300 800;
+	font-stretch: 100%;
+	font-display: swap;
+	src: url('{{ "/assets/fonts/OpenSans.woff2" | relative_url }}') format('woff2');
+	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* layout */
+html, body {
+	overflow-x: hidden;
+}
+
+body {
+	font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
+	line-height: 1.5;
+	min-height: 100vh;
+}
+
+aside {
+	padding: 1rem;
+	width: var(--aside-width);
+	height: 100vh;
+	border-right: 1px solid black;
+	position: fixed;
+	top: 0;
+	left: 0;
+	margin-right: 1rem;
+}
+
+aside > nav.docs {
+	overflow-y: scroll;
+	max-height: 90vh;
+}
+
+aside > nav.docs > ul {
+	margin-bottom: 10rem; /* extra scroll space */
+}
+
+aside > nav.docs ul {
+	padding-left: 1rem;
+	margin-right: 1rem;
+}
+
+main {
+	margin-left: calc(var(--aside-width) + 50px);
+	max-width: 70vw;
+}
+
+img {
+	max-width: 100%;
+}
+
+section.search {
+	margin-top: 1rem;
+	border-bottom: 1px solid black;
+}
+
+div.nojs-search {
+	margin-bottom: 1rem;
+}
+
+nav.mobile-nav {
+	display: none;
+}
+
+nav.lang-dropdown {
+	display: none;
+}
+
+div.lang-dropdown:target nav {
+	padding: 0.2rem;
+	display: block;
+	border: 1px solid black;
+	border-radius: 6px;
+	position: absolute;
+	background-color: white;
+	z-index: 100;
+	text-wrap: nowrap;
+}
+
+div#mobile-language:target nav {
+	right: 0.2rem;
+}
+
+div.lang-dropdown:target nav a:hover {
+	background-color: silver;
+}
+
+div.lang-dropdown:target a.close {
+	display: block;
+	cursor: default;
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100vw;
+	height: 100vh;
+	z-index: 99; /* just below language dropdown */
+}
+
+nav.lang-dropdown ul {
+	margin: 0;
+	padding: 0;
+	list-style-type: none;
+}
+
+/* mobile layout */
+
+@media (max-width: 1200px) {
+	aside {
+		display: none;
+	}
+
+	aside a.close {
+		display: block;
+	}
+
+	aside a.lang-btn {
+		display: none;
+	}
+
+	html:has(aside:target) {
+		overflow-y: hidden;
+	}
+
+	body:has(aside:target) > a.close {
+		display: block;
+		cursor: default;
+		position: fixed;
+		top: 0;
+		background-color: black;
+		opacity: 0.3;
+		width: 100vw;
+		height: 100vh;
+		z-index: 99; /* just below aside */
+	}
+
+	aside:target {
+		display: block;
+		position: fixed;
+		background-color: white;
+		z-index: 100;
+	}
+
+	main {
+		margin-left: 1rem;
+		max-width: 90vw;
+	}
+
+	nav.mobile-nav {
+		display: block;
+		border-bottom: 1px dashed black;
+	}
+
+	nav.mobile-nav > a.toggle-aside {
+		color: black;
+		-webkit-tap-highlight-color: transparent;
+	}
+
+	nav.mobile-nav > a.toggle-aside svg {
+		width: 2rem;
+		height: 2rem;
+	}
+}
+
+/* general styling */
+a {
+	text-decoration: none;
+	color: #00e;
+}
+
+a.close {
+	display: none;
+}
+
+aside a.close {
+	float: right;
+	color: #f00;
+	font-size: 24px;
+	position: relative;
+	bottom: 4px;
+}
+
+table, th, tr, td {
+	border: 1px solid black;
+	border-collapse: collapse;
+}
+
+th, tr, td {
+	padding: 0.3rem;
+}
+
+button.copy {
+	margin: 0.5rem;
+	float: right;
+}
+
+div.lang-dropdown {
+	float: right;
+}
+
+a.lang-btn {
+	color: black;
+}

+ 77 - 0
docs/_includes/docs_nav.html

@@ -0,0 +1,77 @@
+{% if page.lang == 'en' %}
+	<ul>
+		<li><a href="{{ '/en/' | relative_url }}">Home</a></li>
+		<li>
+			<a href="{{ '/en/users/02_First_steps.html' | relative_url }}">User documentation</a>
+			<ul>
+				{% for page in site.pages %}
+					{% if page.url contains '/en/users/' and page.title != nil and page.url != "/en/users/02_First_steps.html" %}
+						<li><a href="{{ page.url | relative_url }}">{{ page.title | escape }}</a></li>
+					{% endif %}
+				{% endfor %}
+			</ul>
+		</li>
+		<li>
+			<a href="{{ '/en/admins/01_Index.html' | relative_url }}">Administrator documentation</a>
+			<ul>
+				{% for page in site.pages %}
+					{% if page.url contains '/en/admins/' and page.url != "/en/admins/01_Index.html" and page.title != nil %}
+						<li><a href="{{ page.url | relative_url }}">{{ page.title | escape }}</a></li>
+					{% endif %}
+				{% endfor %}
+			</ul>
+		</li>
+		<li>
+			<a href="{{ '/en/developers/01_Index.html' | relative_url }}">Developer documentation</a>
+			<ul>
+				{% for page in site.pages %}
+					{% if page.url contains '/en/developers/' and page.url != "/en/developers/01_Index.html" and page.title != nil %}
+						<li><a href="{{ page.url | relative_url }}">{{ page.title | escape }}</a></li>
+					{% endif %}
+				{% endfor %}
+			</ul>
+		</li>
+		<li>
+			<a href="{{ '/en/contributing.html' | relative_url}}">Contributor guidelines</a>
+		</li>
+	</ul>
+{% elsif page.lang == 'fr' %}
+	<ul>
+		<li><a href="{{ '/fr/' | relative_url }}">Accueil</a></li>
+		<li>
+			<a href="{{ '/fr/users/02_First_steps.html' | relative_url }}">Documentation utilisateur</a>
+			<ul>
+				{% for page in site.pages %}
+					{% if page.url contains '/fr/users/' and page.title != nil and page.url != "/fr/users/02_First_steps.html" %}
+						<li><a href="{{ page.url | relative_url }}">{{ page.title | escape }}</a></li>
+					{% endif %}
+				{% endfor %}
+			</ul>
+		</li>
+		<!-- TODO: French doesn't have admin docs -->
+		<!-- <li> -->
+			<!-- <a href="{{ '/fr/admins/01_Index.html' | relative_url }}">Administrator documentation</a> -->
+			<!-- <ul> -->
+				<!-- {% for page in site.pages %} -->
+					<!-- {% if page.url contains '/fr/admins/' and page.url != "/fr/admins/01_Index.html" and page.title != nil %} -->
+						<!-- <li><a href="{{ page.url | relative_url }}">{{ page.title | escape }}</a></li> -->
+					<!-- {% endif %} -->
+				<!-- {% endfor %} -->
+			<!-- </ul> -->
+		<!-- </li> -->
+		<li>
+			<a href="{{ '/fr/developers/01_First_steps.html' | relative_url }}">Documentation développeur</a>
+			<ul>
+				{% for page in site.pages %}
+					{% if page.url contains '/fr/developers/' and page.url != "/fr/developers/01_First_steps.html" and page.title != nil %}
+						<li><a href="{{ page.url | relative_url }}">{{ page.title | escape }}</a></li>
+					{% endif %}
+				{% endfor %}
+			</ul>
+		</li>
+		<li>
+			<a href="{{ '/fr/contributing.html' | relative_url}}">Directives pour les contributeurs</a>
+		</li>
+	</ul>
+{% endif %}
+

+ 15 - 0
docs/_includes/lang_dropdown.html

@@ -0,0 +1,15 @@
+<div class="lang-dropdown" id="{{ include.location }}-language">
+	<a class="lang-btn" href="#{{ include.location }}-language" title="{%t choose_language %}">
+		<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 16 16">
+				<path d="M4.545 6.714 4.11 8H3l1.862-5h1.284L8 8H6.833l-.435-1.286zm1.634-.736L5.5 3.956h-.049l-.679 2.022z"></path>
+				<path d="M0 2a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v3h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-3H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zm7.138 9.995q.289.451.63.846c-.748.575-1.673 1.001-2.768 1.292.178.217.451.635.555.867 1.125-.359 2.08-.844 2.886-1.494.777.665 1.739 1.165 2.93 1.472.133-.254.414-.673.629-.89-1.125-.253-2.057-.694-2.82-1.284.681-.747 1.222-1.651 1.621-2.757H14V8h-3v1.047h.765c-.318.844-.74 1.546-1.272 2.13a6 6 0 0 1-.415-.492 2 2 0 0 1-.94.31"></path>
+			</svg>
+	</a>
+	<a class="close" href="#close"></a>
+	<nav class="lang-dropdown">
+		<ul>
+			<li><a href="{{ '/en/' | relative_url }}">English (en)</a></li>
+			<li><a href="{{ '/fr/' | relative_url }}">Français (fr)</a></li>
+		</ul>
+	</nav>
+</div>

+ 78 - 44
docs/_layouts/default.html

@@ -1,50 +1,84 @@
 <!DOCTYPE html>
 <!DOCTYPE html>
-<html lang="{{ site.lang | default: "en-US" }}">
+<html lang="{{ page.lang | default: 'en' }}">
   <head>
   <head>
-    <meta charset="UTF-8">
-    <title>{{ page.title | default: site.title }}</title>
-    <meta name="description" content="{{ page.description | default: site.description | default: site.github.project_tagline }}"/>
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <meta name="theme-color" content="#157878">
-    <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'>
-    <link rel="stylesheet" href="{{ '/assets/css/style.css?v=' | append: site.github.build_revision | relative_url }}">
+		<meta charset="UTF-8">
+		<title>{{ page.title | default: site.title }} · FreshRSS</title>
+		<meta name="description" content="{{ page.description | default: site.description | default: site.github.project_tagline }}">
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' data:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self'; connect-src 'self'">
+
+		<link rel="stylesheet" href="{{ '/assets/css/docs.css?v=' | append: site.github.build_revision | relative_url }}">
+		<link rel="stylesheet" href="{{ '/assets/css/highlight.css?v=' | append: site.github.build_revision | relative_url }}">
+		<link rel="stylesheet" href="{{ '/assets/css/normalize.css?v=' | append: site.github.build_revision | relative_url }}">
+		<link rel="stylesheet" media="(prefers-color-scheme: dark)" href="{{ '/assets/css/darkmode.css?v=' | append: site.github.build_revision | relative_url}}">
+
+		<link rel="icon" href="{{ '/favicon.ico' | relative_url }}">
+
+		<script>
+			var i18n = {
+				"copy_to_clipboard": "{%t copy_to_clipboard %}"
+			};
+		</script>
+		<script src="{{ '/assets/js/docs.js?v=' | append: site.github.build_revision | relative_url }}"></script>
   </head>
   </head>
   <body>
   <body>
-    <section class="page-header">
-      <h1 class="project-name">
-        <a href="{{ site.github.url }}">{{ site.title | default: site.github.repository_name }}</a>
-      </h1>
-      <h2 class="project-tagline">{{ site.description | default: site.github.project_tagline }}</h2>
-      {% if site.github.is_project_page %}
-        <a href="{{ site.github.repository_url }}" class="btn">View on GitHub</a>
-      {% endif %}
-      {% if site.show_downloads %}
-        <a href="{{ site.github.zip_url }}" class="btn">Download .zip</a>
-        <a href="{{ site.github.tar_url }}" class="btn">Download .tar.gz</a>
-      {% endif %}
-    </section>
-
-    <section class="main-content">
-      {{ content }}
-
-      <footer class="site-footer">
-        {% if site.github.is_project_page %}
-          <span class="site-footer-owner"><a href="{{ site.github.repository_url }}">{{ site.github.repository_name }}</a> is maintained by <a href="{{ site.github.owner_url }}">{{ site.github.owner_name }}</a>.</span>
-        {% endif %}
-        <span class="site-footer-credits">This page was generated by <a href="https://pages.github.com">GitHub Pages</a>.</span>
-      </footer>
-    </section>
-
-    {% if site.google_analytics %}
-      <script type="text/javascript">
-        (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
-        (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
-        m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
-        })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
-
-        ga('create', '{{ site.google_analytics }}', 'auto');
-        ga('send', 'pageview');
-      </script>
-    {% endif %}
+		<nav class="mobile-nav">
+			<a class="toggle-aside" href="#aside" title="{%t toggle_aside %}">
+				<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
+  				<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5"/>
+				</svg>
+			</a>
+			{% include lang_dropdown.html location="mobile" %}
+		</nav>
+
+		<a class="close" href="#close"></a>
+
+		<aside id="aside">
+			<a href="https://freshrss.org/">&lt; {%t back_to_freshrss %}</a>
+			{% include lang_dropdown.html location="aside" %}
+			<a class="close" href="#close" title="{%t close %}">&times;</a>
+
+			<section class="search">
+				<noscript>
+					<style>div.js-search { display: none; }</style>
+					<div class="nojs-search">
+						<form action="https://duckduckgo.com/" method="get">
+							<input type="search" name="q" placeholder="{%t search_docs %}">
+							<input type="hidden" name="sites" value="freshrss.github.io">
+							<button type="submit">{%t search %}</button>
+						</form>
+					</div>
+				</noscript>
+
+				<div class="js-search">
+					<input type="text" id="search-input" placeholder="{%t search_docs %}">
+					<ul id="results-container"></ul>
+
+					<script src="{{ '/assets/js/simple-jekyll-search.min.js?v=' | append: site.github.build_revision | relative_url }}"></script>
+					<script>
+						const search = document.querySelector('#search-input');
+
+						function init_search() {
+							search.removeEventListener('focus', init_search);
+							SimpleJekyllSearch({
+								searchInput: search,
+								resultsContainer: document.querySelector('#results-container'),
+								json: '{{ "/search." | append: page.lang | append: ".json?v=" | append: site.github.build_revision | relative_url }}',
+								searchResultTemplate: '<li><a href="{url}">{title}</a></li>'
+							});
+						}
+
+						search.addEventListener('focus', init_search);
+					</script>
+				</div>
+			</section>
+			<nav class="docs">
+				{% include docs_nav.html %}
+			</nav>
+		</aside>
+
+    <main>
+			{% include anchor_headings.html html=content anchorBody="#" %}
+    </main>
   </body>
   </body>
 </html>
 </html>

+ 620 - 0
docs/assets/css/darkmode.css

@@ -0,0 +1,620 @@
+/* stylelint-disable */
+/*! Dark reader generated CSS | Licensed under MIT https://github.com/darkreader/darkreader/blob/main/LICENSE */
+
+/* User-Agent Style */
+@layer {
+html {
+    background-color: var(--darkreader-background-ffffff, #181a1b) !important;
+}
+html {
+    color-scheme: dark !important;
+}
+iframe {
+    color-scheme: dark !important;
+}
+html, body {
+    background-color: var(--darkreader-background-ffffff, #181a1b);
+}
+html, body {
+    border-color: var(--darkreader-border-4c4c4c, #736b5e);
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+a {
+    color: var(--darkreader-text-0040ff, #3391ff);
+}
+table {
+    border-color: var(--darkreader-border-808080, #545b5e);
+}
+mark {
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+::placeholder {
+    color: var(--darkreader-text-a9a9a9, #b2aba1);
+}
+input:-webkit-autofill,
+textarea:-webkit-autofill,
+select:-webkit-autofill {
+    background-color: var(--darkreader-background-faffbd, #404400) !important;
+    color: var(--darkreader-text-000000, #e8e6e3) !important;
+}
+::selection {
+    background-color: var(--darkreader-background-0060d4, #004daa) !important;
+    color: var(--darkreader-text-ffffff, #e8e6e3) !important;
+}
+::-moz-selection {
+    background-color: var(--darkreader-background-0060d4, #004daa) !important;
+    color: var(--darkreader-text-ffffff, #e8e6e3) !important;
+}
+}
+
+/* Variables Style */
+:root {
+   --darkreader-neutral-background: var(--darkreader-background-ffffff, #181a1b);
+   --darkreader-neutral-text: var(--darkreader-text-000000, #e8e6e3);
+   --darkreader-selection-background: var(--darkreader-background-0060d4, #004daa);
+   --darkreader-selection-text: var(--darkreader-text-ffffff, #e8e6e3);
+}
+
+/* Modified CSS */
+:root {
+    --aside-width: 300px;
+}
+aside {
+    border-right-color: var(--darkreader-border-000000, #8c8273);
+}
+section.search {
+    border-bottom-color: var(--darkreader-border-000000, #8c8273);
+}
+div.lang-dropdown:target nav {
+    background-color: var(--darkreader-background-ffffff, #181a1b);
+    border-bottom-color: var(--darkreader-border-000000, #8c8273);
+    border-left-color: var(--darkreader-border-000000, #8c8273);
+    border-right-color: var(--darkreader-border-000000, #8c8273);
+    border-top-color: var(--darkreader-border-000000, #8c8273);
+}
+div.lang-dropdown:target nav a:hover {
+    background-color: var(--darkreader-background-c0c0c0, #3c4143);
+}
+@media (max-width: 1200px) {
+    body:has(aside:target) > a.close {
+        background-color: var(--darkreader-background-000000, #000000);
+    }
+    aside:target {
+        background-color: var(--darkreader-background-ffffff, #181a1b);
+    }
+    nav.mobile-nav {
+        border-bottom-color: var(--darkreader-border-000000, #8c8273);
+    }
+    nav.mobile-nav > a.toggle-aside {
+        color: var(--darkreader-text-000000, #e8e6e3);
+    }
+}
+a {
+    color: var(--darkreader-text-0000ee, #3d84ff);
+    text-decoration-color: currentcolor;
+}
+aside a.close {
+    color: var(--darkreader-text-ff0000, #ff1a1a);
+}
+table,
+th,
+tr,
+td {
+    border-bottom-color: var(--darkreader-border-000000, #8c8273);
+    border-left-color: var(--darkreader-border-000000, #8c8273);
+    border-right-color: var(--darkreader-border-000000, #8c8273);
+    border-top-color: var(--darkreader-border-000000, #8c8273);
+}
+a.lang-btn {
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+.highlight .cm {
+    color: var(--darkreader-text-999988, #a29a8e);
+}
+.highlight .cp {
+    color: var(--darkreader-text-999999, #a8a095);
+}
+.highlight .c1 {
+    color: var(--darkreader-text-999988, #a29a8e);
+}
+.highlight .cs {
+    color: var(--darkreader-text-999999, #a8a095);
+}
+.highlight .c,
+.highlight .ch,
+.highlight .cd,
+.highlight .cpf {
+    color: var(--darkreader-text-999988, #a29a8e);
+}
+.highlight .err {
+    background-color: var(--darkreader-background-e3d2d2, #3a2424);
+    color: var(--darkreader-text-a61717, #e95e5e);
+}
+.highlight .gd {
+    background-color: var(--darkreader-background-ffdddd, #470000);
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+.highlight .ge {
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+.highlight .gr {
+    color: var(--darkreader-text-aa0000, #ff5555);
+}
+.highlight .gh {
+    color: var(--darkreader-text-999999, #a8a095);
+}
+.highlight .gi {
+    background-color: var(--darkreader-background-ddffdd, #124700);
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+.highlight .go {
+    color: var(--darkreader-text-888888, #9d9488);
+}
+.highlight .gp {
+    color: var(--darkreader-text-555555, #b2aca2);
+}
+.highlight .gu {
+    color: var(--darkreader-text-aaaaaa, #b2aca2);
+}
+.highlight .gt {
+    color: var(--darkreader-text-aa0000, #ff5555);
+}
+.highlight .kc {
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+.highlight .kd {
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+.highlight .kn {
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+.highlight .kp {
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+.highlight .kr {
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+.highlight .kt {
+    color: var(--darkreader-text-445588, #8ba6c5);
+}
+.highlight .k,
+.highlight .kv {
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+.highlight .mf {
+    color: var(--darkreader-text-009999, #61ffff);
+}
+.highlight .mh {
+    color: var(--darkreader-text-009999, #61ffff);
+}
+.highlight .il {
+    color: var(--darkreader-text-009999, #61ffff);
+}
+.highlight .mi {
+    color: var(--darkreader-text-009999, #61ffff);
+}
+.highlight .mo {
+    color: var(--darkreader-text-009999, #61ffff);
+}
+.highlight .m,
+.highlight .mb,
+.highlight .mx {
+    color: var(--darkreader-text-009999, #61ffff);
+}
+.highlight .sa {
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+.highlight .sb {
+    color: var(--darkreader-text-dd1144, #ef3564);
+}
+.highlight .sc {
+    color: var(--darkreader-text-dd1144, #ef3564);
+}
+.highlight .sd {
+    color: var(--darkreader-text-dd1144, #ef3564);
+}
+.highlight .s2 {
+    color: var(--darkreader-text-dd1144, #ef3564);
+}
+.highlight .se {
+    color: var(--darkreader-text-dd1144, #ef3564);
+}
+.highlight .sh {
+    color: var(--darkreader-text-dd1144, #ef3564);
+}
+.highlight .si {
+    color: var(--darkreader-text-dd1144, #ef3564);
+}
+.highlight .sx {
+    color: var(--darkreader-text-dd1144, #ef3564);
+}
+.highlight .sr {
+    color: var(--darkreader-text-009926, #61ff88);
+}
+.highlight .s1 {
+    color: var(--darkreader-text-dd1144, #ef3564);
+}
+.highlight .ss {
+    color: var(--darkreader-text-990073, #ff61d8);
+}
+.highlight .s,
+.highlight .dl {
+    color: var(--darkreader-text-dd1144, #ef3564);
+}
+.highlight .na {
+    color: var(--darkreader-text-008080, #72ffff);
+}
+.highlight .bp {
+    color: var(--darkreader-text-999999, #a8a095);
+}
+.highlight .nb {
+    color: var(--darkreader-text-0086b3, #4fd3ff);
+}
+.highlight .nc {
+    color: var(--darkreader-text-445588, #8ba6c5);
+}
+.highlight .no {
+    color: var(--darkreader-text-008080, #72ffff);
+}
+.highlight .nd {
+    color: var(--darkreader-text-3c5d5d, #b8b2a8);
+}
+.highlight .ni {
+    color: var(--darkreader-text-800080, #ff72ff);
+}
+.highlight .ne {
+    color: var(--darkreader-text-990000, #ff6161);
+}
+.highlight .nf,
+.highlight .fm {
+    color: var(--darkreader-text-990000, #ff6161);
+}
+.highlight .nl {
+    color: var(--darkreader-text-990000, #ff6161);
+}
+.highlight .nn {
+    color: var(--darkreader-text-555555, #b2aca2);
+}
+.highlight .nt {
+    color: var(--darkreader-text-000080, #7faeff);
+}
+.highlight .vc {
+    color: var(--darkreader-text-008080, #72ffff);
+}
+.highlight .vg {
+    color: var(--darkreader-text-008080, #72ffff);
+}
+.highlight .vi {
+    color: var(--darkreader-text-008080, #72ffff);
+}
+.highlight .nv,
+.highlight .vm {
+    color: var(--darkreader-text-008080, #72ffff);
+}
+.highlight .ow {
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+.highlight .o {
+    color: var(--darkreader-text-000000, #e8e6e3);
+}
+.highlight .w {
+    color: var(--darkreader-text-bbbbbb, #bdb7af);
+}
+.highlight {
+    background-color: var(--darkreader-background-f8f8f8, #1c1e1f);
+}
+a {
+    background-color: transparent;
+}
+abbr[title] {
+    border-bottom-color: currentcolor;
+    text-decoration-color: currentcolor;
+}
+button:focus-visible,
+[type="button"]:focus-visible,
+[type="reset"]:focus-visible,
+[type="submit"]:focus-visible {
+    outline-color: var(--darkreader-border-000000, #8c8273);
+}
+legend {
+    color: inherit;
+}
+@layer  {
+    html {
+        background-color: var(--darkreader-bg--darkreader-background-ffffff, var(--darkreader-background-181a1b, #131516)) !important;
+    }
+    html {
+        color-scheme: dark !important;
+    }
+    iframe {
+        color-scheme: dark !important;
+    }
+    html,
+    body {
+        background-color: var(--darkreader-bg--darkreader-background-ffffff, var(--darkreader-background-181a1b, #131516));
+    }
+    html,
+    body {
+        border-color: var(--darkreader-border--darkreader-border-4c4c4c, var(--darkreader-border-736b5e, #6a6257));
+        color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf));
+    }
+    a {
+        color: var(--darkreader-text--darkreader-text-0040ff, var(--darkreader-text-3391ff, #3da5ff));
+    }
+    table {
+        border-color: var(--darkreader-border--darkreader-border-808080, var(--darkreader-border-545b5e, #6f675b));
+    }
+    mark {
+        color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf));
+    }
+    ::placeholder {
+        color: var(--darkreader-text--darkreader-text-a9a9a9, var(--darkreader-text-b2aba1, #b2aba1));
+    }
+    input:autofill,
+    textarea:autofill,
+    select:autofill {
+        background-color: var(--darkreader-bg--darkreader-background-faffbd, var(--darkreader-background-404400, #333600)) !important;
+        color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf)) !important;
+    }
+    ::selection {
+        background-color: var(--darkreader-bg--darkreader-background-0060d4, var(--darkreader-background-004daa, #003e88)) !important;
+        color: var(--darkreader-text--darkreader-text-ffffff, var(--darkreader-text-e8e6e3, #d8d4cf)) !important;
+    }
+    ::selection {
+        background-color: var(--darkreader-bg--darkreader-background-0060d4, var(--darkreader-background-004daa, #003e88)) !important;
+        color: var(--darkreader-text--darkreader-text-ffffff, var(--darkreader-text-e8e6e3, #d8d4cf)) !important;
+    }
+}
+:root {
+    --darkreader-neutral-background: var(--darkreader-background-ffffff, #181a1b);
+    --darkreader-neutral-text: var(--darkreader-text-000000, #e8e6e3);
+    --darkreader-selection-background: var(--darkreader-background-0060d4, #004daa);
+    --darkreader-selection-text: var(--darkreader-text-ffffff, #e8e6e3);
+}
+:root {
+    --aside-width: 300px;
+}
+aside {
+    border-right-color: var(--darkreader-border--darkreader-border-000000, var(--darkreader-border-8c8273, #545b5f));
+}
+section.search {
+    border-bottom-color: var(--darkreader-border--darkreader-border-000000, var(--darkreader-border-8c8273, #545b5f));
+}
+@media (max-width: 1200px) {
+    body:has(aside:target) > a#close {
+        background-color: var(--darkreader-bg--darkreader-background-000000, var(--darkreader-background-000000, #000000));
+    }
+    aside:target {
+        background-color: var(--darkreader-bg--darkreader-background-ffffff, var(--darkreader-background-181a1b, #131516));
+    }
+    nav.mobile-nav {
+        border-bottom-color: var(--darkreader-border--darkreader-border-000000, var(--darkreader-border-8c8273, #545b5f));
+    }
+}
+a {
+    color: var(--darkreader-text--darkreader-text-0000ee, var(--darkreader-text-3d84ff, #44a2ff));
+    text-decoration-color: currentcolor;
+}
+aside a#close {
+    color: var(--darkreader-text--darkreader-text-ff0000, var(--darkreader-text-ff1a1a, #ff2c2c));
+}
+table,
+th,
+tr,
+td {
+    border-bottom-color: var(--darkreader-border--darkreader-border-000000, var(--darkreader-border-8c8273, #545b5f));
+    border-left-color: var(--darkreader-border--darkreader-border-000000, var(--darkreader-border-8c8273, #545b5f));
+    border-right-color: var(--darkreader-border--darkreader-border-000000, var(--darkreader-border-8c8273, #545b5f));
+    border-top-color: var(--darkreader-border--darkreader-border-000000, var(--darkreader-border-8c8273, #545b5f));
+}
+.highlight .cm {
+    color: var(--darkreader-text--darkreader-text-999988, var(--darkreader-text-a29a8e, #a79f94));
+}
+.highlight .cp {
+    color: var(--darkreader-text--darkreader-text-999999, var(--darkreader-text-a8a095, #aba499));
+}
+.highlight .c1 {
+    color: var(--darkreader-text--darkreader-text-999988, var(--darkreader-text-a29a8e, #a79f94));
+}
+.highlight .cs {
+    color: var(--darkreader-text--darkreader-text-999999, var(--darkreader-text-a8a095, #aba499));
+}
+.highlight .c,
+.highlight .ch,
+.highlight .cd,
+.highlight .cpf {
+    color: var(--darkreader-text--darkreader-text-999988, var(--darkreader-text-a29a8e, #a79f94));
+}
+.highlight .err {
+    background-color: var(--darkreader-bg--darkreader-background-e3d2d2, var(--darkreader-background-3a2424, #2e1d1d));
+    color: var(--darkreader-text--darkreader-text-a61717, var(--darkreader-text-e95e5e, #e96161));
+}
+.highlight .gd {
+    background-color: var(--darkreader-bg--darkreader-background-ffdddd, var(--darkreader-background-470000, #390000));
+    color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf));
+}
+.highlight .ge {
+    color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf));
+}
+.highlight .gr {
+    color: var(--darkreader-text--darkreader-text-aa0000, var(--darkreader-text-ff5555, #ff5555));
+}
+.highlight .gh {
+    color: var(--darkreader-text--darkreader-text-999999, var(--darkreader-text-a8a095, #aba499));
+}
+.highlight .gi {
+    background-color: var(--darkreader-bg--darkreader-background-ddffdd, var(--darkreader-background-124700, #0e3900));
+    color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf));
+}
+.highlight .go {
+    color: var(--darkreader-text--darkreader-text-888888, var(--darkreader-text-9d9488, #a39c90));
+}
+.highlight .gp {
+    color: var(--darkreader-text--darkreader-text-555555, var(--darkreader-text-b2aca2, #b2aca2));
+}
+.highlight .gu {
+    color: var(--darkreader-text--darkreader-text-aaaaaa, var(--darkreader-text-b2aca2, #b2aca2));
+}
+.highlight .gt {
+    color: var(--darkreader-text--darkreader-text-aa0000, var(--darkreader-text-ff5555, #ff5555));
+}
+.highlight .kc {
+    color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf));
+}
+.highlight .kd {
+    color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf));
+}
+.highlight .kn {
+    color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf));
+}
+.highlight .kp {
+    color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf));
+}
+.highlight .kr {
+    color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf));
+}
+.highlight .kt {
+    color: var(--darkreader-text--darkreader-text-445588, var(--darkreader-text-8ba6c5, #8cabc5));
+}
+.highlight .k,
+.highlight .kv {
+    color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf));
+}
+.highlight .mf {
+    color: var(--darkreader-text--darkreader-text-009999, var(--darkreader-text-61ffff, #5dffff));
+}
+.highlight .mh {
+    color: var(--darkreader-text--darkreader-text-009999, var(--darkreader-text-61ffff, #5dffff));
+}
+.highlight .il {
+    color: var(--darkreader-text--darkreader-text-009999, var(--darkreader-text-61ffff, #5dffff));
+}
+.highlight .mi {
+    color: var(--darkreader-text--darkreader-text-009999, var(--darkreader-text-61ffff, #5dffff));
+}
+.highlight .mo {
+    color: var(--darkreader-text--darkreader-text-009999, var(--darkreader-text-61ffff, #5dffff));
+}
+.highlight .m,
+.highlight .mb,
+.highlight .mx {
+    color: var(--darkreader-text--darkreader-text-009999, var(--darkreader-text-61ffff, #5dffff));
+}
+.highlight .sa {
+    color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf));
+}
+.highlight .sb {
+    color: var(--darkreader-text--darkreader-text-dd1144, var(--darkreader-text-ef3564, #f0426e));
+}
+.highlight .sc {
+    color: var(--darkreader-text--darkreader-text-dd1144, var(--darkreader-text-ef3564, #f0426e));
+}
+.highlight .sd {
+    color: var(--darkreader-text--darkreader-text-dd1144, var(--darkreader-text-ef3564, #f0426e));
+}
+.highlight .s2 {
+    color: var(--darkreader-text--darkreader-text-dd1144, var(--darkreader-text-ef3564, #f0426e));
+}
+.highlight .se {
+    color: var(--darkreader-text--darkreader-text-dd1144, var(--darkreader-text-ef3564, #f0426e));
+}
+.highlight .sh {
+    color: var(--darkreader-text--darkreader-text-dd1144, var(--darkreader-text-ef3564, #f0426e));
+}
+.highlight .si {
+    color: var(--darkreader-text--darkreader-text-dd1144, var(--darkreader-text-ef3564, #f0426e));
+}
+.highlight .sx {
+    color: var(--darkreader-text--darkreader-text-dd1144, var(--darkreader-text-ef3564, #f0426e));
+}
+.highlight .sr {
+    color: var(--darkreader-text--darkreader-text-009926, var(--darkreader-text-61ff88, #5dff85));
+}
+.highlight .s1 {
+    color: var(--darkreader-text--darkreader-text-dd1144, var(--darkreader-text-ef3564, #f0426e));
+}
+.highlight .ss {
+    color: var(--darkreader-text--darkreader-text-990073, var(--darkreader-text-ff61d8, #ff5dd7));
+}
+.highlight .s,
+.highlight .dl {
+    color: var(--darkreader-text--darkreader-text-dd1144, var(--darkreader-text-ef3564, #f0426e));
+}
+.highlight .na {
+    color: var(--darkreader-text--darkreader-text-008080, var(--darkreader-text-72ffff, #69ffff));
+}
+.highlight .bp {
+    color: var(--darkreader-text--darkreader-text-999999, var(--darkreader-text-a8a095, #aba499));
+}
+.highlight .nb {
+    color: var(--darkreader-text--darkreader-text-0086b3, var(--darkreader-text-4fd3ff, #51d3ff));
+}
+.highlight .nc {
+    color: var(--darkreader-text--darkreader-text-445588, var(--darkreader-text-8ba6c5, #8cabc5));
+}
+.highlight .no {
+    color: var(--darkreader-text--darkreader-text-008080, var(--darkreader-text-72ffff, #69ffff));
+}
+.highlight .nd {
+    color: var(--darkreader-text--darkreader-text-3c5d5d, var(--darkreader-text-b8b2a8, #b6b0a6));
+}
+.highlight .ni {
+    color: var(--darkreader-text--darkreader-text-800080, var(--darkreader-text-ff72ff, #ff69ff));
+}
+.highlight .ne {
+    color: var(--darkreader-text--darkreader-text-990000, var(--darkreader-text-ff6161, #ff5d5d));
+}
+.highlight .nf,
+.highlight .fm {
+    color: var(--darkreader-text--darkreader-text-990000, var(--darkreader-text-ff6161, #ff5d5d));
+}
+.highlight .nl {
+    color: var(--darkreader-text--darkreader-text-990000, var(--darkreader-text-ff6161, #ff5d5d));
+}
+.highlight .nn {
+    color: var(--darkreader-text--darkreader-text-555555, var(--darkreader-text-b2aca2, #b2aca2));
+}
+.highlight .nt {
+    color: var(--darkreader-text--darkreader-text-000080, var(--darkreader-text-7faeff, #72b9ff));
+}
+.highlight .vc {
+    color: var(--darkreader-text--darkreader-text-008080, var(--darkreader-text-72ffff, #69ffff));
+}
+.highlight .vg {
+    color: var(--darkreader-text--darkreader-text-008080, var(--darkreader-text-72ffff, #69ffff));
+}
+.highlight .vi {
+    color: var(--darkreader-text--darkreader-text-008080, var(--darkreader-text-72ffff, #69ffff));
+}
+.highlight .nv,
+.highlight .vm {
+    color: var(--darkreader-text--darkreader-text-008080, var(--darkreader-text-72ffff, #69ffff));
+}
+.highlight .ow {
+    color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf));
+}
+.highlight .o {
+    color: var(--darkreader-text--darkreader-text-000000, var(--darkreader-text-e8e6e3, #d8d4cf));
+}
+.highlight .w {
+    color: var(--darkreader-text--darkreader-text-bbbbbb, var(--darkreader-text-bdb7af, #bab4ab));
+}
+.highlight {
+    background-color: var(--darkreader-bg--darkreader-background-f8f8f8, var(--darkreader-background-1c1e1f, #161819));
+}
+a {
+    background-color: transparent;
+}
+abbr[title] {
+    border-bottom-color: currentcolor;
+    text-decoration-color: currentcolor;
+}
+button:focus-visible,
+[type="button"]:focus-visible,
+[type="reset"]:focus-visible,
+[type="submit"]:focus-visible {
+    outline-color: var(--darkreader-border--darkreader-border-000000, var(--darkreader-border-8c8273, #545b5f));
+}
+legend {
+    color: inherit;
+}

+ 3 - 0
docs/assets/css/docs.css

@@ -0,0 +1,3 @@
+---
+---
+{% include docs.css %}

+ 282 - 0
docs/assets/css/highlight.css

@@ -0,0 +1,282 @@
+pre.highlight {
+	padding: 1rem;
+	overflow-x: scroll;
+}
+
+/* generated with `rougify style github` */
+.highlight table td { padding: 5px; }
+
+.highlight table pre { margin: 0; }
+
+.highlight .cm {
+	color: #998;
+	font-style: italic;
+}
+
+.highlight .cp {
+	color: #999;
+	font-weight: bold;
+}
+
+.highlight .c1 {
+	color: #998;
+	font-style: italic;
+}
+
+.highlight .cs {
+	color: #999;
+	font-weight: bold;
+	font-style: italic;
+}
+
+.highlight .c, .highlight .ch, .highlight .cd, .highlight .cpf {
+	color: #998;
+	font-style: italic;
+}
+
+.highlight .err {
+	color: #a61717;
+	background-color: #e3d2d2;
+}
+
+.highlight .gd {
+	color: #000;
+	background-color: #fdd;
+}
+
+.highlight .ge {
+	color: #000;
+	font-style: italic;
+}
+
+.highlight .gr {
+	color: #a00;
+}
+
+.highlight .gh {
+	color: #999;
+}
+
+.highlight .gi {
+	color: #000;
+	background-color: #dfd;
+}
+
+.highlight .go {
+	color: #888;
+}
+
+.highlight .gp {
+	color: #555;
+}
+
+.highlight .gs {
+	font-weight: bold;
+}
+
+.highlight .gu {
+	color: #aaa;
+}
+
+.highlight .gt {
+	color: #a00;
+}
+
+.highlight .kc {
+	color: #000;
+	font-weight: bold;
+}
+
+.highlight .kd {
+	color: #000;
+	font-weight: bold;
+}
+
+.highlight .kn {
+	color: #000;
+	font-weight: bold;
+}
+
+.highlight .kp {
+	color: #000;
+	font-weight: bold;
+}
+
+.highlight .kr {
+	color: #000;
+	font-weight: bold;
+}
+
+.highlight .kt {
+	color: #458;
+	font-weight: bold;
+}
+
+.highlight .k, .highlight .kv {
+	color: #000;
+	font-weight: bold;
+}
+
+.highlight .mf {
+	color: #099;
+}
+
+.highlight .mh {
+	color: #099;
+}
+
+.highlight .il {
+	color: #099;
+}
+
+.highlight .mi {
+	color: #099;
+}
+
+.highlight .mo {
+	color: #099;
+}
+
+.highlight .m, .highlight .mb, .highlight .mx {
+	color: #099;
+}
+
+.highlight .sa {
+	color: #000;
+	font-weight: bold;
+}
+
+.highlight .sb {
+	color: #d14;
+}
+
+.highlight .sc {
+	color: #d14;
+}
+
+.highlight .sd {
+	color: #d14;
+}
+
+.highlight .s2 {
+	color: #d14;
+}
+
+.highlight .se {
+	color: #d14;
+}
+
+.highlight .sh {
+	color: #d14;
+}
+
+.highlight .si {
+	color: #d14;
+}
+
+.highlight .sx {
+	color: #d14;
+}
+
+.highlight .sr {
+	color: #009926;
+}
+
+.highlight .s1 {
+	color: #d14;
+}
+
+.highlight .ss {
+	color: #990073;
+}
+
+.highlight .s, .highlight .dl {
+	color: #d14;
+}
+
+.highlight .na {
+	color: #008080;
+}
+
+.highlight .bp {
+	color: #999;
+}
+
+.highlight .nb {
+	color: #0086b3;
+}
+
+.highlight .nc {
+	color: #458;
+	font-weight: bold;
+}
+
+.highlight .no {
+	color: #008080;
+}
+
+.highlight .nd {
+	color: #3c5d5d;
+	font-weight: bold;
+}
+
+.highlight .ni {
+	color: #800080;
+}
+
+.highlight .ne {
+	color: #900;
+	font-weight: bold;
+}
+
+.highlight .nf, .highlight .fm {
+	color: #900;
+	font-weight: bold;
+}
+
+.highlight .nl {
+	color: #900;
+	font-weight: bold;
+}
+
+.highlight .nn {
+	color: #555;
+}
+
+.highlight .nt {
+	color: #000080;
+}
+
+.highlight .vc {
+	color: #008080;
+}
+
+.highlight .vg {
+	color: #008080;
+}
+
+.highlight .vi {
+	color: #008080;
+}
+
+.highlight .nv, .highlight .vm {
+	color: #008080;
+}
+
+.highlight .ow {
+	color: #000;
+	font-weight: bold;
+}
+
+.highlight .o {
+	color: #000;
+	font-weight: bold;
+}
+
+.highlight .w {
+	color: #bbb;
+}
+
+.highlight {
+	background-color: #f8f8f8;
+}

+ 350 - 0
docs/assets/css/normalize.css

@@ -0,0 +1,350 @@
+/* stylelint-disable */
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+   ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+  line-height: 1.15; /* 1 */
+  -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+   ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+  margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+
+main {
+  display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+/* Grouping content
+   ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+  box-sizing: content-box; /* 1 */
+  height: 0; /* 1 */
+  overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+   ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+  background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+  border-bottom: none; /* 1 */
+  text-decoration: underline; /* 2 */
+  text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+  font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+  /* font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+  font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+sup {
+  top: -0.5em;
+}
+
+/* Embedded content
+   ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+  border-style: none;
+}
+
+/* Forms
+   ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: inherit; /* 1 */
+  font-size: 100%; /* 1 */
+  line-height: 1.15; /* 1 */
+  margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+  overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+  text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+  -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+  padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ *    `fieldset` elements in all browsers.
+ */
+
+legend {
+  box-sizing: border-box; /* 1 */
+  color: inherit; /* 2 */
+  display: table; /* 1 */
+  max-width: 100%; /* 1 */
+  padding: 0; /* 3 */
+  white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+  vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+  overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+  box-sizing: border-box; /* 1 */
+  padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+  -webkit-appearance: textfield; /* 1 */
+  outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+  -webkit-appearance: button; /* 1 */
+  font: inherit; /* 2 */
+}
+
+/* Interactive
+   ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+  display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+  display: list-item;
+}
+
+/* Misc
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+  display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+  display: none;
+}

+ 0 - 11
docs/assets/css/style.scss

@@ -1,11 +0,0 @@
-// stylelint-disable-next-line
-@import "{{ site.theme }}";
-
-.page-header .project-name a {
-	color: #fff;
-
-	&:hover {
-		text-decoration: none;
-		opacity: .7;
-	}
-}

BIN
docs/assets/fonts/OpenSans.woff2


+ 63 - 0
docs/assets/js/docs.js

@@ -0,0 +1,63 @@
+/* globals i18n */
+
+if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
+	document.head.insertAdjacentHTML('beforeend', `
+	<meta name="darkreader-lock">
+	`);
+}
+
+let asideNav;
+
+window.addEventListener('beforeunload', () => {
+	sessionStorage.setItem('sidebar_scrollTop', asideNav.scrollTop);
+});
+
+window.addEventListener('keydown', (e) => {
+	if (e.key === 'Escape') {
+		location.hash = 'close';
+	}
+});
+
+document.addEventListener('DOMContentLoaded', () => {
+	asideNav = document.querySelector('aside > nav.docs');
+
+	const sidebar_scrollTop = sessionStorage.getItem('sidebar_scrollTop');
+	if (sidebar_scrollTop) {
+		asideNav.scrollTo(0, sidebar_scrollTop);
+		sessionStorage.removeItem('sidebar_scrollTop');
+	}
+
+	for (const el of document.querySelectorAll('div.highlight')) {
+		/* eslint-disable @stylistic/max-len */
+		el.insertAdjacentHTML('afterbegin', `
+		<button class="copy" title="${i18n.copy_to_clipboard}">
+			<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
+				<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1z"/>
+				<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0z"/>
+			</svg>
+		</button>
+		`);
+		/* eslint-enable @stylistic/max-len */
+		const copyBtn = el.querySelector('button.copy');
+		copyBtn.addEventListener('click', () => {
+			const snippet = el.querySelector('code').innerText;
+			if (navigator.clipboard) {
+				navigator.clipboard.writeText(snippet);
+			} else {
+				// Fallback if no HTTPS
+				const input = document.createElement('textarea');
+				input.innerHTML = snippet;
+				document.body.append(input);
+				input.select();
+				document.execCommand('copy');
+				input.remove();
+			}
+		});
+	}
+
+	for (const el of document.querySelectorAll('img')) {
+		if (el.parentNode.tagName !== 'A') {
+			el.outerHTML = `<a href="${el.getAttribute('src')}">${el.outerHTML}</a>`;
+		}
+	}
+});

Разница между файлами не показана из-за своего большого размера
+ 5 - 0
docs/assets/js/simple-jekyll-search.min.js


+ 1 - 1
docs/en/developers/03_Running_tests.md

@@ -61,7 +61,7 @@ If you do not have one, you need to create one.
 		},
 		},
 		"response": {
 		"response": {
 			"status": 200,
 			"status": 200,
-			"bodyFileName": "{{request.pathSegments.[0]}}",
+			"bodyFileName": "{{ '{{' }}request.pathSegments.[0]}}",
 			"transformers": ["response-template"],
 			"transformers": ["response-template"],
 			"headers": {
 			"headers": {
 				"Content-Type": "application/rss+xml"
 				"Content-Type": "application/rss+xml"

+ 4 - 0
docs/en/index.md

@@ -1,3 +1,7 @@
+---
+lang: en
+---
+
 ![FreshRSS logo](img/logo_freshrss.png)
 ![FreshRSS logo](img/logo_freshrss.png)
 
 
 # FreshRSS manual (English)
 # FreshRSS manual (English)

BIN
docs/favicon.ico


+ 3 - 0
docs/fr/index.md

@@ -1,3 +1,6 @@
+---
+lang: fr
+---
 ![Logo de FreshRSS](img/logo_freshrss.png)
 ![Logo de FreshRSS](img/logo_freshrss.png)
 
 
 FreshRSS est un agrégateur et lecteur de flux RSS. Il permet de regrouper
 FreshRSS est un agrégateur et lecteur de flux RSS. Il permet de regrouper

+ 6 - 0
docs/index.md

@@ -1,6 +1,12 @@
+---
+lang: en
+---
+
 # Welcome to the FreshRSS documentation
 # Welcome to the FreshRSS documentation
 
 
 If you want to contribute, you can [find us on GitHub](https://github.com/FreshRSS/FreshRSS).
 If you want to contribute, you can [find us on GitHub](https://github.com/FreshRSS/FreshRSS).
 
 
 - [English documentation](./en/index.md)
 - [English documentation](./en/index.md)
 - [Documentation française](./fr/index.md)
 - [Documentation française](./fr/index.md)
+
+<meta http-equiv="Refresh" content="0; url=./en/">

+ 13 - 0
docs/search.en.json

@@ -0,0 +1,13 @@
+---
+layout: none
+---
+[
+{% assign pages = site.pages | where_exp: "p", "p.title != nil" | where: "lang", "en" %}
+{% for page in pages %}
+  {
+    "title": "{{ page.title | escape }}",
+    "url": "{{ site.baseurl }}{{ page.url }}",
+    "content": {{ page.content | markdownify | strip_html | jsonify }}
+  }{% unless forloop.last %},{% endunless %}
+{% endfor %}
+]

+ 13 - 0
docs/search.fr.json

@@ -0,0 +1,13 @@
+---
+layout: none
+---
+[
+{% assign pages = site.pages | where_exp: "p", "p.title != nil" | where: "lang", "fr" %}
+{% for page in pages %}
+  {
+    "title": "{{ page.title | escape }}",
+    "url": "{{ site.baseurl }}{{ page.url }}",
+    "content": {{ page.content | markdownify | strip_html | jsonify }}
+  }{% unless forloop.last %},{% endunless %}
+{% endfor %}
+]

Некоторые файлы не были показаны из-за большого количества измененных файлов