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>
4
.github/workflows/docs.yml
vendored
|
@ -10,14 +10,14 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: 'ayrat555/docs-site'
|
ref: 'master'
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
- run: |
|
- run: |
|
||||||
git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
|
git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
|
||||||
- run: |
|
- run: |
|
||||||
git config --global user.email "ayratin555@gmail.com"
|
git config --global user.email "ayratin555@gmail.com"
|
||||||
git config --global user.name "Ayrat Badykov"
|
git config --global user.name "Ayrat Badykov"
|
||||||
git add . && git commit -m "Commit copied files"
|
|
||||||
- name: Deploy docs
|
- name: Deploy docs
|
||||||
run: |
|
run: |
|
||||||
source ./docs/deploy.sh && build && deploy
|
source ./docs/deploy.sh && build && deploy
|
||||||
|
|
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
||||||
**/target
|
**/target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
src/schema.rs
|
src/schema.rs
|
||||||
|
docs/content/docs/CHANGELOG.md
|
||||||
|
docs/content/docs/README.md
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# The URL the site will be built for
|
# The URL the site will be built for
|
||||||
base_url = "https://adidoks.netlify.com"
|
base_url = "https://fang.badykov.com"
|
||||||
title = "Fang"
|
title = "Fang"
|
||||||
description = "Fang is a background task processing for Rust. It uses Postgres DB as a task queue."
|
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]
|
[extra]
|
||||||
# Put all your custom variables here
|
# Put all your custom variables here
|
||||||
author = "Airat Badykov, Pepe Márquez"
|
author = "Ayrat Badykov, Pepe Márquez"
|
||||||
github = "https://github.com/ayrat555/fang"
|
github = "https://github.com/ayrat555/fang"
|
||||||
|
|
||||||
# If running on netlify.app site, set to true
|
# If running on netlify.app site, set to true
|
||||||
|
@ -59,7 +59,7 @@ theme_color = "#fff"
|
||||||
|
|
||||||
# More about site's title
|
# More about site's title
|
||||||
title_separator = "|" # set as |, -, _, etc
|
title_separator = "|" # set as |, -, _, etc
|
||||||
title_addition = "Modern Documentation Theme"
|
title_addition = "Background processing"
|
||||||
|
|
||||||
|
|
||||||
# Set date format in blog publish metadata
|
# Set date format in blog publish metadata
|
||||||
|
@ -68,8 +68,8 @@ timezone = "America/New_York"
|
||||||
|
|
||||||
# Edit page on reposity or not
|
# Edit page on reposity or not
|
||||||
edit_page = false
|
edit_page = false
|
||||||
docs_repo = "https://github.com/aaranxu/adidoks"
|
docs_repo = "https://github.com/ayrat555/fang"
|
||||||
repo_branch = "main"
|
repo_branch = "master"
|
||||||
|
|
||||||
## Math settings
|
## Math settings
|
||||||
# options: true, false. Enable math support globally,
|
# options: true, false. Enable math support globally,
|
||||||
|
@ -81,20 +81,20 @@ library = "katex" # options: "katex", "mathjax". default is "katex".
|
||||||
[extra.open]
|
[extra.open]
|
||||||
enable = true
|
enable = true
|
||||||
# this image will be used as fallback if a page has no image of its own
|
# this image will be used as fallback if a page has no image of its own
|
||||||
image = "doks.png"
|
image = "logo.png"
|
||||||
twitter_site = "aaranxu"
|
twitter_site = ""
|
||||||
twitter_creator = "aaranxu"
|
twitter_creator = ""
|
||||||
facebook_author = "ichunyun"
|
facebook_author = ""
|
||||||
facebook_publisher = "ichunyun"
|
facebook_publisher = ""
|
||||||
og_locale = "en_US"
|
og_locale = "en_US"
|
||||||
|
|
||||||
## JSON-LD
|
## JSON-LD
|
||||||
[extra.schema]
|
[extra.schema]
|
||||||
type = "Organization"
|
type = "FANG"
|
||||||
logo = "logo-doks.png"
|
logo = "logo.png"
|
||||||
twitter = "https://twitter.com/aaranxu"
|
twitter = ""
|
||||||
linked_in = ""
|
linked_in = ""
|
||||||
github = "https://github.com/aaranxu"
|
github = "https://github.com/ayrat555/fang"
|
||||||
section = "blog" # see config.extra.main~url
|
section = "blog" # see config.extra.main~url
|
||||||
## Sitelinks Search Box
|
## Sitelinks Search Box
|
||||||
site_links_search_box = false
|
site_links_search_box = false
|
||||||
|
|
|
@ -5,7 +5,9 @@ title = "FANG"
|
||||||
# The homepage contents
|
# The homepage contents
|
||||||
[extra]
|
[extra]
|
||||||
lead = '<b>Fang</b> is a background task processing for Rust. It uses Postgres DB as a task queue.'
|
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"
|
url_button = "Get started"
|
||||||
repo_version = "GitHub v0.9.0"
|
repo_version = "GitHub v0.9.0"
|
||||||
repo_license = "Open-source MIT License."
|
repo_license = "Open-source MIT License."
|
||||||
|
@ -13,31 +15,39 @@ repo_url = "https://github.com/ayrat555/fang"
|
||||||
|
|
||||||
# Menu items
|
# Menu items
|
||||||
[[extra.menu.main]]
|
[[extra.menu.main]]
|
||||||
name = "Docs"
|
name = "README"
|
||||||
section = "docs"
|
section = "docs"
|
||||||
url = "/docs/getting-started/introduction/"
|
url = "/docs/readme"
|
||||||
weight = 10
|
weight = 10
|
||||||
|
|
||||||
|
[[extra.menu.main]]
|
||||||
|
name = "CHANGELOG"
|
||||||
|
section = "docs"
|
||||||
|
url = "/docs/changelog"
|
||||||
|
|
||||||
[[extra.menu.main]]
|
[[extra.menu.main]]
|
||||||
name = "Blog"
|
name = "Blog"
|
||||||
section = "blog"
|
section = "blog"
|
||||||
url = "/blog/"
|
url = "/blog/"
|
||||||
weight = 20
|
weight = 20
|
||||||
|
|
||||||
|
|
||||||
[[extra.list]]
|
[[extra.list]]
|
||||||
title = "Async and threaded workers"
|
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]]
|
[[extra.list]]
|
||||||
title = "Scheduled tasks"
|
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]]
|
[[extra.list]]
|
||||||
title = "Periodic (CRON) tasks"
|
title = "Periodic (CRON) tasks"
|
||||||
content = "Tasks can be scheduled use cron schedules"
|
content = 'Tasks can be scheduled using cron expressions'
|
||||||
|
|
||||||
[[extra.list]]
|
[[extra.list]]
|
||||||
title = "Unique tasks"
|
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'
|
||||||
+++
|
+++
|
||||||
|
|
15
docs/content/authors/_index.md
Normal 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.
|
11
docs/content/authors/ayrat-badykov.md
Normal 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)
|
11
docs/content/authors/pepe-marquez.md
Normal 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)
|
189
docs/content/blog/2022-08-06-async-processing.md
Normal 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))
|
||||||
|
|
7
docs/content/blog/_index.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
+++
|
||||||
|
title = "Blog"
|
||||||
|
description = "Blog"
|
||||||
|
sort_by = "date"
|
||||||
|
paginate_by = 2
|
||||||
|
template = "blog/section.html"
|
||||||
|
+++
|
10
docs/content/docs/_index.md
Normal 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
|
||||||
|
+++
|
|
@ -3,9 +3,36 @@ set -e
|
||||||
|
|
||||||
BRANCH="gh-pages"
|
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() {
|
build() {
|
||||||
echo "Starting building..."
|
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
|
cd docs
|
||||||
|
|
||||||
sudo snap install --edge zola
|
sudo snap install --edge zola
|
||||||
|
@ -23,7 +50,8 @@ deploy() {
|
||||||
cp -vr /tmp/public/* .
|
cp -vr /tmp/public/* .
|
||||||
git config user.name "GitHub Actions"
|
git config user.name "GitHub Actions"
|
||||||
git config user.email "github-actions-bot@users.noreply.github.com"
|
git config user.email "github-actions-bot@users.noreply.github.com"
|
||||||
rm -rf docs
|
|
||||||
|
rm -r docs/themes
|
||||||
git add .
|
git add .
|
||||||
git commit -m "Deploy new version docs"
|
git commit -m "Deploy new version docs"
|
||||||
git push --force "https://${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" ${BRANCH}
|
git push --force "https://${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" ${BRANCH}
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
/*
|
|
||||||
Access-Control-Allow-Origin: *
|
|
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 5 KiB |
|
@ -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}
|
|
6
docs/public/bootstrap/scss/bootstrap.css
vendored
Before Width: | Height: | Size: 8 KiB |
|
@ -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 |
Before Width: | Height: | Size: 416 B |
Before Width: | Height: | Size: 773 B |
Before Width: | Height: | Size: 15 KiB |
BIN
docs/public/fonts/vendor/jost/jost-v4-latin-500.woff
vendored
BIN
docs/public/fonts/vendor/jost/jost-v4-latin-700.woff
vendored
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}());
|
|
|
@ -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');
|
|
||||||
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
Before Width: | Height: | Size: 20 KiB |
10
docs/public/plugins/elasticlunr.min.js
vendored
|
@ -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
After Width: | Height: | Size: 245 KiB |
BIN
docs/static/logo.png
vendored
Normal file
After Width: | Height: | Size: 376 KiB |