Docs site (#82)

* init static site

* add index

* changes footer

* remove search

* add docs workflows

* remove unused pages

* remove commit step

* README and CHANGELOG added, also fix cron typo

* Sorry ayrat my bad xd

* little glitch

* fix base url

* dates modify

* fix authors

* change structure

* don't remove docs :]

* remove rust code for static site

* copy recursively

* add random attribute

* remove submodules

* more fixes

* master branch

* CHANGELOG AND README update automated

* fix ref

* Icons may be work ?

* fix deploy bug

* i think fix the deploy bug

* gitignore README and CHANGELOG

* delete -f

* delete desc

* delete lead

Co-authored-by: Ayrat Badykov <ayratin555@gmail.com>
This commit is contained in:
Pmarquez 2022-09-07 16:23:13 +00:00 committed by Ayrat Badykov
parent 020ac943b5
commit 8b6e5c19b8
No known key found for this signature in database
GPG key ID: EC1148A46811EFB5
45 changed files with 308 additions and 547 deletions

View file

@ -10,14 +10,14 @@ jobs:
steps:
- uses: actions/checkout@v3
with:
ref: 'ayrat555/docs-site'
ref: 'master'
submodules: 'recursive'
- run: |
git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
- run: |
git config --global user.email "ayratin555@gmail.com"
git config --global user.name "Ayrat Badykov"
git add . && git commit -m "Commit copied files"
- name: Deploy docs
run: |
source ./docs/deploy.sh && build && deploy

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
**/target
Cargo.lock
src/schema.rs
docs/content/docs/CHANGELOG.md
docs/content/docs/README.md

View file

@ -1,5 +1,5 @@
# The URL the site will be built for
base_url = "https://adidoks.netlify.com"
base_url = "https://fang.badykov.com"
title = "Fang"
description = "Fang is a background task processing for Rust. It uses Postgres DB as a task queue."
@ -45,7 +45,7 @@ highlight_code = true
[extra]
# Put all your custom variables here
author = "Airat Badykov, Pepe Márquez"
author = "Ayrat Badykov, Pepe Márquez"
github = "https://github.com/ayrat555/fang"
# If running on netlify.app site, set to true
@ -59,7 +59,7 @@ theme_color = "#fff"
# More about site's title
title_separator = "|" # set as |, -, _, etc
title_addition = "Modern Documentation Theme"
title_addition = "Background processing"
# Set date format in blog publish metadata
@ -68,8 +68,8 @@ timezone = "America/New_York"
# Edit page on reposity or not
edit_page = false
docs_repo = "https://github.com/aaranxu/adidoks"
repo_branch = "main"
docs_repo = "https://github.com/ayrat555/fang"
repo_branch = "master"
## Math settings
# options: true, false. Enable math support globally,
@ -81,20 +81,20 @@ library = "katex" # options: "katex", "mathjax". default is "katex".
[extra.open]
enable = true
# this image will be used as fallback if a page has no image of its own
image = "doks.png"
twitter_site = "aaranxu"
twitter_creator = "aaranxu"
facebook_author = "ichunyun"
facebook_publisher = "ichunyun"
image = "logo.png"
twitter_site = ""
twitter_creator = ""
facebook_author = ""
facebook_publisher = ""
og_locale = "en_US"
## JSON-LD
[extra.schema]
type = "Organization"
logo = "logo-doks.png"
twitter = "https://twitter.com/aaranxu"
type = "FANG"
logo = "logo.png"
twitter = ""
linked_in = ""
github = "https://github.com/aaranxu"
github = "https://github.com/ayrat555/fang"
section = "blog" # see config.extra.main~url
## Sitelinks Search Box
site_links_search_box = false

View file

@ -5,7 +5,9 @@ title = "FANG"
# The homepage contents
[extra]
lead = '<b>Fang</b> is a background task processing for Rust. It uses Postgres DB as a task queue.'
url = "/docs/getting-started/introduction/"
url = "/docs/readme"
url_button = "Get started"
repo_version = "GitHub v0.9.0"
repo_license = "Open-source MIT License."
@ -13,31 +15,39 @@ repo_url = "https://github.com/ayrat555/fang"
# Menu items
[[extra.menu.main]]
name = "Docs"
name = "README"
section = "docs"
url = "/docs/getting-started/introduction/"
url = "/docs/readme"
weight = 10
[[extra.menu.main]]
name = "CHANGELOG"
section = "docs"
url = "/docs/changelog"
[[extra.menu.main]]
name = "Blog"
section = "blog"
url = "/blog/"
weight = 20
[[extra.list]]
title = "Async and threaded workers"
content = "Workers can be started in threads (threaded workers) or tokio tasks (async workers)"
content = 'Workers can be started in threads (threaded workers) or tokio tasks (async workers)'
[[extra.list]]
title = "Scheduled tasks"
content = "Tasks can be scheduled at any time in the future"
content = 'Tasks can be scheduled at any time in the future'
[[extra.list]]
title = "Periodic (CRON) tasks"
content = "Tasks can be scheduled use cron schedules"
content = 'Tasks can be scheduled using cron expressions'
[[extra.list]]
title = "Unique tasks"
content = "Tasks are not duplicated in the queue if they are unique"
content = 'Tasks are not duplicated in the queue if they are unique'
[[extra.list]]
title = "Single-purpose workers"
content = 'Tasks are stored in a single table but workers can execute only tasks of the specific type'
+++

View file

@ -0,0 +1,15 @@
+++
title = "Authors"
description = "The authors of the blog articles."
date = 2022-09-06T8:00:00+00:00
updated = 2022-09-06T8:00:00+00:00
draft = false
# Authors
[extra.author_pages]
"ayrat-badykov" = "authors/ayrat-badykov.md"
"pepe-marquez" = "authors/pepe-marquez.md"
+++
The authors of the blog articles.

View file

@ -0,0 +1,11 @@
+++
title = "Ayrat Badykov"
description = "Creator of Fang."
date = 2021-04-01T08:50:45+00:00
updated = 2021-04-01T08:50:45+00:00
draft = false
+++
Co-creator of **Fang**
[@ayrat555](https://github.com/ayrat555)

View file

@ -0,0 +1,11 @@
+++
title = "Pepe Márquez Romero"
description = "Co-Creator of Fang."
date = 2021-04-01T08:50:45+00:00
updated = 2021-04-01T08:50:45+00:00
draft = false
+++
Co-creator of **Fang**.
[@pxp9](https://github.com/pxp9)

View file

@ -0,0 +1,189 @@
+++
title = "Fang, async background processing for Rust"
description = "Async background processing for rust with tokio and postgres"
date = 2022-08-06T08:00:00+00:00
updated = 2022-08-06T08:00:00+00:00
template = "blog/page.html"
sort_by = "weight"
weight = 1
draft = false
[taxonomies]
authors = ["Ayrat Badykov", "Pepe Márquez"]
[extra]
lead = 'Async background processing for rust with tokio and postgres'
images = []
+++
Even though the first stable version of Rust was released in 2015, there are still some holes in its ecosystem for solving common tasks. One of which is background processing.
In software engineering background processing is a common approach for solving several problems:
- Carry out periodic tasks. For example, deliver notifications, update cached values.
- Defer expensive work so your application stays responsive while performing calculations in the background
Most programming languages have go-to background processing frameworks/libraries. For example:
- Ruby - [sidekiq](https://github.com/mperham/sidekiq). It uses Redis as a job queue.
- Python - [dramatiq](https://github.com/Bogdanp/dramatiq). It uses RabbitMQ as a job queue.
- Elixir - [oban](https://github.com/sorentwo/oban). It uses a Postgres DB as a job queue.
The async programming (async/await) can be used for background processing but it has several major disadvantages if used directly:
- It doesn't give control of the number of tasks that are being executed at any given time. So a lot of spawned tasks can overload a thread/threads that they're started on.
- It doesn't provide any monitoring which can be useful to investigate your system and find bottlenecks
- Tasks are not persistent. So all enqueued tasks are lost on every application restart
To solve these shortcomings of the async programming we implemented the async processing in [the fang library](https://github.com/ayrat555/fang).
## Threaded Fang
Fang is a background processing library for rust. The first version of Fang was released exactly one year ago. Its key features were:
- Each worker is started in a separate thread
- A Postgres table is used as the task queue
This implementation was written for a specific use case - [el monitorro bot](https://github.com/ayrat555/el_monitorro). This specific implementation of background processing was proved by time. Each day it processes more and more feeds every minute (the current number is more than 3000). Some users host the bot on their infrastructure.
You can find out more about the threaded processing in fang in [this blog post](https://www.badykov.com/rust/fang/).
## Async Fang
<blockquote>
<p>
Async provides significantly reduced CPU and memory overhead, especially for workloads with a large amount of IO-bound tasks, such as servers and databases. All else equal, you can have orders of magnitude more tasks than OS threads, because an async runtime uses a small amount of (expensive) threads to handle a large amount of (cheap) tasks
</p>
<footer><cite title="Async book">From the Rust's Async book</cite></footer>
</blockquote>
For some lightweight background tasks, it's cheaper to run them on the same thread using async instead of starting one thread per worker. That's why we implemented this kind of processing in fang. Its key features:
- Each worker is started as a tokio task
- If any worker fails during task execution, it's restarted
- Tasks are saved to a Postgres database. Instead of diesel, [tokio-postgres](https://github.com/sfackler/rust-postgres) is used to interact with a db. The threaded processing uses the [diesel](https://github.com/diesel-rs/diesel) ORM which blocks the thread.
- The implementation is based on traits so it's easy to implement additional backends (redis, in-memory) to store tasks.
## Usage
The usage is straightforward:
1. Define a serializable task by adding `serde` derives to a task struct.
2. Implement `AsyncRunnable` runnable trait for fang to be able to run it.
3. Start workers.
4. Enqueue tasks.
Let's go over each step.
### Define a job
```rust
use fang::serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(crate = "fang::serde")]
pub struct MyTask {
pub number: u16,
}
impl MyTask {
pub fn new(number: u16) -> Self {
Self { number }
}
}
```
Fang re-exports `serde` so it's not required to add it to the `Cargo.toml` file
### Implement the AsyncRunnable trait
```rust
use fang::async_trait;
use fang::typetag;
use fang::AsyncRunnable;
use std::time::Duration;
#[async_trait]
#[typetag::serde]
impl AsyncRunnable for MyTask {
async fn run(&self, queue: &mut dyn AsyncQueueable) -> Result<(), Error> {
let new_task = MyTask::new(self.number + 1);
queue
.insert_task(&new_task as &dyn AsyncRunnable)
.await
.unwrap();
log::info!("the current number is {}", self.number);
tokio::time::sleep(Duration::from_secs(3)).await;
Ok(())
}
}
```
- Fang uses the [typetag library](https://github.com/dtolnay/typetag) to serialize trait objects and save them to the queue.
- The [async-trait](https://github.com/dtolnay/async-trait) is used for implementing async traits
### Init queue
```rust
use fang::asynk::async_queue::AsyncQueue;
let max_pool_size: u32 = 2;
let mut queue = AsyncQueue::builder()
.uri("postgres://postgres:postgres@localhost/fang")
.max_pool_size(max_pool_size)
.duplicated_tasks(true)
.build();
```
### Start workers
```rust
use fang::asynk::async_worker_pool::AsyncWorkerPool;
use fang::NoTls;
let mut pool: AsyncWorkerPool<AsyncQueue<NoTls>> = AsyncWorkerPool::builder()
.number_of_workers(10_u32)
.queue(queue.clone())
.build();
pool.start().await;
```
### Insert tasks
```rust
let task = MyTask::new(0);
queue
.insert_task(&task1 as &dyn AsyncRunnable)
.await
.unwrap();
```
## Pitfalls
The async processing is suitable for lightweight tasks. But for heavier tasks it's advised to use one of the following approaches:
- start a separate tokio runtime to run fang workers
- use the threaded processing feature implemented in fang instead of the async processing
## Future directions
There are a couple of features planned for fang:
- Retries with different backoff modes
- Additional backends (in-memory, redis)
- Graceful shutdown for async workers (for the threaded processing this feature is implemented)
- Cron jobs
## Conclusion
The project is available on [GitHub](https://github.com/ayrat555/fang)
The async feature and this post is written in collaboration between [Ayrat Badykov](https://www.badykov.com/) ([github](https://github.com/ayrat555)) and [Pepe Márquez Romero](https://pxp9.github.io/) ([github](https://github.com/pxp9))

View file

@ -0,0 +1,7 @@
+++
title = "Blog"
description = "Blog"
sort_by = "date"
paginate_by = 2
template = "blog/section.html"
+++

View file

@ -0,0 +1,10 @@
+++
title = "Docs"
description = "The documentation of Fang library."
date = 2022-09-06T08:00:00+00:00
updated = 2022-09-06T08:00:00+00:00
template = "docs/section.html"
sort_by = "weight"
weight = 4
draft = false
+++

View file

@ -3,9 +3,36 @@ set -e
BRANCH="gh-pages"
current_time() {
TIME=$(date -u --rfc-3339=seconds)
DATE=$(echo $TIME | cut -d' ' -f1)
HOUR=$(echo $TIME | cut -d' ' -f2)
VALID_TIME=${DATE}T${HOUR}
echo $VALID_TIME
}
build() {
echo "Starting building..."
TIME=$(current_time)
printf "+++\ntitle = \"CHANGELOG\"\ndate = $TIME\nupdated = $TIME\ndraft = false\nweight = 410\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\ntoc = true\ntop = false\n+++\n\n" > docs/content/docs/CHANGELOG.md
cat CHANGELOG.md >> docs/content/docs/CHANGELOG.md
printf "+++\ntitle = \"README\"\ndate = $TIME\nupdated = $TIME\ndraft = false\nweight = 410\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\ntoc = true\ntop = false\n+++\n\n" > docs/content/docs/README.md
cat README.md >> docs/content/docs/README.md
cp -R docs ../docs_backup
rm -r *
cp -R ../docs_backup ./docs
cd docs
sudo snap install --edge zola
@ -23,7 +50,8 @@ deploy() {
cp -vr /tmp/public/* .
git config user.name "GitHub Actions"
git config user.email "github-actions-bot@users.noreply.github.com"
rm -rf docs
rm -r docs/themes
git add .
git commit -m "Deploy new version docs"
git push --force "https://${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" ${BRANCH}

View file

@ -1,2 +0,0 @@
/*
Access-Control-Allow-Origin: *

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

File diff suppressed because one or more lines are too long

View file

@ -1,7 +0,0 @@
/*!
* Bootstrap Reboot v5.0.0-beta3 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,*::before,*::after{box-sizing:border-box}@media (prefers-reduced-motion: no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h6,h5,h4,h3,h2,h1{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width: 1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width: 1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width: 1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width: 1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-bs-original-title]{text-decoration:underline dotted;cursor:help;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr /* rtl:ignore */;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}thead,tbody,tfoot,tr,td,th{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role="button"]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button:not(:disabled),[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width: 1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-text,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type="search"]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none !important}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#E1E8ED" d="M32.415 9.586l-9-9C23.054.225 22.553 0 22 0c-1.104 0-1.999.896-2 2 0 .552.224 1.053.586 1.415l-3.859 3.859 9 9 3.859-3.859c.362.361.862.585 1.414.585 1.104 0 2.001-.896 2-2 0-.552-.224-1.052-.585-1.414z"/><path fill="#CCD6DD" d="M22 0H7C4.791 0 3 1.791 3 4v28c0 2.209 1.791 4 4 4h22c2.209 0 4-1.791 4-4V11h-9c-1 0-2-1-2-2V0z"/><path fill="#99AAB5" d="M22 0h-2v9c0 2.209 1.791 4 4 4h9v-2h-9c-1 0-2-1-2-2V0zm-5 8c0 .552-.448 1-1 1H8c-.552 0-1-.448-1-1s.448-1 1-1h8c.552 0 1 .448 1 1zm0 4c0 .552-.448 1-1 1H8c-.552 0-1-.448-1-1s.448-1 1-1h8c.552 0 1 .448 1 1zm12 4c0 .552-.447 1-1 1H8c-.552 0-1-.448-1-1s.448-1 1-1h20c.553 0 1 .448 1 1zm0 4c0 .553-.447 1-1 1H8c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1zm0 4c0 .553-.447 1-1 1H8c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1zm0 4c0 .553-.447 1-1 1H8c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1z"/></svg>

Before

Width:  |  Height:  |  Size: 972 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 773 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,146 +0,0 @@
var suggestions = document.getElementById('suggestions');
var userinput = document.getElementById('userinput');
document.addEventListener('keydown', inputFocus);
function inputFocus(e) {
if (e.keyCode === 191 ) {
e.preventDefault();
userinput.focus();
}
if (e.keyCode === 27 ) {
userinput.blur();
suggestions.classList.add('d-none');
}
}
document.addEventListener('click', function(event) {
var isClickInsideElement = suggestions.contains(event.target);
if (!isClickInsideElement) {
suggestions.classList.add('d-none');
}
});
/*
Source:
- https://dev.to/shubhamprakash/trap-focus-using-javascript-6a3
*/
document.addEventListener('keydown',suggestionFocus);
function suggestionFocus(e){
const focusableSuggestions= suggestions.querySelectorAll('a');
const focusable= [...focusableSuggestions];
const index = focusable.indexOf(document.activeElement);
let nextIndex = 0;
if (e.keyCode === 38) {
e.preventDefault();
nextIndex= index > 0 ? index-1 : 0;
focusableSuggestions[nextIndex].focus();
}
else if (e.keyCode === 40) {
e.preventDefault();
nextIndex= index+1 < focusable.length ? index+1 : index;
focusableSuggestions[nextIndex].focus();
}
}
/*
Source:
- https://github.com/nextapps-de/flexsearch#index-documents-field-search
- https://raw.githack.com/nextapps-de/flexsearch/master/demo/autocomplete.html
*/
(function(){
var index = new FlexSearch({
preset: 'score',
cache: true,
doc: {
id: 'id',
field: [
'title',
'description',
'content',
],
store: [
'href',
'title',
'description',
],
},
});
var docs = [
{{ range $index, $page := (where .Site.Pages "Section" "docs") -}}
{
id: {{ $index }},
href: "{{ .RelPermalink | relURL }}",
title: {{ .Title | jsonify }},
description: {{ .Params.description | jsonify }},
content: {{ .Content | jsonify }}
},
{{ end -}}
];
index.add(docs);
userinput.addEventListener('input', show_results, true);
suggestions.addEventListener('click', accept_suggestion, true);
function show_results(){
var value = this.value;
var results = index.search(value, 5);
var entry, childs = suggestions.childNodes;
var i = 0, len = results.length;
suggestions.classList.remove('d-none');
results.forEach(function(page) {
entry = document.createElement('div');
entry.innerHTML = '<a href><span></span><span></span></a>';
a = entry.querySelector('a'),
t = entry.querySelector('span:first-child'),
d = entry.querySelector('span:nth-child(2)');
a.href = page.href;
t.textContent = page.title;
d.textContent = page.description;
suggestions.appendChild(entry);
});
while(childs.length > len){
suggestions.removeChild(childs[i])
}
}
function accept_suggestion(){
while(suggestions.lastChild){
suggestions.removeChild(suggestions.lastChild);
}
return false;
}
}());

View file

@ -1,14 +0,0 @@
// Set darkmode
document.getElementById('mode').addEventListener('click', () => {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
});
// enforce local storage setting but also fallback to user-agent preferences
if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
document.body.classList.add('dark');
}

View file

@ -1,317 +0,0 @@
var suggestions = document.getElementById('suggestions');
var userinput = document.getElementById('userinput');
document.addEventListener('keydown', inputFocus);
function inputFocus(e) {
if (e.keyCode === 191
&& document.activeElement.tagName !== "INPUT"
&& document.activeElement.tagName !== "TEXTAREA") {
e.preventDefault();
userinput.focus();
}
if (e.keyCode === 27 ) {
userinput.blur();
suggestions.classList.add('d-none');
}
}
document.addEventListener('click', function(event) {
var isClickInsideElement = suggestions.contains(event.target);
if (!isClickInsideElement) {
suggestions.classList.add('d-none');
}
});
/*
Source:
- https://dev.to/shubhamprakash/trap-focus-using-javascript-6a3
*/
document.addEventListener('keydown',suggestionFocus);
function suggestionFocus(e){
const focusableSuggestions= suggestions.querySelectorAll('a');
if (suggestions.classList.contains('d-none')
|| focusableSuggestions.length === 0) {
return;
}
const focusable= [...focusableSuggestions];
const index = focusable.indexOf(document.activeElement);
let nextIndex = 0;
if (e.keyCode === 38) {
e.preventDefault();
nextIndex= index > 0 ? index-1 : 0;
focusableSuggestions[nextIndex].focus();
}
else if (e.keyCode === 40) {
e.preventDefault();
nextIndex= index+1 < focusable.length ? index+1 : index;
focusableSuggestions[nextIndex].focus();
}
}
/*
Source:
- https://github.com/nextapps-de/flexsearch#index-documents-field-search
- https://raw.githack.com/nextapps-de/flexsearch/master/demo/autocomplete.html
- http://elasticlunr.com/
- https://github.com/getzola/zola/blob/master/docs/static/search.js
*/
(function(){
var index = elasticlunr.Index.load(window.searchIndex);
userinput.addEventListener('input', show_results, true);
suggestions.addEventListener('click', accept_suggestion, true);
function show_results(){
var value = this.value.trim();
var options = {
bool: "OR",
fields: {
title: {boost: 2},
body: {boost: 1},
}
};
var results = index.search(value, options);
var entry, childs = suggestions.childNodes;
var i = 0, len = results.length;
var items = value.split(/\s+/);
suggestions.classList.remove('d-none');
results.forEach(function(page) {
if (page.doc.body !== '') {
entry = document.createElement('div');
entry.innerHTML = '<a href><span></span><span></span></a>';
a = entry.querySelector('a'),
t = entry.querySelector('span:first-child'),
d = entry.querySelector('span:nth-child(2)');
a.href = page.ref;
t.textContent = page.doc.title;
d.innerHTML = makeTeaser(page.doc.body, items);
suggestions.appendChild(entry);
}
});
while(childs.length > len){
suggestions.removeChild(childs[i])
}
}
function accept_suggestion(){
while(suggestions.lastChild){
suggestions.removeChild(suggestions.lastChild);
}
return false;
}
// Taken from mdbook
// The strategy is as follows:
// First, assign a value to each word in the document:
// Words that correspond to search terms (stemmer aware): 40
// Normal words: 2
// First word in a sentence: 8
// Then use a sliding window with a constant number of words and count the
// sum of the values of the words within the window. Then use the window that got the
// maximum sum. If there are multiple maximas, then get the last one.
// Enclose the terms in <b>.
function makeTeaser(body, terms) {
var TERM_WEIGHT = 40;
var NORMAL_WORD_WEIGHT = 2;
var FIRST_WORD_WEIGHT = 8;
var TEASER_MAX_WORDS = 30;
var stemmedTerms = terms.map(function (w) {
return elasticlunr.stemmer(w.toLowerCase());
});
var termFound = false;
var index = 0;
var weighted = []; // contains elements of ["word", weight, index_in_document]
// split in sentences, then words
var sentences = body.toLowerCase().split(". ");
for (var i in sentences) {
var words = sentences[i].split(/[\s\n]/);
var value = FIRST_WORD_WEIGHT;
for (var j in words) {
var word = words[j];
if (word.length > 0) {
for (var k in stemmedTerms) {
if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {
value = TERM_WEIGHT;
termFound = true;
}
}
weighted.push([word, value, index]);
value = NORMAL_WORD_WEIGHT;
}
index += word.length;
index += 1; // ' ' or '.' if last word in sentence
}
index += 1; // because we split at a two-char boundary '. '
}
if (weighted.length === 0) {
if (body.length !== undefined && body.length > TEASER_MAX_WORDS * 10) {
return body.substring(0, TEASER_MAX_WORDS * 10) + '...';
} else {
return body;
}
}
var windowWeights = [];
var windowSize = Math.min(weighted.length, TEASER_MAX_WORDS);
// We add a window with all the weights first
var curSum = 0;
for (var i = 0; i < windowSize; i++) {
curSum += weighted[i][1];
}
windowWeights.push(curSum);
for (var i = 0; i < weighted.length - windowSize; i++) {
curSum -= weighted[i][1];
curSum += weighted[i + windowSize][1];
windowWeights.push(curSum);
}
// If we didn't find the term, just pick the first window
var maxSumIndex = 0;
if (termFound) {
var maxFound = 0;
// backwards
for (var i = windowWeights.length - 1; i >= 0; i--) {
if (windowWeights[i] > maxFound) {
maxFound = windowWeights[i];
maxSumIndex = i;
}
}
}
var teaser = [];
var startIndex = weighted[maxSumIndex][2];
for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) {
var word = weighted[i];
if (startIndex < word[2]) {
// missing text from index to start of `word`
teaser.push(body.substring(startIndex, word[2]));
startIndex = word[2];
}
// add <em/> around search terms
if (word[1] === TERM_WEIGHT) {
teaser.push("<b>");
}
startIndex = word[2] + word[0].length;
// Check the string is ascii characters or not
var re = /^[\x00-\xff]+$/
if (word[1] !== TERM_WEIGHT && word[0].length >= 12 && !re.test(word[0])) {
// If the string's length is too long, it maybe a Chinese/Japance/Korean article
// if using substring method directly, it may occur error codes on emoji chars
var strBefor = body.substring(word[2], startIndex);
var strAfter = substringByByte(strBefor, 12);
teaser.push(strAfter);
} else {
teaser.push(body.substring(word[2], startIndex));
}
if (word[1] === TERM_WEIGHT) {
teaser.push("</b>");
}
}
teaser.push("…");
return teaser.join("");
}
}());
// Get substring by bytes
// If using JavaScript inline substring method, it will return error codes
// Source: https://www.52pojie.cn/thread-1059814-1-1.html
function substringByByte(str, maxLength) {
var result = "";
var flag = false;
var len = 0;
var length = 0;
var length2 = 0;
for (var i = 0; i < str.length; i++) {
var code = str.codePointAt(i).toString(16);
if (code.length > 4) {
i++;
if ((i + 1) < str.length) {
flag = str.codePointAt(i + 1).toString(16) == "200d";
}
}
if (flag) {
len += getByteByHex(code);
if (i == str.length - 1) {
length += len;
if (length <= maxLength) {
result += str.substr(length2, i - length2 + 1);
} else {
break
}
}
} else {
if (len != 0) {
length += len;
length += getByteByHex(code);
if (length <= maxLength) {
result += str.substr(length2, i - length2 + 1);
length2 = i + 1;
} else {
break
}
len = 0;
continue;
}
length += getByteByHex(code);
if (length <= maxLength) {
if (code.length <= 4) {
result += str[i]
} else {
result += str[i - 1] + str[i]
}
length2 = i + 1;
} else {
break
}
}
}
return result;
}
// Get the string bytes from binary
function getByteByBinary(binaryCode) {
// Binary system, starts with `0b` in ES6
// Octal number system, starts with `0` in ES5 and starts with `0o` in ES6
// Hexadecimal, starts with `0x` in both ES5 and ES6
var byteLengthDatas = [0, 1, 2, 3, 4];
var len = byteLengthDatas[Math.ceil(binaryCode.length / 8)];
return len;
}
// Get the string bytes from hexadecimal
function getByteByHex(hexCode) {
return getByteByBinary(parseInt(hexCode, 16).toString(2));
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
{"name":"Zola Theme AdiDoks","short_name":"AdiDoks","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#fff","background_color":"#fff","display":"standalone"}

BIN
docs/static/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

BIN
docs/static/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB