mirror of
https://github.com/kiwix/libkiwix.git
synced 2026-01-06 13:28:07 -05:00
Compare commits
255 Commits
autocomple
...
12.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb74c9c7c7 | ||
|
|
312cecf5f2 | ||
|
|
a4d207a03a | ||
|
|
7e36dd5ddb | ||
|
|
8ca809f8d9 | ||
|
|
fd22e34d58 | ||
|
|
3be1ddd8a9 | ||
|
|
e9d9d85427 | ||
|
|
9599a31d2f | ||
|
|
4d60b106a2 | ||
|
|
60cce602a3 | ||
|
|
abb81e7798 | ||
|
|
1808857173 | ||
|
|
556b94daae | ||
|
|
5f4dad60b9 | ||
|
|
820ffa8134 | ||
|
|
f6f7214c99 | ||
|
|
1f5a160d3d | ||
|
|
f41007989b | ||
|
|
f25d287afa | ||
|
|
550fc2fcf9 | ||
|
|
96fb65f560 | ||
|
|
93197f8175 | ||
|
|
144945cfe0 | ||
|
|
c1ad65d515 | ||
|
|
af2dfdccbc | ||
|
|
f2072d87a0 | ||
|
|
e9c3a7ff45 | ||
|
|
a715203d3e | ||
|
|
552717b9ce | ||
|
|
0ed805ae6b | ||
|
|
b45cfd767a | ||
|
|
df164aefe5 | ||
|
|
58890a3f97 | ||
|
|
2d58142c58 | ||
|
|
0afa5e569c | ||
|
|
ae605dc26d | ||
|
|
d8f02ac225 | ||
|
|
e4595f357d | ||
|
|
881c121142 | ||
|
|
2d51e1f0c6 | ||
|
|
b24f681c24 | ||
|
|
deb02d92e2 | ||
|
|
dc58e278c7 | ||
|
|
9994302312 | ||
|
|
8c190cf34f | ||
|
|
1273570e01 | ||
|
|
9bd2df2327 | ||
|
|
08834d6f17 | ||
|
|
47950f132e | ||
|
|
1a92d4a0b5 | ||
|
|
272dc142c5 | ||
|
|
bf1d207651 | ||
|
|
6f0e55d603 | ||
|
|
ebe16f92a5 | ||
|
|
4f6a5759aa | ||
|
|
d85eb1b747 | ||
|
|
41a1124585 | ||
|
|
98853a0708 | ||
|
|
95bde675ef | ||
|
|
fcde243117 | ||
|
|
9fd7f7da34 | ||
|
|
453f02cc85 | ||
|
|
a6659cbe96 | ||
|
|
e13fed8670 | ||
|
|
25f589ee73 | ||
|
|
208f0f5f69 | ||
|
|
951e15c665 | ||
|
|
cc35fe503f | ||
|
|
37aadb86fb | ||
|
|
f843ea48f0 | ||
|
|
a48e2e6f06 | ||
|
|
0f7e11bd86 | ||
|
|
dbded6eee2 | ||
|
|
c1d7cc37fd | ||
|
|
6071b98fb7 | ||
|
|
dca47d35f7 | ||
|
|
d8656ec149 | ||
|
|
f1873876b2 | ||
|
|
cb20317047 | ||
|
|
ae58f009fb | ||
|
|
d7a3a417e1 | ||
|
|
68c6c93945 | ||
|
|
4c256e97c7 | ||
|
|
7478217ad4 | ||
|
|
ea33a3b65e | ||
|
|
f4e8f688ad | ||
|
|
4c4969d95a | ||
|
|
676a5d11f5 | ||
|
|
6b57ad89b7 | ||
|
|
174deddf35 | ||
|
|
782a25bba8 | ||
|
|
24ed5491fd | ||
|
|
88de978a9c | ||
|
|
eb002ae306 | ||
|
|
2550306052 | ||
|
|
51fcb90dc0 | ||
|
|
b1ad319d52 | ||
|
|
12826a57bd | ||
|
|
5bda7fd45c | ||
|
|
30725136c8 | ||
|
|
571b6089a4 | ||
|
|
32b4bca745 | ||
|
|
f838314435 | ||
|
|
08d6376eed | ||
|
|
3cdc6c41c4 | ||
|
|
973ac28dcb | ||
|
|
a855b422c7 | ||
|
|
28673c1bb8 | ||
|
|
df4b16e485 | ||
|
|
936707f73b | ||
|
|
9e2a601d52 | ||
|
|
1d074cda40 | ||
|
|
5850e0d489 | ||
|
|
904615a51a | ||
|
|
763fb86ad0 | ||
|
|
fbf6d97f5e | ||
|
|
c85466995d | ||
|
|
514d6e6514 | ||
|
|
351bc87231 | ||
|
|
ac742e9da2 | ||
|
|
0581da44fe | ||
|
|
2825c4c63d | ||
|
|
fa7d044037 | ||
|
|
d42fa22450 | ||
|
|
7307a9a1b7 | ||
|
|
bf80367b5a | ||
|
|
a04646b7b2 | ||
|
|
cfe3f8e3d9 | ||
|
|
2d0cff2dc1 | ||
|
|
b24157ddf9 | ||
|
|
c57b5ba1ad | ||
|
|
fe646511d1 | ||
|
|
cc31846152 | ||
|
|
cb4938c5f8 | ||
|
|
b1055e814a | ||
|
|
13951c13df | ||
|
|
60fbe7f714 | ||
|
|
595817852d | ||
|
|
2e0124710a | ||
|
|
340fadd9be | ||
|
|
4bdc1d76c6 | ||
|
|
738c06ada6 | ||
|
|
93bb0f098b | ||
|
|
e8c8a297b5 | ||
|
|
f4f7879ff3 | ||
|
|
706108256b | ||
|
|
12f0614350 | ||
|
|
29519df906 | ||
|
|
6b8f9aa6ab | ||
|
|
e3a211e41c | ||
|
|
fa80be87be | ||
|
|
51206f4037 | ||
|
|
c2fffacbbd | ||
|
|
02f631fdb6 | ||
|
|
05a66ead6e | ||
|
|
97f0314fe6 | ||
|
|
a7fe4193e3 | ||
|
|
2c5e84b6b3 | ||
|
|
71a66e0528 | ||
|
|
a807ce27f1 | ||
|
|
58bb8b9843 | ||
|
|
2e9bec95b0 | ||
|
|
2f419996ab | ||
|
|
1ba588272c | ||
|
|
2c3b7409aa | ||
|
|
f239f2de18 | ||
|
|
18b7b5f277 | ||
|
|
0e612de4d1 | ||
|
|
52ae5c3a5f | ||
|
|
d1fe1b89ae | ||
|
|
1aa8521e15 | ||
|
|
95ebb6a492 | ||
|
|
a74aaa5b13 | ||
|
|
4bf4b66b27 | ||
|
|
57484fd63d | ||
|
|
3a40b6b6d7 | ||
|
|
2781da3221 | ||
|
|
4629673161 | ||
|
|
fe30438854 | ||
|
|
291fca2b17 | ||
|
|
6fd54c7e6e | ||
|
|
a9e4d8a0a1 | ||
|
|
f3c0d5d422 | ||
|
|
a620c8658b | ||
|
|
d59cfb1fa2 | ||
|
|
ca65dd9000 | ||
|
|
6c2f229d31 | ||
|
|
eba7e15358 | ||
|
|
e42719c9df | ||
|
|
2995a00cd0 | ||
|
|
9f34613473 | ||
|
|
430bcb17c2 | ||
|
|
37bf993759 | ||
|
|
886a92a795 | ||
|
|
2b01b8168f | ||
|
|
35aacf7a48 | ||
|
|
0e0044f840 | ||
|
|
76dfc03751 | ||
|
|
ca079a72cc | ||
|
|
471c5b89f4 | ||
|
|
3bf8211b70 | ||
|
|
ec81d5904d | ||
|
|
82dcba542a | ||
|
|
63e0d5c7c2 | ||
|
|
772243e832 | ||
|
|
bad13d76b4 | ||
|
|
0bde4d9412 | ||
|
|
239b108fa7 | ||
|
|
c5ccbd37e2 | ||
|
|
822fb3748a | ||
|
|
aa2e443eb8 | ||
|
|
82d477009d | ||
|
|
e49081da80 | ||
|
|
07c7d3931d | ||
|
|
cf59a93cf1 | ||
|
|
e35e7585e0 | ||
|
|
fcb97c3c06 | ||
|
|
0edee4d066 | ||
|
|
b9937e6859 | ||
|
|
59012c50b4 | ||
|
|
7a98878273 | ||
|
|
8eb527389e | ||
|
|
78b2c1a273 | ||
|
|
497c0700b5 | ||
|
|
bac12010aa | ||
|
|
dad33a850c | ||
|
|
0968fc98ee | ||
|
|
ff44d88f21 | ||
|
|
1e7baee9d7 | ||
|
|
d9342acf5b | ||
|
|
b3f1ab6579 | ||
|
|
f5c9b2404a | ||
|
|
8b1fe21e4e | ||
|
|
815c59ff6d | ||
|
|
90318dfb6b | ||
|
|
f3d2f474a7 | ||
|
|
12140098e6 | ||
|
|
c7d8081e9a | ||
|
|
a10067e6b6 | ||
|
|
28e9fb48b6 | ||
|
|
634f3fcf14 | ||
|
|
88597e1834 | ||
|
|
69b3e1f8a7 | ||
|
|
669d8898ac | ||
|
|
14f0f79061 | ||
|
|
600ff07986 | ||
|
|
1d74b5e311 | ||
|
|
c0fe6f4aee | ||
|
|
aa7053bbe8 | ||
|
|
99f24eb598 | ||
|
|
6790a144a1 | ||
|
|
cd3d2110d9 | ||
|
|
b404241d0b | ||
|
|
2d42d6dc60 |
27
.github/move.yml
vendored
27
.github/move.yml
vendored
@@ -1,27 +0,0 @@
|
||||
# Configuration for Move Issues - https://github.com/dessant/move-issues
|
||||
|
||||
# Delete the command comment when it contains no other content
|
||||
deleteCommand: true
|
||||
|
||||
# Close the source issue after moving
|
||||
closeSourceIssue: true
|
||||
|
||||
# Lock the source issue after moving
|
||||
lockSourceIssue: false
|
||||
|
||||
# Mention issue and comment authors
|
||||
mentionAuthors: true
|
||||
|
||||
# Preserve mentions in the issue content
|
||||
keepContentMentions: true
|
||||
|
||||
# Move labels that also exist on the target repository
|
||||
moveLabels: true
|
||||
|
||||
# Set custom aliases for targets
|
||||
# aliases:
|
||||
# r: repo
|
||||
# or: owner/repo
|
||||
|
||||
# Repository to extend settings from
|
||||
# _extends: repo
|
||||
94
.github/workflows/ci.yml
vendored
94
.github/workflows/ci.yml
vendored
@@ -3,46 +3,45 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
Macos:
|
||||
runs-on: macos-latest
|
||||
macOS:
|
||||
runs-on: macos-11
|
||||
env:
|
||||
HOME: /Users/runner
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Retrieve source code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
brew update
|
||||
brew install gcovr pkg-config ninja || brew link --overwrite python
|
||||
- name: Install python modules
|
||||
run: pip3 install meson==0.49.2 pytest
|
||||
- name: Install deps
|
||||
shell: bash
|
||||
brew unlink python3
|
||||
# upgrade from python@3.11.2_1 to python@3.11.3 fails to overwrite those
|
||||
rm -f /usr/local/bin/2to3 /usr/local/bin/2to3-3.11 /usr/local/bin/idle3 /usr/local/bin/idle3.11 /usr/local/bin/pydoc3 /usr/local/bin/pydoc3.11 /usr/local/bin/python3 /usr/local/bin/python3-config /usr/local/bin/python3.11 /usr/local/bin/python3.11-config
|
||||
brew install pkg-config ninja meson
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
ARCHIVE_NAME: deps2_macos_native_dyn_libkiwix.tar.xz
|
||||
run: |
|
||||
ARCHIVE_NAME=deps2_osx_native_dyn_libkiwix.tar.xz
|
||||
wget -O- http://tmp.kiwix.org/ci/${ARCHIVE_NAME} | tar -xJ -C $HOME
|
||||
- name: Compile
|
||||
shell: bash
|
||||
wget -O- https://tmp.kiwix.org/ci/${{env.ARCHIVE_NAME}} | tar -xJ -C ${{env.HOME}}
|
||||
|
||||
- name: Compile source code
|
||||
env:
|
||||
PKG_CONFIG_PATH: ${{env.HOME}}/BUILD_native_dyn/INSTALL/lib/pkgconfig
|
||||
CPPFLAGS: -I${{env.HOME}}/BUILD_native_dyn/INSTALL/include
|
||||
run: |
|
||||
export PKG_CONFIG_PATH=$HOME/BUILD_native_dyn/INSTALL/lib/pkgconfig
|
||||
export CPPFLAGS="-I$HOME/BUILD_native_dyn/INSTALL/include"
|
||||
meson . build --default-library=shared -Db_coverage=true
|
||||
cd build
|
||||
ninja
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: |
|
||||
export LD_LIBRARY_PATH=$HOME/BUILD_native_dyn/INSTALL/lib:$HOME/BUILD_native_dyn/INSTALL/lib64
|
||||
cd build
|
||||
meson test --verbose
|
||||
ninja -C build
|
||||
|
||||
- name: Test libkiwix
|
||||
env:
|
||||
SKIP_BIG_MEMORY_TEST: 1
|
||||
LD_LIBRARY_PATH: ${{env.HOME}}/BUILD_native_dyn/INSTALL/lib:${{env.HOME}}/BUILD_native_dyn/INSTALL/lib64
|
||||
run: meson test -C build --verbose
|
||||
|
||||
Linux:
|
||||
strategy:
|
||||
@@ -58,19 +57,19 @@ jobs:
|
||||
include:
|
||||
- name: native_static
|
||||
target: native_static
|
||||
image_variant: bionic
|
||||
image_variant: focal
|
||||
lib_postfix: '/x86_64-linux-gnu'
|
||||
- name: native_dyn
|
||||
target: native_dyn
|
||||
image_variant: bionic
|
||||
image_variant: focal
|
||||
lib_postfix: '/x86_64-linux-gnu'
|
||||
- name: android_arm
|
||||
target: android_arm
|
||||
image_variant: bionic
|
||||
image_variant: focal
|
||||
lib_postfix: '/arm-linux-androideabi'
|
||||
- name: android_arm64
|
||||
target: android_arm64
|
||||
image_variant: bionic
|
||||
image_variant: focal
|
||||
lib_postfix: '/aarch64-linux-android'
|
||||
- name: win32_static
|
||||
target: win32_static
|
||||
@@ -82,22 +81,12 @@ jobs:
|
||||
lib_postfix: '64'
|
||||
env:
|
||||
HOME: /home/runner
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
container:
|
||||
image: "kiwix/kiwix-build_ci:${{matrix.image_variant}}-31"
|
||||
image: "ghcr.io/kiwix/kiwix-build_ci_${{matrix.image_variant}}:37"
|
||||
steps:
|
||||
- name: Checkout code
|
||||
shell: python
|
||||
run: |
|
||||
from subprocess import check_call
|
||||
from os import environ
|
||||
command = [
|
||||
'git', 'clone',
|
||||
'https://github.com/${{github.repository}}',
|
||||
'--depth=1',
|
||||
'--branch', '${{ github.head_ref || github.ref_name }}'
|
||||
]
|
||||
check_call(command, cwd=environ['HOME'])
|
||||
uses: actions/checkout@v3
|
||||
- name: Install deps
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -120,7 +109,6 @@ jobs:
|
||||
if [[ "${{matrix.target}}" =~ android_.* ]]; then
|
||||
MESON_OPTION="$MESON_OPTION -Dstatic-linkage=true"
|
||||
fi
|
||||
cd $HOME/libkiwix
|
||||
meson . build ${MESON_OPTION}
|
||||
cd build
|
||||
ninja
|
||||
@@ -131,19 +119,15 @@ jobs:
|
||||
if: startsWith(matrix.target, 'native_')
|
||||
shell: bash
|
||||
run: |
|
||||
cd $HOME/libkiwix/build
|
||||
cd build
|
||||
meson test --verbose
|
||||
ninja coverage
|
||||
env:
|
||||
LD_LIBRARY_PATH: "/home/runner/BUILD_${{matrix.target}}/INSTALL/lib:/home/runner/BUILD_${{matrix.target}}/INSTALL/lib${{matrix.lib_postfix}}"
|
||||
SKIP_BIG_MEMORY_TEST: 1
|
||||
|
||||
- name: Publish coverage
|
||||
shell: bash
|
||||
run: |
|
||||
cd $HOME/libkiwix
|
||||
curl https://codecov.io/bash -o codecov.sh
|
||||
bash codecov.sh -n "${OS_NAME}_${{matrix.target}}" -Z
|
||||
rm codecov.sh
|
||||
if: startsWith(matrix.target, 'native_')
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
24
.github/workflows/package.yml
vendored
24
.github/workflows/package.yml
vendored
@@ -1,5 +1,10 @@
|
||||
name: Packages
|
||||
on: [push, pull_request]
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-deb:
|
||||
@@ -11,9 +16,8 @@ jobs:
|
||||
- ubuntu-kinetic
|
||||
- ubuntu-jammy
|
||||
- ubuntu-focal
|
||||
- ubuntu-bionic
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# Determine which PPA we should upload to
|
||||
- name: PPA
|
||||
@@ -58,23 +62,15 @@ jobs:
|
||||
args: --no-sign
|
||||
ppa: ${{ steps.ppa.outputs.ppa }}
|
||||
|
||||
- uses: legoktm/gh-action-build-deb@ubuntu-bionic
|
||||
if: matrix.distro == 'ubuntu-bionic'
|
||||
name: Build package for ubuntu-bionic
|
||||
id: build-ubuntu-bionic
|
||||
with:
|
||||
args: --no-sign
|
||||
ppa: ${{ steps.ppa.outputs.ppa }}
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Packages for ${{ matrix.distro }}
|
||||
path: output
|
||||
|
||||
- uses: legoktm/gh-action-dput@master
|
||||
name: Upload dev package
|
||||
# Only upload on pushes to master
|
||||
if: github.event_name == 'push' && github.event.ref == 'refs/heads/master' && startswith(matrix.distro, 'ubuntu-')
|
||||
# Only upload on pushes to git default branch
|
||||
if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' && startswith(matrix.distro, 'ubuntu-')
|
||||
with:
|
||||
gpg_key: ${{ secrets.LAUNCHPAD_GPG }}
|
||||
repository: ppa:kiwixteam/dev
|
||||
|
||||
21
.readthedocs.yaml
Normal file
21
.readthedocs.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the version of Python and other tools you might need
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
# We recommend specifying your dependencies to enable reproducible builds:
|
||||
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
37
ChangeLog
37
ChangeLog
@@ -1,3 +1,40 @@
|
||||
libkiwix 12.1.0
|
||||
===============
|
||||
|
||||
* Server:
|
||||
- Introduce a `/nojs` endpoint to browse catalog and zim files with a browser without js (@juuz0 #897)
|
||||
- Translate the viewer (@veloman-yunkan #871 #846)
|
||||
- Display `mul` on tile when zim is multi-languages (@juuz0 #934)
|
||||
- Suggestion links point to the `/content` endpoint (@veloman-yunkan #862)
|
||||
- Correctly compress web fonts in http answers (@kelson42 #856)
|
||||
- Correctly encode link in suggestions (@veloman-yunkan #859 #860 #963)
|
||||
- Correctly encode url redirection (@veloman-yunkan #866 #890)
|
||||
- Properly handle user language, through cookies and http headers (@veloman-yunkan #849 #869)
|
||||
- Fix url encoding (@veloman-yunkan #870)
|
||||
- Fix viewer for viewer for SeaMonkey (@veloman-yunkan #887)
|
||||
- Make the downloader threadsafe (@mgautierfr #886)
|
||||
- Add RSS feed in the main page (pointing to the catalog) (@juuz0 #882 #920)
|
||||
- Correctly set the mimetype for json and ico (@veloman-yunkan #892)
|
||||
- `count=-1` correspond to unlimited count (instead of 0) (@veloman-yunkan #894)
|
||||
- Keep the navigation bar on top (@juuz0 #896)
|
||||
- Make the viewer's iframe "safe" (@veloman-yunkan #906 #930)
|
||||
- Correctly escape search link in XML Opds output (@veloman-yunkan #936)
|
||||
- Store values needed for the viewer js in the url fragment instead of the query string (@juuz0 #907)
|
||||
- Get rid of legacy OPDS API usage in the viewer (@veloman-yunkan #939)
|
||||
- Fix charset encoding declaration in OPDS response MIME types (@veloman-yunkan #942)
|
||||
- Fix PDF in the viewer (@veloman-yunkan #940)
|
||||
- Fix external links handling in the viewer (@veloman-yunkan #959)
|
||||
- Add tests of searching with accents (@mgautierfs #954)
|
||||
* Fix handling of missing illustration in the book (@veloman-yunkan #961)
|
||||
* Add support for multi languages zim files (@veloman-yunkan #904)
|
||||
* Fix includes for openbsd (@bentley #949)
|
||||
* Fix pathes in git to allow git clone on Windows (@adamlamar #868)
|
||||
* Switch to `main` as principal branch (instead of `master`) (@kelson42)
|
||||
* Remove libkiwix android publisher from the repository (@kelson42 #884)
|
||||
* Various fixes of meson and CI. (@mgautierfr @kelson42)
|
||||
|
||||
|
||||
|
||||
libkiwix 12.0.0
|
||||
===============
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ GNU/Linux, macOS, Android, iOS, ...).
|
||||
|
||||
[](https://download.kiwix.org/release/libkiwix/)
|
||||
[](https://github.com/kiwix/libkiwix/wiki/Repology)
|
||||
[](https://github.com/kiwix/libkiwix/actions?query=branch%3Amaster)
|
||||
[](https://github.com/kiwix/libkiwix/actions?query=branch%3Amain)
|
||||
[](https://libkiwix.readthedocs.org/en/latest/?badge=latest)
|
||||
[](https://www.codefactor.io/repository/github/kiwix/libkiwix)
|
||||
[](https://codecov.io/gh/kiwix/libkiwix)
|
||||
[](https://codecov.io/gh/kiwix/libkiwix)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
Disclaimer
|
||||
@@ -190,7 +190,7 @@ To use JS provided by kiwix-serve you can use the following template to start wi
|
||||
- To get books listed using `index.js` add - `<div class="book__list"></div>` under body tag.
|
||||
- To get number of books listed add - `<h3 class="kiwixHomeBody__results"></h3>` under body tag.
|
||||
- To add language select box add - `<select id="languageFilter"></select>` under body tag.
|
||||
- To add language select box add - `<select id="categoryFilter"></select>` under body tag.
|
||||
- To add category select box add - `<select id="categoryFilter"></select>` under body tag.
|
||||
- To add search box for books use following form -
|
||||
```
|
||||
<form id='kiwixSearchForm'>
|
||||
|
||||
13
android-kiwix-lib-publisher/.gitignore
vendored
13
android-kiwix-lib-publisher/.gitignore
vendored
@@ -1,13 +0,0 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
@@ -1,25 +0,0 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.4.1'
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
Binary file not shown.
@@ -1,6 +0,0 @@
|
||||
#Wed Jun 19 15:28:39 BST 2019
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
|
||||
172
android-kiwix-lib-publisher/gradlew
vendored
172
android-kiwix-lib-publisher/gradlew
vendored
@@ -1,172 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
84
android-kiwix-lib-publisher/gradlew.bat
vendored
84
android-kiwix-lib-publisher/gradlew.bat
vendored
@@ -1,84 +0,0 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,64 +0,0 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'maven'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
defaultConfig {
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 28
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.getkeepsafe.relinker:relinker:1.3.1'
|
||||
}
|
||||
|
||||
task writePom {
|
||||
pom {
|
||||
project {
|
||||
groupId 'org.kiwix.kiwixlib'
|
||||
artifactId 'kiwixlib'
|
||||
version '10.1.1' + (System.env.KIWIXLIB_BUILDVERSION == null ? '' : '-'+System.env.KIWIXLIB_BUILDVERSION)
|
||||
packaging 'aar'
|
||||
name 'kiwixlib'
|
||||
url 'https://github.com/kiwix/libkiwix'
|
||||
licenses {
|
||||
license {
|
||||
name 'GPLv3'
|
||||
url 'https://www.gnu.org/licenses/gpl-3.0.en.html'
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id 'kiwix'
|
||||
name 'kiwix'
|
||||
email 'contact@kiwix.org'
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection 'https://github.com/kiwix/libkiwix.git'
|
||||
developerConnection 'https://github.com/kiwix/libkiwix.git'
|
||||
url 'https://github.com/kiwix/libkiwix'
|
||||
}
|
||||
}
|
||||
}.withXml {
|
||||
def dependenciesNode = asNode().appendNode('dependencies')
|
||||
|
||||
//Iterate over the implementation dependencies, adding a <dependency> node for each
|
||||
configurations.implementation.allDependencies.each {
|
||||
def dependencyNode = dependenciesNode.appendNode('dependency')
|
||||
dependencyNode.appendNode('groupId', it.group)
|
||||
dependencyNode.appendNode('artifactId', it.name)
|
||||
dependencyNode.appendNode('version', it.version)
|
||||
}
|
||||
}.writeTo("$buildDir/pom.xml")
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -1,10 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.kiwix.kiwixlib">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:supportsRtl="true">
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1 +0,0 @@
|
||||
include ':kiwixLibAndroid'
|
||||
@@ -79,7 +79,9 @@ class Book
|
||||
bool isPathValid() const { return m_pathValid; }
|
||||
const std::string& getTitle() const { return m_title; }
|
||||
const std::string& getDescription() const { return m_description; }
|
||||
const std::string& getLanguage() const { return m_language; }
|
||||
DEPRECATED const std::string& getLanguage() const { return m_language; }
|
||||
const std::string& getCommaSeparatedLanguages() const { return m_language; }
|
||||
const std::vector<std::string> getLanguages() const;
|
||||
const std::string& getCreator() const { return m_creator; }
|
||||
const std::string& getPublisher() const { return m_publisher; }
|
||||
const std::string& getDate() const { return m_date; }
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <mutex>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
@@ -43,6 +44,14 @@ class AriaError : public std::runtime_error {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A representation of a current download.
|
||||
*
|
||||
* `Download` is not thread safe. User must care to not call method on a
|
||||
* same download from different threads.
|
||||
* However, it is safe to use different `Download`s from different threads.
|
||||
*/
|
||||
|
||||
class Download {
|
||||
public:
|
||||
typedef enum { K_ACTIVE, K_WAITING, K_PAUSED, K_ERROR, K_COMPLETE, K_REMOVED, K_UNKNOWN } StatusResult;
|
||||
@@ -53,19 +62,89 @@ class Download {
|
||||
: mp_aria(p_aria),
|
||||
m_status(K_UNKNOWN),
|
||||
m_did(did) {};
|
||||
void updateStatus(bool follow=false);
|
||||
|
||||
/**
|
||||
* Update the status of the download.
|
||||
*
|
||||
* This call make an aria rpc call and is blocking.
|
||||
* Some download (started with a metalink) are in fact several downloads.
|
||||
* - A first one to download the metadlink.
|
||||
* - A second one to download the real file.
|
||||
*
|
||||
* If `follow` is true, updateStatus tries to detect that and tracks
|
||||
* the second download when the first one is finished.
|
||||
* By passing false to `follow`, `Download` will only track the first download.
|
||||
*
|
||||
* `getFoo` methods are based on the last statusUpdate.
|
||||
*
|
||||
* @param follow: Do we have to follow following downloads.
|
||||
*/
|
||||
void updateStatus(bool follow);
|
||||
|
||||
/**
|
||||
* Pause the download (and call updateStatus)
|
||||
*/
|
||||
void pauseDownload();
|
||||
|
||||
/**
|
||||
* Resume the download (and call updateStatus)
|
||||
*/
|
||||
void resumeDownload();
|
||||
|
||||
/**
|
||||
* Cancel the download.
|
||||
*
|
||||
* A canceled downlod cannot be resume and updateStatus does nothing.
|
||||
* However, you can still get information based on the last known information.
|
||||
*/
|
||||
void cancelDownload();
|
||||
StatusResult getStatus() { return m_status; }
|
||||
std::string getDid() { return m_did; }
|
||||
std::string getFollowedBy() { return m_followedBy; }
|
||||
uint64_t getTotalLength() { return m_totalLength; }
|
||||
uint64_t getCompletedLength() { return m_completedLength; }
|
||||
uint64_t getDownloadSpeed() { return m_downloadSpeed; }
|
||||
uint64_t getVerifiedLength() { return m_verifiedLength; }
|
||||
std::string getPath() { return m_path; }
|
||||
std::vector<std::string>& getUris() { return m_uris; }
|
||||
|
||||
/*
|
||||
* Get the status of the download.
|
||||
*/
|
||||
StatusResult getStatus() const { return m_status; }
|
||||
|
||||
/*
|
||||
* Get the id of the download.
|
||||
*/
|
||||
const std::string& getDid() const { return m_did; }
|
||||
|
||||
/*
|
||||
* Get the id of the "second" download.
|
||||
*
|
||||
* Set only if the "first" download is a metalink and is complete.
|
||||
*/
|
||||
const std::string& getFollowedBy() const { return m_followedBy; }
|
||||
|
||||
/*
|
||||
* Get the total length of the download.
|
||||
*/
|
||||
uint64_t getTotalLength() const { return m_totalLength; }
|
||||
|
||||
/*
|
||||
* Get the completed length of the download.
|
||||
*/
|
||||
uint64_t getCompletedLength() const { return m_completedLength; }
|
||||
|
||||
/*
|
||||
* Get the download speed of the download.
|
||||
*/
|
||||
uint64_t getDownloadSpeed() const { return m_downloadSpeed; }
|
||||
|
||||
/*
|
||||
* Get the verified length of the download.
|
||||
*/
|
||||
uint64_t getVerifiedLength() const { return m_verifiedLength; }
|
||||
|
||||
/*
|
||||
* Get the path (local file) of the download.
|
||||
*/
|
||||
const std::string& getPath() const { return m_path; }
|
||||
|
||||
/*
|
||||
* Get the download uris of the download.
|
||||
*/
|
||||
const std::vector<std::string>& getUris() const { return m_uris; }
|
||||
|
||||
protected:
|
||||
std::shared_ptr<Aria2> mp_aria;
|
||||
@@ -83,6 +162,9 @@ class Download {
|
||||
/**
|
||||
* A tool to download things.
|
||||
*
|
||||
* A Downloader manages `Download` using aria2 in the background.
|
||||
* `Downloader` is threadsafe.
|
||||
* However, the returned `Download`s are NOT threadsafe.
|
||||
*/
|
||||
class Downloader
|
||||
{
|
||||
@@ -92,14 +174,41 @@ class Downloader
|
||||
|
||||
void close();
|
||||
|
||||
Download* startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options = {});
|
||||
Download* getDownload(const std::string& did);
|
||||
/**
|
||||
* Start a new download.
|
||||
*
|
||||
* This method is thread safe and return a pointer to a newly created `Download`.
|
||||
* User should call `update` on the returned `Download` to have an accurate status.
|
||||
*
|
||||
* @param uri: The uri of the thing to download.
|
||||
* @param options: A series of pair <option_name, option_value> to pass to aria.
|
||||
* @return: The newly created Download.
|
||||
*/
|
||||
std::shared_ptr<Download> startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options = {});
|
||||
|
||||
size_t getNbDownload() { return m_knownDownloads.size(); }
|
||||
std::vector<std::string> getDownloadIds();
|
||||
/**
|
||||
* Get a download corrsponding to a download id (did)
|
||||
* User should call `update` on the returned `Download` to have an accurate status.
|
||||
*
|
||||
* @param did: The download id to search for.
|
||||
* @return: The Download corresponding to did.
|
||||
* @throw: Throw std::out_of_range if did is not found.
|
||||
*/
|
||||
std::shared_ptr<Download> getDownload(const std::string& did);
|
||||
|
||||
/**
|
||||
* Get the number of downloads currently managed.
|
||||
*/
|
||||
size_t getNbDownload() const;
|
||||
|
||||
/**
|
||||
* Get the ids of the managed downloads.
|
||||
*/
|
||||
std::vector<std::string> getDownloadIds() const;
|
||||
|
||||
private:
|
||||
std::map<std::string, std::unique_ptr<Download>> m_knownDownloads;
|
||||
mutable std::mutex m_lock;
|
||||
std::map<std::string, std::shared_ptr<Download>> m_knownDownloads;
|
||||
std::shared_ptr<Aria2> mp_aria;
|
||||
};
|
||||
}
|
||||
|
||||
50
include/html_dumper.h
Normal file
50
include/html_dumper.h
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2023 Nikhil Tanwar <2002nikhiltanwar@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef KIWIX_HTML_DUMPER_H
|
||||
#define KIWIX_HTML_DUMPER_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "library_dumper.h"
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
/**
|
||||
* A class to dump Library in HTML format.
|
||||
*/
|
||||
class HTMLDumper : public LibraryDumper
|
||||
{
|
||||
public:
|
||||
HTMLDumper(const Library* library, const NameMapper* NameMapper);
|
||||
~HTMLDumper();
|
||||
|
||||
|
||||
/**
|
||||
* Dump library in HTML
|
||||
*
|
||||
* @return HTML content
|
||||
*/
|
||||
std::string dumpPlainHTML(kiwix::Filter filter) const;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // KIWIX_HTML_DUMPER_H
|
||||
@@ -120,6 +120,8 @@ class Filter {
|
||||
Filter& maxSize(size_t size);
|
||||
Filter& query(std::string query, bool partial=true);
|
||||
Filter& name(std::string name);
|
||||
Filter& clearLang();
|
||||
Filter& clearCategory();
|
||||
|
||||
bool hasQuery() const;
|
||||
const std::string& getQuery() const { return _query; }
|
||||
|
||||
91
include/library_dumper.h
Normal file
91
include/library_dumper.h
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2023 Nikhil Tanwar <2002nikhiltanwar@gmail.com>
|
||||
* Copyright 2017 Matthieu Gautier <mgautier@kymeria.fr>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef KIWIX_LIBRARY_DUMPER_H
|
||||
#define KIWIX_LIBRARY_DUMPER_H
|
||||
|
||||
#include <string>
|
||||
|
||||
|
||||
#include "library.h"
|
||||
#include "name_mapper.h"
|
||||
#include <mustache.hpp>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
/**
|
||||
* A base class to dump Library in various formats.
|
||||
*
|
||||
*/
|
||||
class LibraryDumper
|
||||
{
|
||||
public:
|
||||
LibraryDumper(const Library* library, const NameMapper* NameMapper);
|
||||
~LibraryDumper();
|
||||
|
||||
void setLibraryId(const std::string& id) { this->libraryId = id;}
|
||||
|
||||
/**
|
||||
* Set the root location used when generating url.
|
||||
*
|
||||
* @param rootLocation the root location to use.
|
||||
*/
|
||||
void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; }
|
||||
|
||||
/**
|
||||
* Set some informations about the search results.
|
||||
*
|
||||
* @param totalResult the total number of results of the search.
|
||||
* @param startIndex the start index of the result.
|
||||
* @param count the number of result of the current set (or page).
|
||||
*/
|
||||
void setOpenSearchInfo(int totalResult, int startIndex, int count);
|
||||
|
||||
/**
|
||||
* Sets user default language
|
||||
*
|
||||
* @param userLang the user language to be set
|
||||
*/
|
||||
void setUserLanguage(std::string userLang) { this->m_userLang = userLang; }
|
||||
|
||||
/**
|
||||
* Get the data of categories
|
||||
*/
|
||||
kainjow::mustache::list getCategoryData() const;
|
||||
|
||||
/**
|
||||
* Get the data of languages
|
||||
*/
|
||||
kainjow::mustache::list getLanguageData() const;
|
||||
|
||||
protected:
|
||||
const kiwix::Library* const library;
|
||||
const kiwix::NameMapper* const nameMapper;
|
||||
std::string libraryId;
|
||||
std::string rootLocation;
|
||||
std::string m_userLang;
|
||||
int m_totalResults;
|
||||
int m_startIndex;
|
||||
int m_count;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // KIWIX_LIBRARY_DUMPER_H
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
#include "library.h"
|
||||
#include "name_mapper.h"
|
||||
#include "library_dumper.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -38,11 +39,10 @@ namespace kiwix
|
||||
* A tool to dump a `Library` into a opds stream.
|
||||
*
|
||||
*/
|
||||
class OPDSDumper
|
||||
class OPDSDumper : public LibraryDumper
|
||||
{
|
||||
public:
|
||||
OPDSDumper() = default;
|
||||
OPDSDumper(Library* library, NameMapper* NameMapper);
|
||||
OPDSDumper(const Library* library, const NameMapper* NameMapper);
|
||||
~OPDSDumper();
|
||||
|
||||
/**
|
||||
@@ -85,38 +85,6 @@ class OPDSDumper
|
||||
* @return The OPDS feed.
|
||||
*/
|
||||
std::string languagesOPDSFeed() const;
|
||||
|
||||
/**
|
||||
* Set the id of the library.
|
||||
*
|
||||
* @param id the id to use.
|
||||
*/
|
||||
void setLibraryId(const std::string& id) { this->libraryId = id;}
|
||||
|
||||
/**
|
||||
* Set the root location used when generating url.
|
||||
*
|
||||
* @param rootLocation the root location to use.
|
||||
*/
|
||||
void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; }
|
||||
|
||||
/**
|
||||
* Set some informations about the search results.
|
||||
*
|
||||
* @param totalResult the total number of results of the search.
|
||||
* @param startIndex the start index of the result.
|
||||
* @param count the number of result of the current set (or page).
|
||||
*/
|
||||
void setOpenSearchInfo(int totalResult, int startIndex, int count);
|
||||
|
||||
protected:
|
||||
kiwix::Library* library;
|
||||
kiwix::NameMapper* nameMapper;
|
||||
std::string libraryId;
|
||||
std::string rootLocation;
|
||||
int m_totalResults;
|
||||
int m_startIndex;
|
||||
int m_count;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
project('libkiwix', 'cpp',
|
||||
version : '12.0.0',
|
||||
version : '12.1.0',
|
||||
license : 'GPLv3+',
|
||||
default_options : ['c_std=c11', 'cpp_std=c++11', 'werror=true'])
|
||||
|
||||
@@ -36,7 +36,7 @@ else
|
||||
endif
|
||||
|
||||
libzim_dep = dependency('libzim', version : '>=8.1.0', static:static_deps)
|
||||
if not compiler.has_header_symbol('zim/zim.h', 'LIBZIM_WITH_XAPIAN')
|
||||
if not compiler.has_header_symbol('zim/zim.h', 'LIBZIM_WITH_XAPIAN', dependencies: libzim_dep)
|
||||
error('Libzim seems to be compiled without xapian. Xapian support is mandatory.')
|
||||
endif
|
||||
|
||||
|
||||
@@ -202,15 +202,17 @@ if __name__ == "__main__":
|
||||
parser.add_argument('--source_dir',
|
||||
help="Additional directory where to look for resources.",
|
||||
action='append')
|
||||
parser.add_argument('resource_file',
|
||||
parser.add_argument('resource_files', nargs='+',
|
||||
help='The list of resources to compile.')
|
||||
args = parser.parse_args()
|
||||
|
||||
base_dir = os.path.dirname(os.path.realpath(args.resource_file))
|
||||
source_dir = args.source_dir or []
|
||||
with open(args.resource_file, 'r') as f:
|
||||
resources = [Resource([base_dir]+source_dir, *line.strip().split())
|
||||
for line in f.readlines()]
|
||||
resources = []
|
||||
for resfile in args.resource_files:
|
||||
base_dir = os.path.dirname(os.path.realpath(resfile))
|
||||
with open(resfile, 'r') as f:
|
||||
resources += [Resource([base_dir]+source_dir, *line.strip().split())
|
||||
for line in f.readlines()]
|
||||
|
||||
h_identifier = to_identifier(os.path.basename(args.hfile))
|
||||
with open(args.hfile, 'w') as f:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.SH NAME
|
||||
kiwix-compile-resources \- helper to compile and generate some Kiwix resources
|
||||
.SH SYNOPSIS
|
||||
\fBkiwix\-compile\-resources\fR [\-h] [\-\-cxxfile CXXFILE] [\-\-hfile HFILE] resource_file\fR
|
||||
\fBkiwix\-compile\-resources\fR [\-h] [\-\-cxxfile CXXFILE] [\-\-hfile HFILE] resource_file ...\fR
|
||||
.SH DESCRIPTION
|
||||
.TP
|
||||
resource_file
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#define LOG_ARIA_ERROR() \
|
||||
{ \
|
||||
std::cerr << "ERROR: aria2 RPC request failed. (" << res << ")." << std::endl; \
|
||||
std::cerr << (m_curlErrorBuffer[0] ? m_curlErrorBuffer.get() : curl_easy_strerror(res)) << std::endl; \
|
||||
std::cerr << (curlErrorBuffer[0] ? curlErrorBuffer : curl_easy_strerror(res)) << std::endl; \
|
||||
}
|
||||
|
||||
namespace kiwix {
|
||||
@@ -32,9 +32,7 @@ namespace kiwix {
|
||||
Aria2::Aria2():
|
||||
mp_aria(nullptr),
|
||||
m_port(42042),
|
||||
m_secret(getNewRpcSecret()),
|
||||
m_curlErrorBuffer(new char[CURL_ERROR_SIZE]),
|
||||
mp_curl(nullptr)
|
||||
m_secret(getNewRpcSecret())
|
||||
{
|
||||
m_downloadDir = getDataDirectory();
|
||||
makeDirectory(m_downloadDir);
|
||||
@@ -91,36 +89,32 @@ Aria2::Aria2():
|
||||
launchCmd.append(cmd).append(" ");
|
||||
}
|
||||
mp_aria = Subprocess::run(callCmd);
|
||||
mp_curl = curl_easy_init();
|
||||
|
||||
curl_easy_setopt(mp_curl, CURLOPT_URL, "http://localhost/rpc");
|
||||
curl_easy_setopt(mp_curl, CURLOPT_PORT, m_port);
|
||||
curl_easy_setopt(mp_curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(mp_curl, CURLOPT_ERRORBUFFER, m_curlErrorBuffer.get());
|
||||
CURL* p_curl = curl_easy_init();
|
||||
char curlErrorBuffer[CURL_ERROR_SIZE];
|
||||
|
||||
curl_easy_setopt(p_curl, CURLOPT_URL, "http://localhost/rpc");
|
||||
curl_easy_setopt(p_curl, CURLOPT_PORT, m_port);
|
||||
curl_easy_setopt(p_curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(p_curl, CURLOPT_ERRORBUFFER, curlErrorBuffer);
|
||||
|
||||
int watchdog = 50;
|
||||
while(--watchdog) {
|
||||
sleep(10);
|
||||
m_curlErrorBuffer[0] = 0;
|
||||
auto res = curl_easy_perform(mp_curl);
|
||||
curlErrorBuffer[0] = 0;
|
||||
auto res = curl_easy_perform(p_curl);
|
||||
if (res == CURLE_OK) {
|
||||
break;
|
||||
} else if (watchdog == 1) {
|
||||
LOG_ARIA_ERROR();
|
||||
}
|
||||
}
|
||||
curl_easy_cleanup(p_curl);
|
||||
if (!watchdog) {
|
||||
curl_easy_cleanup(mp_curl);
|
||||
throw std::runtime_error("Cannot connect to aria2c rpc. Aria2c launch cmd : " + launchCmd);
|
||||
}
|
||||
}
|
||||
|
||||
Aria2::~Aria2()
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_lock);
|
||||
curl_easy_cleanup(mp_curl);
|
||||
}
|
||||
|
||||
void Aria2::close()
|
||||
{
|
||||
saveSession();
|
||||
@@ -140,20 +134,25 @@ std::string Aria2::doRequest(const MethodCall& methodCall)
|
||||
std::stringstream outStream;
|
||||
CURLcode res;
|
||||
long response_code;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_lock);
|
||||
curl_easy_setopt(mp_curl, CURLOPT_POSTFIELDSIZE, requestContent.size());
|
||||
curl_easy_setopt(mp_curl, CURLOPT_POSTFIELDS, requestContent.c_str());
|
||||
curl_easy_setopt(mp_curl, CURLOPT_WRITEFUNCTION, &write_callback_to_iss);
|
||||
curl_easy_setopt(mp_curl, CURLOPT_WRITEDATA, &outStream);
|
||||
m_curlErrorBuffer[0] = 0;
|
||||
res = curl_easy_perform(mp_curl);
|
||||
if (res != CURLE_OK) {
|
||||
LOG_ARIA_ERROR();
|
||||
throw std::runtime_error("Cannot perform request");
|
||||
}
|
||||
curl_easy_getinfo(mp_curl, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
char curlErrorBuffer[CURL_ERROR_SIZE];
|
||||
CURL* p_curl = curl_easy_init();
|
||||
curl_easy_setopt(p_curl, CURLOPT_URL, "http://localhost/rpc");
|
||||
curl_easy_setopt(p_curl, CURLOPT_PORT, m_port);
|
||||
curl_easy_setopt(p_curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(p_curl, CURLOPT_ERRORBUFFER, curlErrorBuffer);
|
||||
curl_easy_setopt(p_curl, CURLOPT_POSTFIELDSIZE, requestContent.size());
|
||||
curl_easy_setopt(p_curl, CURLOPT_POSTFIELDS, requestContent.c_str());
|
||||
curl_easy_setopt(p_curl, CURLOPT_WRITEFUNCTION, &write_callback_to_iss);
|
||||
curl_easy_setopt(p_curl, CURLOPT_WRITEDATA, &outStream);
|
||||
curlErrorBuffer[0] = 0;
|
||||
res = curl_easy_perform(p_curl);
|
||||
if (res != CURLE_OK) {
|
||||
LOG_ARIA_ERROR();
|
||||
curl_easy_cleanup(p_curl);
|
||||
throw std::runtime_error("Cannot perform request");
|
||||
}
|
||||
curl_easy_getinfo(p_curl, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
curl_easy_cleanup(p_curl);
|
||||
|
||||
auto responseContent = outStream.str();
|
||||
if (response_code != 200) {
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include "xmlrpc.h"
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <curl/curl.h>
|
||||
|
||||
namespace kiwix {
|
||||
@@ -24,15 +23,11 @@ class Aria2
|
||||
int m_port;
|
||||
std::string m_secret;
|
||||
std::string m_downloadDir;
|
||||
std::unique_ptr<char[]> m_curlErrorBuffer;
|
||||
CURL* mp_curl;
|
||||
std::mutex m_lock;
|
||||
|
||||
std::string doRequest(const MethodCall& methodCall);
|
||||
|
||||
public:
|
||||
Aria2();
|
||||
virtual ~Aria2();
|
||||
virtual ~Aria2() = default;
|
||||
void close();
|
||||
|
||||
std::string addUri(const std::vector<std::string>& uri, const std::vector<std::pair<std::string, std::string>>& options = {});
|
||||
|
||||
14
src/book.cpp
14
src/book.cpp
@@ -117,11 +117,12 @@ void Book::updateFromXml(const pugi::xml_node& node, const std::string& baseDir)
|
||||
m_articleCount = strtoull(ATTR("articleCount"), 0, 0);
|
||||
m_mediaCount = strtoull(ATTR("mediaCount"), 0, 0);
|
||||
m_size = strtoull(ATTR("size"), 0, 0) << 10;
|
||||
std::string favicon_mimetype = ATTR("faviconMimeType");
|
||||
if (! favicon_mimetype.empty()) {
|
||||
const std::string faviconMimeType = ATTR("faviconMimeType");
|
||||
const std::string faviconBase64EncodedData = ATTR("favicon");
|
||||
if ( !faviconMimeType.empty() && !faviconBase64EncodedData.empty() ) {
|
||||
const auto favicon = std::make_shared<Illustration>();
|
||||
favicon->data = base64_decode(ATTR("favicon"));
|
||||
favicon->mimeType = favicon_mimetype;
|
||||
favicon->data = base64_decode(faviconBase64EncodedData);
|
||||
favicon->mimeType = faviconMimeType;
|
||||
favicon->url = ATTR("faviconUrl");
|
||||
m_illustrations.assign(1, favicon);
|
||||
}
|
||||
@@ -286,4 +287,9 @@ std::string Book::getCategoryFromTags() const
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::string> Book::getLanguages() const
|
||||
{
|
||||
return kiwix::split(m_language, ",");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -127,22 +127,24 @@ void Download::cancelDownload()
|
||||
Downloader::Downloader() :
|
||||
mp_aria(new Aria2())
|
||||
{
|
||||
try {
|
||||
for (auto gid : mp_aria->tellActive()) {
|
||||
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
|
||||
m_knownDownloads[gid]->updateStatus();
|
||||
}
|
||||
} catch (std::exception& e) {
|
||||
std::cerr << "aria2 tellActive failed : " << e.what() << std::endl;
|
||||
}
|
||||
try {
|
||||
for (auto gid : mp_aria->tellWaiting()) {
|
||||
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
|
||||
m_knownDownloads[gid]->updateStatus();
|
||||
m_knownDownloads[gid]->updateStatus(false);
|
||||
}
|
||||
} catch (std::exception& e) {
|
||||
std::cerr << "aria2 tellWaiting failed : " << e.what() << std::endl;
|
||||
}
|
||||
try {
|
||||
for (auto gid : mp_aria->tellActive()) {
|
||||
if( m_knownDownloads.find(gid) == m_knownDownloads.end()) {
|
||||
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
|
||||
m_knownDownloads[gid]->updateStatus(false);
|
||||
}
|
||||
}
|
||||
} catch (std::exception& e) {
|
||||
std::cerr << "aria2 tellActive failed : " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
/* Destructor */
|
||||
@@ -155,7 +157,8 @@ void Downloader::close()
|
||||
mp_aria->close();
|
||||
}
|
||||
|
||||
std::vector<std::string> Downloader::getDownloadIds() {
|
||||
std::vector<std::string> Downloader::getDownloadIds() const {
|
||||
std::unique_lock<std::mutex> lock(m_lock);
|
||||
std::vector<std::string> ret;
|
||||
for(auto& p:m_knownDownloads) {
|
||||
ret.push_back(p.first);
|
||||
@@ -163,42 +166,46 @@ std::vector<std::string> Downloader::getDownloadIds() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
Download* Downloader::startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options)
|
||||
std::shared_ptr<Download> Downloader::startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_lock);
|
||||
for (auto& p: m_knownDownloads) {
|
||||
auto& d = p.second;
|
||||
auto& uris = d->getUris();
|
||||
if (std::find(uris.begin(), uris.end(), uri) != uris.end())
|
||||
return d.get();
|
||||
return d;
|
||||
}
|
||||
std::vector<std::string> uris = {uri};
|
||||
auto gid = mp_aria->addUri(uris, options);
|
||||
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
|
||||
return m_knownDownloads[gid].get();
|
||||
m_knownDownloads[gid] = std::make_shared<Download>(mp_aria, gid);
|
||||
return m_knownDownloads[gid];
|
||||
}
|
||||
|
||||
Download* Downloader::getDownload(const std::string& did)
|
||||
std::shared_ptr<Download> Downloader::getDownload(const std::string& did)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_lock);
|
||||
try {
|
||||
m_knownDownloads.at(did).get()->updateStatus(true);
|
||||
return m_knownDownloads.at(did).get();
|
||||
return m_knownDownloads.at(did);
|
||||
} catch(std::exception& e) {
|
||||
for (auto gid : mp_aria->tellActive()) {
|
||||
if (gid == did) {
|
||||
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
|
||||
m_knownDownloads.at(gid).get()->updateStatus(true);
|
||||
return m_knownDownloads[gid].get();
|
||||
}
|
||||
}
|
||||
for (auto gid : mp_aria->tellWaiting()) {
|
||||
if (gid == did) {
|
||||
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
|
||||
m_knownDownloads.at(gid).get()->updateStatus(true);
|
||||
return m_knownDownloads[gid].get();
|
||||
m_knownDownloads[gid] = std::make_shared<Download>(mp_aria, gid);
|
||||
return m_knownDownloads[gid];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto gid : mp_aria->tellActive()) {
|
||||
if (gid == did) {
|
||||
m_knownDownloads[gid] = std::make_shared<Download>(mp_aria, gid);
|
||||
return m_knownDownloads[gid];
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
size_t Downloader::getNbDownload() const {
|
||||
std::unique_lock<std::mutex> lock(m_lock);
|
||||
return m_knownDownloads.size();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
120
src/html_dumper.cpp
Normal file
120
src/html_dumper.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "html_dumper.h"
|
||||
#include "libkiwix-resources.h"
|
||||
#include "tools/otherTools.h"
|
||||
#include "tools.h"
|
||||
#include "tools/regexTools.h"
|
||||
#include "server/i18n.h"
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
/* Constructor */
|
||||
HTMLDumper::HTMLDumper(const Library* library, const NameMapper* nameMapper)
|
||||
: LibraryDumper(library, nameMapper)
|
||||
{
|
||||
}
|
||||
/* Destructor */
|
||||
HTMLDumper::~HTMLDumper()
|
||||
{
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::string humanFriendlyTitle(std::string title)
|
||||
{
|
||||
std::string humanFriendlyString = replaceRegex(title, "_", " ");
|
||||
humanFriendlyString[0] = toupper(humanFriendlyString[0]);
|
||||
return humanFriendlyString;
|
||||
}
|
||||
|
||||
kainjow::mustache::list getTagList(std::string tags)
|
||||
{
|
||||
const auto tagsList = kiwix::split(tags, ";", true, false);
|
||||
kainjow::mustache::list finalTagList;
|
||||
for (auto tag : tagsList) {
|
||||
if (tag[0] != '_')
|
||||
finalTagList.push_back(kainjow::mustache::object{
|
||||
{"tag", tag}
|
||||
});
|
||||
}
|
||||
return finalTagList;
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::string HTMLDumper::dumpPlainHTML(kiwix::Filter filter) const
|
||||
{
|
||||
kainjow::mustache::list booksData;
|
||||
const auto filteredBooks = library->filter(filter);
|
||||
const auto searchQuery = filter.getQuery();
|
||||
auto languages = getLanguageData();
|
||||
auto categories = getCategoryData();
|
||||
|
||||
for (auto &category : categories) {
|
||||
const auto categoryName = category.get("name")->string_value();
|
||||
if (categoryName == filter.getCategory()) {
|
||||
category["selected"] = true;
|
||||
}
|
||||
category["hf_name"] = humanFriendlyTitle(categoryName);
|
||||
}
|
||||
|
||||
for (auto &language : languages) {
|
||||
if (language.get("lang_code")->string_value() == filter.getLang()) {
|
||||
language["selected"] = true;
|
||||
}
|
||||
}
|
||||
|
||||
for ( const auto& bookId : filteredBooks ) {
|
||||
const auto bookObj = library->getBookById(bookId);
|
||||
const auto bookTitle = bookObj.getTitle();
|
||||
std::string contentId = "";
|
||||
try {
|
||||
contentId = urlEncode(nameMapper->getNameForId(bookId));
|
||||
} catch (...) {}
|
||||
const auto bookDescription = bookObj.getDescription();
|
||||
const auto langCode = bookObj.getCommaSeparatedLanguages();
|
||||
const auto bookIconUrl = rootLocation + "/catalog/v2/illustration/" + bookId + "/?size=48";
|
||||
const auto tags = bookObj.getTags();
|
||||
const auto downloadAvailable = (bookObj.getUrl() != "");
|
||||
std::string faviconAttr = "style=background-image:url(" + bookIconUrl + ")";
|
||||
|
||||
booksData.push_back(kainjow::mustache::object{
|
||||
{"id", contentId},
|
||||
{"title", bookTitle},
|
||||
{"description", bookDescription},
|
||||
{"langCode", langCode},
|
||||
{"faviconAttr", faviconAttr},
|
||||
{"tagList", getTagList(tags)},
|
||||
{"downloadAvailable", downloadAvailable}
|
||||
});
|
||||
}
|
||||
|
||||
auto getTranslation = i18n::GetTranslatedStringWithMsgId(m_userLang);
|
||||
|
||||
const auto translations = kainjow::mustache::object{
|
||||
getTranslation("search"),
|
||||
getTranslation("download"),
|
||||
getTranslation("count-of-matching-books", {{"COUNT", to_string(filteredBooks.size())}}),
|
||||
getTranslation("book-filtering-all-categories"),
|
||||
getTranslation("book-filtering-all-languages"),
|
||||
getTranslation("powered-by-kiwix-html"),
|
||||
getTranslation("welcome-to-kiwix-server"),
|
||||
getTranslation("preview-book"),
|
||||
getTranslation("welcome-page-overzealous-filter", {{"URL", "?lang="}})
|
||||
};
|
||||
|
||||
return render_template(
|
||||
RESOURCE::templates::no_js_library_page_html,
|
||||
kainjow::mustache::object{
|
||||
{"root", rootLocation},
|
||||
{"books", booksData },
|
||||
{"searchQuery", searchQuery},
|
||||
{"languages", languages},
|
||||
{"categories", categories},
|
||||
{"noResults", filteredBooks.size() == 0},
|
||||
{"translations", translations}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace kiwix
|
||||
@@ -373,12 +373,27 @@ std::vector<std::string> Library::getBookPropValueSet(BookStrPropMemFn p) const
|
||||
|
||||
std::vector<std::string> Library::getBooksLanguages() const
|
||||
{
|
||||
return getBookPropValueSet(&Book::getLanguage);
|
||||
std::vector<std::string> langs;
|
||||
for ( const auto& langAndCount : getBooksLanguagesWithCounts() ) {
|
||||
langs.push_back(langAndCount.first);
|
||||
}
|
||||
return langs;
|
||||
}
|
||||
|
||||
Library::AttributeCounts Library::getBooksLanguagesWithCounts() const
|
||||
{
|
||||
return getBookAttributeCounts(&Book::getLanguage);
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
AttributeCounts langsWithCounts;
|
||||
|
||||
for (const auto& pair: mp_impl->m_books) {
|
||||
const auto& book = pair.second;
|
||||
if (book.getOrigId().empty()) {
|
||||
for ( const auto& lang : book.getLanguages() ) {
|
||||
++langsWithCounts[lang];
|
||||
}
|
||||
}
|
||||
}
|
||||
return langsWithCounts;
|
||||
}
|
||||
|
||||
std::vector<std::string> Library::getBooksCategories() const
|
||||
@@ -440,12 +455,14 @@ void Library::updateBookDB(const Book& book)
|
||||
{
|
||||
Xapian::Stem stemmer;
|
||||
Xapian::TermGenerator indexer;
|
||||
const std::string lang = book.getLanguage();
|
||||
try {
|
||||
stemmer = Xapian::Stem(iso639_3ToXapian(lang));
|
||||
indexer.set_stemmer(stemmer);
|
||||
indexer.set_stemming_strategy(Xapian::TermGenerator::STEM_SOME);
|
||||
} catch (...) {}
|
||||
const auto langs = book.getLanguages();
|
||||
if ( langs.size() == 1 ) {
|
||||
try {
|
||||
stemmer = Xapian::Stem(iso639_3ToXapian(langs[0]));
|
||||
indexer.set_stemmer(stemmer);
|
||||
indexer.set_stemming_strategy(Xapian::TermGenerator::STEM_SOME);
|
||||
} catch (...) {}
|
||||
}
|
||||
Xapian::Document doc;
|
||||
indexer.set_document(doc);
|
||||
|
||||
@@ -460,7 +477,9 @@ void Library::updateBookDB(const Book& book)
|
||||
// Index all fields for field-based search
|
||||
indexer.index_text(title, 1, "S");
|
||||
indexer.index_text(desc, 1, "XD");
|
||||
indexer.index_text(lang, 1, "L");
|
||||
for ( const auto& lang : langs ) {
|
||||
indexer.index_text(lang, 1, "L");
|
||||
}
|
||||
indexer.index_text(normalizeText(book.getCreator()), 1, "A");
|
||||
indexer.index_text(normalizeText(book.getPublisher()), 1, "XP");
|
||||
indexer.index_text(normalizeText(book.getName()), 1, "XN");
|
||||
@@ -859,6 +878,18 @@ Filter& Filter::name(std::string name)
|
||||
return *this;
|
||||
}
|
||||
|
||||
Filter& Filter::clearLang()
|
||||
{
|
||||
activeFilters &= ~LANG;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Filter& Filter::clearCategory()
|
||||
{
|
||||
activeFilters &= ~CATEGORY;
|
||||
return *this;
|
||||
}
|
||||
|
||||
#define ACTIVE(X) (activeFilters & (X))
|
||||
#define FILTER(TAG, TEST) if (ACTIVE(TAG) && !(TEST)) { return false; }
|
||||
bool Filter::hasQuery() const
|
||||
|
||||
121
src/library_dumper.cpp
Normal file
121
src/library_dumper.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "library_dumper.h"
|
||||
#include "tools/stringTools.h"
|
||||
#include "tools/otherTools.h"
|
||||
#include "tools.h"
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
/* Constructor */
|
||||
LibraryDumper::LibraryDumper(const Library* library, const NameMapper* nameMapper)
|
||||
: library(library),
|
||||
nameMapper(nameMapper)
|
||||
{
|
||||
}
|
||||
/* Destructor */
|
||||
LibraryDumper::~LibraryDumper()
|
||||
{
|
||||
}
|
||||
|
||||
void LibraryDumper::setOpenSearchInfo(int totalResults, int startIndex, int count)
|
||||
{
|
||||
m_totalResults = totalResults;
|
||||
m_startIndex = startIndex,
|
||||
m_count = count;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::map<std::string, std::string> iso639_3 = {
|
||||
{"atj", "atikamekw"},
|
||||
{"azb", "آذربایجان دیلی"},
|
||||
{"bcl", "central bikol"},
|
||||
{"bgs", "tagabawa"},
|
||||
{"bxr", "буряад хэлэн"},
|
||||
{"cbk", "chavacano"},
|
||||
{"cdo", "閩東語"},
|
||||
{"dag", "Dagbani"},
|
||||
{"diq", "dimli"},
|
||||
{"dty", "डोटेली"},
|
||||
{"eml", "emiliân-rumagnōl"},
|
||||
{"fbs", "српскохрватски"},
|
||||
{"guw", "Gungbe"},
|
||||
{"hbs", "srpskohrvatski"},
|
||||
{"ido", "ido"},
|
||||
{"kbp", "kabɩyɛ"},
|
||||
{"kld", "Gamilaraay"},
|
||||
{"lbe", "лакку маз"},
|
||||
{"lbj", "ལ་དྭགས་སྐད་"},
|
||||
{"map", "Austronesian"},
|
||||
{"mhr", "марий йылме"},
|
||||
{"mnw", "ဘာသာမန်"},
|
||||
{"myn", "mayan"},
|
||||
{"nah", "nahuatl"},
|
||||
{"nai", "north American Indian"},
|
||||
{"nds", "plattdütsch"},
|
||||
{"nrm", "bhasa narom"},
|
||||
{"olo", "livvi"},
|
||||
{"pih", "Pitcairn-Norfolk"},
|
||||
{"pnb", "Western Panjabi"},
|
||||
{"rmr", "Caló"},
|
||||
{"rmy", "romani shib"},
|
||||
{"roa", "romance languages"},
|
||||
{"twi", "twi"},
|
||||
};
|
||||
|
||||
std::once_flag fillLanguagesFlag;
|
||||
|
||||
void fillLanguagesMap()
|
||||
{
|
||||
for (auto icuLangPtr = icu::Locale::getISOLanguages(); *icuLangPtr != NULL; ++icuLangPtr) {
|
||||
const ICULanguageInfo lang(*icuLangPtr);
|
||||
iso639_3.insert({lang.iso3Code(), lang.selfName()});
|
||||
}
|
||||
}
|
||||
|
||||
std::string getLanguageSelfName(const std::string& lang) {
|
||||
const auto itr = iso639_3.find(lang);
|
||||
if (itr != iso639_3.end()) {
|
||||
return itr->second;
|
||||
}
|
||||
return lang;
|
||||
};
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
kainjow::mustache::list LibraryDumper::getCategoryData() const
|
||||
{
|
||||
const auto now = gen_date_str();
|
||||
kainjow::mustache::list categoryData;
|
||||
for ( const auto& category : library->getBooksCategories() ) {
|
||||
const auto urlencodedCategoryName = urlEncode(category);
|
||||
categoryData.push_back(kainjow::mustache::object{
|
||||
{"name", category},
|
||||
{"urlencoded_name", urlencodedCategoryName},
|
||||
{"updated", now},
|
||||
{"id", gen_uuid(libraryId + "/categories/" + urlencodedCategoryName)}
|
||||
});
|
||||
}
|
||||
return categoryData;
|
||||
}
|
||||
|
||||
kainjow::mustache::list LibraryDumper::getLanguageData() const
|
||||
{
|
||||
const auto now = gen_date_str();
|
||||
kainjow::mustache::list languageData;
|
||||
std::call_once(fillLanguagesFlag, fillLanguagesMap);
|
||||
for ( const auto& langAndBookCount : library->getBooksLanguagesWithCounts() ) {
|
||||
const std::string languageCode = langAndBookCount.first;
|
||||
const int bookCount = langAndBookCount.second;
|
||||
const auto languageSelfName = getLanguageSelfName(languageCode);
|
||||
languageData.push_back(kainjow::mustache::object{
|
||||
{"lang_code", languageCode},
|
||||
{"lang_self_name", languageSelfName},
|
||||
{"book_count", to_string(bookCount)},
|
||||
{"updated", now},
|
||||
{"id", gen_uuid(libraryId + "/languages/" + languageCode)}
|
||||
});
|
||||
}
|
||||
return languageData;
|
||||
}
|
||||
|
||||
} // namespace kiwix
|
||||
@@ -54,7 +54,7 @@ void LibXMLDumper::handleBook(Book book, pugi::xml_node root_node) {
|
||||
if (book.getOrigId().empty()) {
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "title", book.getTitle());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "description", book.getDescription());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "language", book.getLanguage());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "language", book.getCommaSeparatedLanguages());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "creator", book.getCreator());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "publisher", book.getPublisher());
|
||||
ADD_ATTR_NOT_EMPTY(entry_node, "name", book.getName());
|
||||
@@ -97,7 +97,7 @@ void LibXMLDumper::handleBookmark(Bookmark bookmark, pugi::xml_node root_node) {
|
||||
auto book = library->getBookByIdThreadSafe(bookmark.getBookId());
|
||||
ADD_TEXT_ENTRY(book_node, "id", book.getId());
|
||||
ADD_TEXT_ENTRY(book_node, "title", book.getTitle());
|
||||
ADD_TEXT_ENTRY(book_node, "language", book.getLanguage());
|
||||
ADD_TEXT_ENTRY(book_node, "language", book.getCommaSeparatedLanguages());
|
||||
ADD_TEXT_ENTRY(book_node, "date", book.getDate());
|
||||
} catch (...) {
|
||||
ADD_TEXT_ENTRY(book_node, "id", bookmark.getBookId());
|
||||
|
||||
@@ -238,7 +238,7 @@ std::string Manager::addBookFromPathAndGetId(const std::string& pathToOpen,
|
||||
}
|
||||
|
||||
if (!checkMetaData
|
||||
|| (checkMetaData && !book.getTitle().empty() && !book.getLanguage().empty()
|
||||
|| (!book.getTitle().empty() && !book.getLanguages().empty()
|
||||
&& !book.getDate().empty())) {
|
||||
book.setUrl(url);
|
||||
manipulator->addBookToLibrary(book);
|
||||
|
||||
@@ -5,6 +5,8 @@ kiwix_sources = [
|
||||
'manager.cpp',
|
||||
'libxml_dumper.cpp',
|
||||
'opds_dumper.cpp',
|
||||
'html_dumper.cpp',
|
||||
'library_dumper.cpp',
|
||||
'downloader.cpp',
|
||||
'server.cpp',
|
||||
'search_renderer.cpp',
|
||||
@@ -24,7 +26,7 @@ kiwix_sources = [
|
||||
'server/request_context.cpp',
|
||||
'server/response.cpp',
|
||||
'server/internalServer.cpp',
|
||||
'server/internalServer_catalog_v2.cpp',
|
||||
'server/internalServer_catalog.cpp',
|
||||
'server/i18n.cpp',
|
||||
'opds_catalog.cpp',
|
||||
'version.cpp'
|
||||
|
||||
@@ -30,9 +30,8 @@ namespace kiwix
|
||||
{
|
||||
|
||||
/* Constructor */
|
||||
OPDSDumper::OPDSDumper(Library* library, NameMapper* nameMapper)
|
||||
: library(library),
|
||||
nameMapper(nameMapper)
|
||||
OPDSDumper::OPDSDumper(const Library* library, const NameMapper* nameMapper)
|
||||
: LibraryDumper(library, nameMapper)
|
||||
{
|
||||
}
|
||||
/* Destructor */
|
||||
@@ -40,13 +39,6 @@ OPDSDumper::~OPDSDumper()
|
||||
{
|
||||
}
|
||||
|
||||
void OPDSDumper::setOpenSearchInfo(int totalResults, int startIndex, int count)
|
||||
{
|
||||
m_totalResults = totalResults;
|
||||
m_startIndex = startIndex,
|
||||
m_count = count;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
@@ -81,8 +73,8 @@ std::string fullEntryXML(const Book& book, const std::string& rootLocation, cons
|
||||
{"name", book.getName()},
|
||||
{"title", book.getTitle()},
|
||||
{"description", book.getDescription()},
|
||||
{"language", book.getLanguage()},
|
||||
{"content_id", urlEncode(contentId, true)},
|
||||
{"language", book.getCommaSeparatedLanguages()},
|
||||
{"content_id", urlEncode(contentId)},
|
||||
{"updated", bookDate}, // XXX: this should be the entry update datetime
|
||||
{"book_date", bookDate},
|
||||
{"category", book.getCategory()},
|
||||
@@ -133,59 +125,6 @@ BooksData getBooksData(const Library* library, const NameMapper* nameMapper, con
|
||||
return booksData;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> iso639_3 = {
|
||||
{"atj", "atikamekw"},
|
||||
{"azb", "آذربایجان دیلی"},
|
||||
{"bcl", "central bikol"},
|
||||
{"bgs", "tagabawa"},
|
||||
{"bxr", "буряад хэлэн"},
|
||||
{"cbk", "chavacano"},
|
||||
{"cdo", "閩東語"},
|
||||
{"dag", "Dagbani"},
|
||||
{"diq", "dimli"},
|
||||
{"dty", "डोटेली"},
|
||||
{"eml", "emiliân-rumagnōl"},
|
||||
{"fbs", "српскохрватски"},
|
||||
{"ido", "ido"},
|
||||
{"kbp", "kabɩyɛ"},
|
||||
{"kld", "Gamilaraay"},
|
||||
{"lbe", "лакку маз"},
|
||||
{"lbj", "ལ་དྭགས་སྐད་"},
|
||||
{"map", "Austronesian"},
|
||||
{"mhr", "марий йылме"},
|
||||
{"mnw", "ဘာသာမန်"},
|
||||
{"myn", "mayan"},
|
||||
{"nah", "nahuatl"},
|
||||
{"nai", "north American Indian"},
|
||||
{"nds", "plattdütsch"},
|
||||
{"nrm", "bhasa narom"},
|
||||
{"olo", "livvi"},
|
||||
{"pih", "Pitcairn-Norfolk"},
|
||||
{"pnb", "Western Panjabi"},
|
||||
{"rmr", "Caló"},
|
||||
{"rmy", "romani shib"},
|
||||
{"roa", "romance languages"},
|
||||
{"twi", "twi"}
|
||||
};
|
||||
|
||||
std::once_flag fillLanguagesFlag;
|
||||
|
||||
void fillLanguagesMap()
|
||||
{
|
||||
for (auto icuLangPtr = icu::Locale::getISOLanguages(); *icuLangPtr != NULL; ++icuLangPtr) {
|
||||
const ICULanguageInfo lang(*icuLangPtr);
|
||||
iso639_3.insert({lang.iso3Code(), lang.selfName()});
|
||||
}
|
||||
}
|
||||
|
||||
std::string getLanguageSelfName(const std::string& lang) {
|
||||
const auto itr = iso639_3.find(lang);
|
||||
if (itr != iso639_3.end()) {
|
||||
return itr->second;
|
||||
}
|
||||
return lang;
|
||||
};
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const
|
||||
@@ -211,17 +150,17 @@ string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const
|
||||
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, partial);
|
||||
|
||||
const char* const endpoint = partial ? "/partial_entries" : "/entries";
|
||||
const std::string url = endpoint + (query.empty() ? "" : "?" + query);
|
||||
const kainjow::mustache::object template_data{
|
||||
{"date", gen_date_str()},
|
||||
{"endpoint_root", endpointRoot},
|
||||
{"feed_id", gen_uuid(libraryId + endpoint + "?" + query)},
|
||||
{"filter", onlyAsNonEmptyMustacheValue(query)},
|
||||
{"query", query.empty() ? "" : "?" + urlEncode(query)},
|
||||
{"self_url", url},
|
||||
{"totalResults", to_string(m_totalResults)},
|
||||
{"startIndex", to_string(m_startIndex)},
|
||||
{"itemsPerPage", to_string(m_count)},
|
||||
{"books", booksData },
|
||||
{"dump_partial_entries", MustacheData(partial)}
|
||||
{"books", booksData }
|
||||
};
|
||||
|
||||
return render_template(RESOURCE::templates::catalog_v2_entries_xml, template_data);
|
||||
@@ -239,17 +178,7 @@ std::string OPDSDumper::dumpOPDSCompleteEntry(const std::string& bookId) const
|
||||
std::string OPDSDumper::categoriesOPDSFeed() const
|
||||
{
|
||||
const auto now = gen_date_str();
|
||||
kainjow::mustache::list categoryData;
|
||||
for ( const auto& category : library->getBooksCategories() ) {
|
||||
const auto urlencodedCategoryName = urlEncode(category);
|
||||
categoryData.push_back(kainjow::mustache::object{
|
||||
{"name", category},
|
||||
{"urlencoded_name", urlencodedCategoryName},
|
||||
{"updated", now},
|
||||
{"id", gen_uuid(libraryId + "/categories/" + urlencodedCategoryName)}
|
||||
});
|
||||
}
|
||||
|
||||
kainjow::mustache::list categoryData = getCategoryData();
|
||||
return render_template(
|
||||
RESOURCE::templates::catalog_v2_categories_xml,
|
||||
kainjow::mustache::object{
|
||||
@@ -264,21 +193,7 @@ std::string OPDSDumper::categoriesOPDSFeed() const
|
||||
std::string OPDSDumper::languagesOPDSFeed() const
|
||||
{
|
||||
const auto now = gen_date_str();
|
||||
kainjow::mustache::list languageData;
|
||||
std::call_once(fillLanguagesFlag, fillLanguagesMap);
|
||||
for ( const auto& langAndBookCount : library->getBooksLanguagesWithCounts() ) {
|
||||
const std::string languageCode = langAndBookCount.first;
|
||||
const int bookCount = langAndBookCount.second;
|
||||
const auto languageSelfName = getLanguageSelfName(languageCode);
|
||||
languageData.push_back(kainjow::mustache::object{
|
||||
{"lang_code", languageCode},
|
||||
{"lang_self_name", languageSelfName},
|
||||
{"book_count", to_string(bookCount)},
|
||||
{"updated", now},
|
||||
{"id", gen_uuid(libraryId + "/languages/" + languageCode)}
|
||||
});
|
||||
}
|
||||
|
||||
kainjow::mustache::list languageData = getLanguageData();
|
||||
return render_template(
|
||||
RESOURCE::templates::catalog_v2_languages_xml,
|
||||
kainjow::mustache::object{
|
||||
|
||||
@@ -94,7 +94,7 @@ kainjow::mustache::data buildQueryData
|
||||
kainjow::mustache::data query;
|
||||
query.set("pattern", kiwix::encodeDiples(pattern));
|
||||
std::ostringstream ss;
|
||||
ss << searchProtocolPrefix << "?pattern=" << urlEncode(pattern, true);
|
||||
ss << searchProtocolPrefix << "?pattern=" << urlEncode(pattern);
|
||||
ss << "&" << bookQuery;
|
||||
query.set("unpaginatedQuery", ss.str());
|
||||
auto lang = extractValueFromQuery(bookQuery, "books.filter.lang");
|
||||
@@ -171,9 +171,10 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
|
||||
kainjow::mustache::data items{kainjow::mustache::data::type::list};
|
||||
for (auto it = m_srs.begin(); it != m_srs.end(); it++) {
|
||||
kainjow::mustache::data result;
|
||||
std::string zim_id(it.getZimId());
|
||||
const std::string zim_id(it.getZimId());
|
||||
const auto path = mp_nameMapper->getNameForId(zim_id) + "/" + it.getPath();
|
||||
result.set("title", it.getTitle());
|
||||
result.set("absolutePath", absPathPrefix + urlEncode(mp_nameMapper->getNameForId(zim_id), true) + "/" + urlEncode(it.getPath()));
|
||||
result.set("absolutePath", absPathPrefix + urlEncode(path));
|
||||
result.set("snippet", it.getSnippet());
|
||||
if (mp_library) {
|
||||
result.set("bookTitle", mp_library->getBookById(zim_id).getTitle());
|
||||
|
||||
@@ -70,6 +70,14 @@ public: // functions
|
||||
return s;
|
||||
}
|
||||
|
||||
size_t getStringCount(const std::string& lang) const {
|
||||
try {
|
||||
return lang2TableMap.at(lang)->entryCount;
|
||||
} catch(const std::out_of_range&) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private: // functions
|
||||
const I18nStringTable* getStringsFor(const std::string& lang) const {
|
||||
try {
|
||||
@@ -84,13 +92,17 @@ private: // data
|
||||
const I18nStringTable* enStrings;
|
||||
};
|
||||
|
||||
const I18nStringDB& getStringDb()
|
||||
{
|
||||
static const I18nStringDB stringDb;
|
||||
return stringDb;
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::string getTranslatedString(const std::string& lang, const std::string& key)
|
||||
{
|
||||
static const I18nStringDB stringDb;
|
||||
|
||||
return stringDb.get(lang, key);
|
||||
return getStringDb().get(lang, key);
|
||||
}
|
||||
|
||||
namespace i18n
|
||||
@@ -111,4 +123,70 @@ std::string ParameterizedMessage::getText(const std::string& lang) const
|
||||
return i18n::expandParameterizedString(lang, msgId, params);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
LangPreference parseSingleLanguagePreference(const std::string& s)
|
||||
{
|
||||
const size_t langStart = s.find_first_not_of(" \t\n");
|
||||
if ( langStart == std::string::npos ) {
|
||||
return {"", 0};
|
||||
}
|
||||
|
||||
const size_t langEnd = s.find(';', langStart);
|
||||
if ( langEnd == std::string::npos ) {
|
||||
return {s.substr(langStart), 1};
|
||||
}
|
||||
|
||||
const std::string lang = s.substr(langStart, langEnd - langStart);
|
||||
// We don't care about langEnd == langStart which will result in an empty
|
||||
// language name - it will be dismissed by parseUserLanguagePreferences()
|
||||
|
||||
float q = 1.0;
|
||||
int nCharsScanned;
|
||||
if ( 1 == sscanf(s.c_str() + langEnd + 1, "q=%f%n", &q, &nCharsScanned)
|
||||
&& langEnd + 1 + nCharsScanned == s.size() ) {
|
||||
return {lang, q};
|
||||
}
|
||||
|
||||
return {"", 0};
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
UserLangPreferences parseUserLanguagePreferences(const std::string& s)
|
||||
{
|
||||
UserLangPreferences result;
|
||||
std::istringstream iss(s);
|
||||
std::string singleLangPrefStr;
|
||||
while ( std::getline(iss, singleLangPrefStr, ',') )
|
||||
{
|
||||
const auto langPref = parseSingleLanguagePreference(singleLangPrefStr);
|
||||
if ( !langPref.lang.empty() && langPref.preference > 0 ) {
|
||||
result.push_back(langPref);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string selectMostSuitableLanguage(const UserLangPreferences& prefs)
|
||||
{
|
||||
if ( prefs.empty() ) {
|
||||
return "en";
|
||||
}
|
||||
|
||||
std::string bestLangSoFar("en");
|
||||
float bestScoreSoFar = 0;
|
||||
const auto& stringDb = getStringDb();
|
||||
for ( const auto& entry : prefs ) {
|
||||
const float score = entry.preference * stringDb.getStringCount(entry.lang);
|
||||
if ( score > bestScoreSoFar ) {
|
||||
bestScoreSoFar = score;
|
||||
bestLangSoFar = entry.lang;
|
||||
}
|
||||
}
|
||||
return bestLangSoFar;
|
||||
}
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
@@ -69,6 +69,28 @@ private:
|
||||
const std::string m_lang;
|
||||
};
|
||||
|
||||
class GetTranslatedStringWithMsgId
|
||||
{
|
||||
typedef kainjow::mustache::basic_data<std::string> MustacheString;
|
||||
typedef std::pair<std::string, MustacheString> MsgIdAndTranslation;
|
||||
|
||||
public:
|
||||
explicit GetTranslatedStringWithMsgId(const std::string& lang) : m_lang(lang) {}
|
||||
|
||||
MsgIdAndTranslation operator()(const std::string& key) const
|
||||
{
|
||||
return {key, getTranslatedString(m_lang, key)};
|
||||
}
|
||||
|
||||
MsgIdAndTranslation operator()(const std::string& key, const Parameters& params) const
|
||||
{
|
||||
return {key, expandParameterizedString(m_lang, key, params)};
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string m_lang;
|
||||
};
|
||||
|
||||
} // namespace i18n
|
||||
|
||||
struct ParameterizedMessage
|
||||
@@ -89,6 +111,18 @@ private: // data
|
||||
const Parameters params;
|
||||
};
|
||||
|
||||
struct LangPreference
|
||||
{
|
||||
const std::string lang;
|
||||
const float preference;
|
||||
};
|
||||
|
||||
typedef std::vector<LangPreference> UserLangPreferences;
|
||||
|
||||
UserLangPreferences parseUserLanguagePreferences(const std::string& s);
|
||||
|
||||
std::string selectMostSuitableLanguage(const UserLangPreferences& prefs);
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
#endif // KIWIX_SERVER_I18N
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
#include "internalServer.h"
|
||||
|
||||
#ifdef __FreeBSD__
|
||||
#ifndef _WIN32
|
||||
#include <netinet/in.h>
|
||||
#endif
|
||||
|
||||
@@ -53,6 +53,7 @@ extern "C" {
|
||||
#include "name_mapper.h"
|
||||
#include "search_renderer.h"
|
||||
#include "opds_dumper.h"
|
||||
#include "html_dumper.h"
|
||||
#include "i18n.h"
|
||||
|
||||
#include <zim/uuid.h>
|
||||
@@ -94,6 +95,22 @@ inline std::string normalizeRootUrl(std::string rootUrl)
|
||||
return rootUrl.empty() ? rootUrl : "/" + rootUrl;
|
||||
}
|
||||
|
||||
std::string
|
||||
fullURL2LocalURL(const std::string& fullUrl, const std::string& rootLocation)
|
||||
{
|
||||
if ( kiwix::startsWith(fullUrl, rootLocation) ) {
|
||||
return fullUrl.substr(rootLocation.size());
|
||||
} else {
|
||||
return "INVALID URL";
|
||||
}
|
||||
}
|
||||
|
||||
std::string getSearchComponent(const RequestContext& request)
|
||||
{
|
||||
const std::string query = request.get_query();
|
||||
return query.empty() ? query : "?" + query;
|
||||
}
|
||||
|
||||
Filter get_search_filter(const RequestContext& request, const std::string& prefix="")
|
||||
{
|
||||
auto filter = kiwix::Filter().valid(true).local(true);
|
||||
@@ -207,7 +224,8 @@ typedef std::set<std::string> Languages;
|
||||
Languages getLanguages(const Library& lib, const Library::BookIdSet& bookIds) {
|
||||
Languages langs;
|
||||
for ( const auto& b : bookIds ) {
|
||||
langs.insert(lib.getBookById(b).getLanguage());
|
||||
const auto bookLangs = lib.getBookById(b).getLanguages();
|
||||
langs.insert(bookLangs.begin(), bookLangs.end());
|
||||
}
|
||||
return langs;
|
||||
}
|
||||
@@ -404,6 +422,7 @@ InternalServer::InternalServer(Library* library,
|
||||
m_addr(addr),
|
||||
m_port(port),
|
||||
m_root(normalizeRootUrl(root)),
|
||||
m_rootPrefixOfDecodedURL(m_root),
|
||||
m_nbThreads(nbThreads),
|
||||
m_multizimSearchLimit(multizimSearchLimit),
|
||||
m_verbose(verbose),
|
||||
@@ -418,7 +437,9 @@ InternalServer::InternalServer(Library* library,
|
||||
searchCache(getEnvVar<int>("KIWIX_SEARCH_CACHE_SIZE", DEFAULT_CACHE_SIZE)),
|
||||
suggestionSearcherCache(getEnvVar<int>("KIWIX_SUGGESTION_SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U))),
|
||||
m_customizedResources(new CustomizedResources)
|
||||
{}
|
||||
{
|
||||
m_root = urlEncode(m_root);
|
||||
}
|
||||
|
||||
InternalServer::~InternalServer() = default;
|
||||
|
||||
@@ -494,7 +515,7 @@ static MHD_Result staticHandlerCallback(void* cls,
|
||||
}
|
||||
|
||||
MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||
const char* url,
|
||||
const char* fullUrl,
|
||||
const char* method,
|
||||
const char* version,
|
||||
const char* upload_data,
|
||||
@@ -505,8 +526,10 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||
if (m_verbose.load() ) {
|
||||
printf("======================\n");
|
||||
printf("Requesting : \n");
|
||||
printf("full_url : %s\n", url);
|
||||
printf("full_url : %s\n", fullUrl);
|
||||
}
|
||||
|
||||
const auto url = fullURL2LocalURL(fullUrl, m_rootPrefixOfDecodedURL);
|
||||
RequestContext request(connection, m_root, url, method, version);
|
||||
|
||||
if (m_verbose.load() ) {
|
||||
@@ -527,7 +550,7 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||
printf("========== INTERNAL ERROR !! ============\n");
|
||||
if (!m_verbose.load()) {
|
||||
printf("Requesting : \n");
|
||||
printf("full_url : %s\n", url);
|
||||
printf("full_url : %s\n", fullUrl);
|
||||
request.print_debug_info();
|
||||
}
|
||||
}
|
||||
@@ -569,6 +592,13 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
if ( request.get_url() == "" ) {
|
||||
// Redirect /ROOT_LOCATION to /ROOT_LOCATION/ (note the added slash)
|
||||
// so that relative URLs are resolved correctly
|
||||
const std::string query = getSearchComponent(request);
|
||||
return Response::build_redirect(*this, m_root + "/" + query);
|
||||
}
|
||||
|
||||
const ETag etag = get_matching_if_none_match_etag(request, getLibraryId());
|
||||
if ( etag )
|
||||
return Response::build_304(*this, etag);
|
||||
@@ -598,6 +628,9 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||
if (isEndpointUrl(url, "search"))
|
||||
return handle_search(request);
|
||||
|
||||
if (isEndpointUrl(url, "nojs"))
|
||||
return handle_no_js(request);
|
||||
|
||||
if (isEndpointUrl(url, "suggest"))
|
||||
return handle_suggest(request);
|
||||
|
||||
@@ -607,11 +640,9 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||
if (isEndpointUrl(url, "catch"))
|
||||
return handle_catch(request);
|
||||
|
||||
std::string contentUrl = m_root + "/content" + url;
|
||||
const std::string query = request.get_query();
|
||||
if ( ! query.empty() )
|
||||
contentUrl += "?" + query;
|
||||
return Response::build_redirect(*this, contentUrl);
|
||||
const std::string contentUrl = m_root + "/content" + urlEncode(url);
|
||||
const std::string query = getSearchComponent(request);
|
||||
return Response::build_redirect(*this, contentUrl + query);
|
||||
} catch (std::exception& e) {
|
||||
fprintf(stderr, "===== Unhandled error : %s\n", e.what());
|
||||
return HTTP500Response(*this, request)
|
||||
@@ -728,6 +759,73 @@ std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestCo
|
||||
return ContentResponse::build(*this, RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8");
|
||||
}
|
||||
|
||||
std::string InternalServer::getNoJSDownloadPageHTML(const std::string& bookId, const std::string& userLang) const
|
||||
{
|
||||
const auto book = mp_library->getBookById(bookId);
|
||||
auto bookUrl = kiwix::stripSuffix(book.getUrl(), ".meta4");
|
||||
auto getTranslation = i18n::GetTranslatedStringWithMsgId(userLang);
|
||||
const auto translations = kainjow::mustache::object{
|
||||
getTranslation("download-links-heading", {{"BOOK_TITLE", book.getTitle()}}),
|
||||
getTranslation("download-links-title"),
|
||||
getTranslation("direct-download-link-text"),
|
||||
getTranslation("hash-download-link-text"),
|
||||
getTranslation("magnet-link-text"),
|
||||
getTranslation("torrent-download-link-text")
|
||||
};
|
||||
|
||||
return render_template(
|
||||
RESOURCE::templates::no_js_download_html,
|
||||
kainjow::mustache::object{
|
||||
{"url", bookUrl},
|
||||
{"translations", translations}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_no_js(const RequestContext& request)
|
||||
{
|
||||
const auto url = request.get_url();
|
||||
const auto urlParts = kiwix::split(url, "/", true, false);
|
||||
HTMLDumper htmlDumper(mp_library, mp_nameMapper);
|
||||
htmlDumper.setRootLocation(m_root);
|
||||
htmlDumper.setLibraryId(getLibraryId());
|
||||
auto userLang = request.get_user_language();
|
||||
htmlDumper.setUserLanguage(userLang);
|
||||
std::string content;
|
||||
|
||||
if (urlParts.size() == 1) {
|
||||
auto filter = get_search_filter(request);
|
||||
try {
|
||||
if (request.get_argument("category") == "") {
|
||||
filter.clearCategory();
|
||||
}
|
||||
} catch (...) {}
|
||||
try {
|
||||
if (request.get_argument("lang") == "") {
|
||||
filter.clearLang();
|
||||
}
|
||||
} catch (...) {}
|
||||
content = htmlDumper.dumpPlainHTML(filter);
|
||||
} else if ((urlParts.size() == 3) && (urlParts[1] == "download")) {
|
||||
try {
|
||||
const auto bookId = mp_nameMapper->getIdForName(urlParts[2]);
|
||||
content = getNoJSDownloadPageHTML(bookId, userLang);
|
||||
} catch (const std::out_of_range&) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
} else {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
content,
|
||||
"text/html; charset=utf-8"
|
||||
);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
@@ -948,56 +1046,6 @@ std::unique_ptr<Response> InternalServer::handle_catch(const RequestContext& req
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_catalog");
|
||||
}
|
||||
|
||||
std::string host;
|
||||
std::string url;
|
||||
try {
|
||||
host = request.get_header("Host");
|
||||
url = request.get_url_part(1);
|
||||
} catch (const std::out_of_range&) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
if (url == "v2") {
|
||||
return handle_catalog_v2(request);
|
||||
}
|
||||
|
||||
if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
if (url == "searchdescription.xml") {
|
||||
auto response = ContentResponse::build(*this, RESOURCE::opensearchdescription_xml, get_default_data(), "application/opensearchdescription+xml");
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
zim::Uuid uuid;
|
||||
kiwix::OPDSDumper opdsDumper(mp_library, mp_nameMapper);
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
std::vector<std::string> bookIdsToDump;
|
||||
if (url == "root.xml") {
|
||||
uuid = zim::Uuid::generate(host);
|
||||
bookIdsToDump = mp_library->filter(kiwix::Filter().valid(true).local(true).remote(true));
|
||||
} else if (url == "search") {
|
||||
bookIdsToDump = search_catalog(request, opdsDumper);
|
||||
uuid = zim::Uuid::generate();
|
||||
}
|
||||
|
||||
auto response = ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.dumpOPDSFeed(bookIdsToDump, request.get_query()),
|
||||
"application/atom+xml; profile=opds-catalog; kind=acquisition; charset=utf-8");
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
InternalServer::search_catalog(const RequestContext& request,
|
||||
kiwix::OPDSDumper& opdsDumper)
|
||||
@@ -1005,9 +1053,9 @@ InternalServer::search_catalog(const RequestContext& request,
|
||||
const auto filter = get_search_filter(request);
|
||||
std::vector<std::string> bookIdsToDump = mp_library->filter(filter);
|
||||
const auto totalResults = bookIdsToDump.size();
|
||||
const size_t count = request.get_optional_param("count", 10UL);
|
||||
const long count = request.get_optional_param("count", 10L);
|
||||
const size_t startIndex = request.get_optional_param("start", 0UL);
|
||||
const size_t intendedCount = count > 0 ? count : bookIdsToDump.size();
|
||||
const size_t intendedCount = count >= 0 ? count : bookIdsToDump.size();
|
||||
bookIdsToDump = subrange(bookIdsToDump, startIndex, intendedCount);
|
||||
opdsDumper.setOpenSearchInfo(totalResults, startIndex, bookIdsToDump.size());
|
||||
return bookIdsToDump;
|
||||
@@ -1025,14 +1073,37 @@ ParameterizedMessage suggestSearchMsg(const std::string& searchURL, const std::s
|
||||
});
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// The content security policy below is set on responses to the /content
|
||||
// endpoint in order to prevent the ZIM content from interfering with the
|
||||
// viewer (e.g. breaking out of the viewer iframe by performing top-level
|
||||
// navigation).
|
||||
const std::string CONTENT_CSP_HEADER =
|
||||
"default-src 'self' "
|
||||
"data: "
|
||||
"blob: "
|
||||
"about: "
|
||||
"'unsafe-inline' "
|
||||
"'unsafe-eval'; "
|
||||
|
||||
"sandbox allow-scripts "
|
||||
"allow-same-origin "
|
||||
"allow-modals "
|
||||
"allow-popups "
|
||||
"allow-forms "
|
||||
"allow-downloads;";
|
||||
|
||||
// End of content security policy
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::unique_ptr<Response>
|
||||
InternalServer::build_redirect(const std::string& bookName, const zim::Item& item) const
|
||||
{
|
||||
const auto path = kiwix::urlEncode(item.getPath());
|
||||
const auto redirectUrl = m_root + "/content/" + bookName + "/" + path;
|
||||
return Response::build_redirect(*this, redirectUrl);
|
||||
const auto contentPath = "/content/" + bookName + "/" + item.getPath();
|
||||
const auto url = m_root + kiwix::urlEncode(contentPath);
|
||||
return Response::build_redirect(*this, url);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& request)
|
||||
@@ -1055,7 +1126,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
} catch (const std::out_of_range& e) {}
|
||||
|
||||
if (archive == nullptr) {
|
||||
const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern, true);
|
||||
const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern);
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
|
||||
@@ -1086,6 +1157,13 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
auto response = ItemResponse::build(*this, request, entry.getItem());
|
||||
response->set_etag_body(archiveUuid);
|
||||
|
||||
if ( !startsWith(entry.getItem().getMimetype(), "application/pdf") ) {
|
||||
// NOTE: Content security policy is not applied to PDF content so that
|
||||
// NOTE: it can be displayed in the viewer in Chromium-based browsers.
|
||||
response->add_header("Content-Security-Policy", CONTENT_CSP_HEADER);
|
||||
response->add_header("Referrer-Policy", "no-referrer");
|
||||
}
|
||||
|
||||
if (m_verbose.load()) {
|
||||
printf("Found %s\n", entry.getPath().c_str());
|
||||
printf("mimeType: %s\n", entry.getItem(true).getMimetype().c_str());
|
||||
@@ -1096,7 +1174,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
if (m_verbose.load())
|
||||
printf("Failed to find %s\n", urlStr.c_str());
|
||||
|
||||
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern, true);
|
||||
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern);
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
|
||||
|
||||
@@ -131,6 +131,7 @@ class InternalServer {
|
||||
std::unique_ptr<Response> handle_catalog_v2_entries(const RequestContext& request, bool partial);
|
||||
std::unique_ptr<Response> handle_catalog_v2_complete_entry(const RequestContext& request, const std::string& entryId);
|
||||
std::unique_ptr<Response> handle_catalog_v2_categories(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_no_js(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catalog_v2_languages(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catalog_v2_illustration(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_search(const RequestContext& request);
|
||||
@@ -155,6 +156,8 @@ class InternalServer {
|
||||
|
||||
std::string getLibraryId() const;
|
||||
|
||||
std::string getNoJSDownloadPageHTML(const std::string& bookId, const std::string& userLang) const;
|
||||
|
||||
private: // types
|
||||
class LockableSuggestionSearcher;
|
||||
typedef ConcurrentCache<SearchInfo, std::shared_ptr<zim::Search>> SearchCache;
|
||||
@@ -163,7 +166,8 @@ class InternalServer {
|
||||
private: // data
|
||||
std::string m_addr;
|
||||
int m_port;
|
||||
std::string m_root;
|
||||
std::string m_root; // URI-encoded
|
||||
std::string m_rootPrefixOfDecodedURL; // URI-decoded
|
||||
int m_nbThreads;
|
||||
unsigned int m_multizimSearchLimit;
|
||||
std::atomic_bool m_verbose;
|
||||
|
||||
@@ -33,6 +33,74 @@
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
enum OPDSResponseKind
|
||||
{
|
||||
OPDS_ENTRY,
|
||||
OPDS_NAVIGATION_FEED,
|
||||
OPDS_ACQUISITION_FEED
|
||||
};
|
||||
|
||||
const std::string opdsMimeType[] = {
|
||||
"application/atom+xml;type=entry;profile=opds-catalog;charset=utf-8",
|
||||
"application/atom+xml;profile=opds-catalog;kind=navigation;charset=utf-8",
|
||||
"application/atom+xml;profile=opds-catalog;kind=acquisition;charset=utf-8"
|
||||
};
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_catalog");
|
||||
}
|
||||
|
||||
std::string host;
|
||||
std::string url;
|
||||
try {
|
||||
host = request.get_header("Host");
|
||||
url = request.get_url_part(1);
|
||||
} catch (const std::out_of_range&) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
if (url == "v2") {
|
||||
return handle_catalog_v2(request);
|
||||
}
|
||||
|
||||
if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
if (url == "searchdescription.xml") {
|
||||
auto response = ContentResponse::build(*this, RESOURCE::opensearchdescription_xml, get_default_data(), "application/opensearchdescription+xml");
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
zim::Uuid uuid;
|
||||
kiwix::OPDSDumper opdsDumper(mp_library, mp_nameMapper);
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
std::vector<std::string> bookIdsToDump;
|
||||
if (url == "root.xml") {
|
||||
uuid = zim::Uuid::generate(host);
|
||||
bookIdsToDump = mp_library->filter(kiwix::Filter().valid(true).local(true).remote(true));
|
||||
} else if (url == "search") {
|
||||
bookIdsToDump = search_catalog(request, opdsDumper);
|
||||
uuid = zim::Uuid::generate();
|
||||
}
|
||||
|
||||
auto response = ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.dumpOPDSFeed(bookIdsToDump, request.get_query()),
|
||||
opdsMimeType[OPDS_ACQUISITION_FEED]);
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
@@ -90,7 +158,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestCo
|
||||
{"category_list_feed_id", gen_uuid(libraryId + "/categories")},
|
||||
{"language_list_feed_id", gen_uuid(libraryId + "/languages")}
|
||||
},
|
||||
"application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||
opdsMimeType[OPDS_NAVIGATION_FEED]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,7 +172,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const Reques
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsFeed,
|
||||
"application/atom+xml;profile=opds-catalog;kind=acquisition"
|
||||
opdsMimeType[OPDS_ACQUISITION_FEED]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,7 +192,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsFeed,
|
||||
"application/atom+xml;type=entry;profile=opds-catalog"
|
||||
opdsMimeType[OPDS_ENTRY]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,7 +204,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const Req
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.categoriesOPDSFeed(),
|
||||
"application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||
opdsMimeType[OPDS_NAVIGATION_FEED]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,7 +216,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const Requ
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.languagesOPDSFeed(),
|
||||
"application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||
opdsMimeType[OPDS_NAVIGATION_FEED]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,10 @@
|
||||
#include <sstream>
|
||||
#include <cstdio>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
#include "i18n.h"
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
@@ -47,31 +49,15 @@ RequestMethod str2RequestMethod(const std::string& method) {
|
||||
else return RequestMethod::OTHER;
|
||||
}
|
||||
|
||||
std::string
|
||||
fullURL2LocalURL(const std::string& full_url, const std::string& rootLocation)
|
||||
{
|
||||
if (rootLocation.empty()) {
|
||||
// nothing special to handle.
|
||||
return full_url;
|
||||
} else if (full_url == rootLocation) {
|
||||
return "/";
|
||||
} else if (full_url.size() > rootLocation.size() &&
|
||||
full_url.substr(0, rootLocation.size()+1) == rootLocation + "/") {
|
||||
return full_url.substr(rootLocation.size());
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
RequestContext::RequestContext(struct MHD_Connection* connection,
|
||||
std::string rootLocation,
|
||||
const std::string& _url,
|
||||
const std::string& _rootLocation, // URI-encoded
|
||||
const std::string& unrootedUrl, // URI-decoded
|
||||
const std::string& _method,
|
||||
const std::string& version) :
|
||||
full_url(_url),
|
||||
url(fullURL2LocalURL(_url, rootLocation)),
|
||||
rootLocation(_rootLocation),
|
||||
url(unrootedUrl),
|
||||
method(str2RequestMethod(_method)),
|
||||
version(version),
|
||||
requestIndex(s_requestIndex++),
|
||||
@@ -80,6 +66,7 @@ RequestContext::RequestContext(struct MHD_Connection* connection,
|
||||
{
|
||||
MHD_get_connection_values(connection, MHD_HEADER_KIND, &RequestContext::fill_header, this);
|
||||
MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, &RequestContext::fill_argument, this);
|
||||
MHD_get_connection_values(connection, MHD_COOKIE_KIND, &RequestContext::fill_cookie, this);
|
||||
|
||||
try {
|
||||
acceptEncodingGzip =
|
||||
@@ -89,6 +76,8 @@ RequestContext::RequestContext(struct MHD_Connection* connection,
|
||||
try {
|
||||
byteRange_ = ByteRange::parse(get_header(MHD_HTTP_HEADER_RANGE));
|
||||
} catch (const std::out_of_range&) {}
|
||||
|
||||
userlang = determine_user_language();
|
||||
}
|
||||
|
||||
RequestContext::~RequestContext()
|
||||
@@ -110,14 +99,22 @@ MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind,
|
||||
if ( ! _this->queryString.empty() ) {
|
||||
_this->queryString += "&";
|
||||
}
|
||||
_this->queryString += key;
|
||||
_this->queryString += urlEncode(key);
|
||||
if ( value ) {
|
||||
_this->queryString += "=";
|
||||
_this->queryString += value;
|
||||
_this->queryString += urlEncode(value);
|
||||
}
|
||||
return MHD_YES;
|
||||
}
|
||||
|
||||
MHD_Result RequestContext::fill_cookie(void *__this, enum MHD_ValueKind kind,
|
||||
const char *key, const char* value)
|
||||
{
|
||||
RequestContext *_this = static_cast<RequestContext*>(__this);
|
||||
_this->cookies[key] = value == nullptr ? "" : value;
|
||||
return MHD_YES;
|
||||
}
|
||||
|
||||
void RequestContext::print_debug_info() const {
|
||||
printf("method : %s (%d)\n", method==RequestMethod::GET ? "GET" :
|
||||
method==RequestMethod::POST ? "POST" :
|
||||
@@ -139,7 +136,6 @@ void RequestContext::print_debug_info() const {
|
||||
printf("\n");
|
||||
}
|
||||
printf("Parsed : \n");
|
||||
printf("full_url: %s\n", full_url.c_str());
|
||||
printf("url : %s\n", url.c_str());
|
||||
printf("acceptEncodingGzip : %d\n", acceptEncodingGzip);
|
||||
printf("has_range : %d\n", byteRange_.kind() != ByteRange::NONE);
|
||||
@@ -177,11 +173,15 @@ std::string RequestContext::get_url_part(int number) const {
|
||||
}
|
||||
|
||||
std::string RequestContext::get_full_url() const {
|
||||
return full_url;
|
||||
return rootLocation + urlEncode(url);
|
||||
}
|
||||
|
||||
std::string RequestContext::get_root_path() const {
|
||||
return rootLocation.empty() ? "/" : rootLocation;
|
||||
}
|
||||
|
||||
bool RequestContext::is_valid_url() const {
|
||||
return !url.empty();
|
||||
return url.empty() || url[0] == '/';
|
||||
}
|
||||
|
||||
ByteRange RequestContext::get_range() const {
|
||||
@@ -198,16 +198,33 @@ std::string RequestContext::get_header(const std::string& name) const {
|
||||
}
|
||||
|
||||
std::string RequestContext::get_user_language() const
|
||||
{
|
||||
return userlang.lang;
|
||||
}
|
||||
|
||||
bool RequestContext::user_language_comes_from_cookie() const
|
||||
{
|
||||
return userlang.selectedBy == UserLanguage::SelectorKind::COOKIE;
|
||||
}
|
||||
|
||||
RequestContext::UserLanguage RequestContext::determine_user_language() const
|
||||
{
|
||||
try {
|
||||
return get_argument("userlang");
|
||||
return {UserLanguage::SelectorKind::QUERY_PARAM, get_argument("userlang")};
|
||||
} catch(const std::out_of_range&) {}
|
||||
|
||||
try {
|
||||
return get_header("Accept-Language");
|
||||
return {UserLanguage::SelectorKind::COOKIE, cookies.at("userlang")};
|
||||
} catch(const std::out_of_range&) {}
|
||||
|
||||
return "en";
|
||||
try {
|
||||
const std::string acceptLanguage = get_header("Accept-Language");
|
||||
const auto userLangPrefs = parseUserLanguagePreferences(acceptLanguage);
|
||||
const auto lang = selectMostSuitableLanguage(userLangPrefs);
|
||||
return {UserLanguage::SelectorKind::ACCEPT_LANGUAGE_HEADER, lang};
|
||||
} catch(const std::out_of_range&) {}
|
||||
|
||||
return {UserLanguage::SelectorKind::DEFAULT, "en"};
|
||||
}
|
||||
|
||||
std::string RequestContext::get_requested_format() const
|
||||
|
||||
@@ -57,8 +57,8 @@ class IndexError: public std::runtime_error {};
|
||||
class RequestContext {
|
||||
public: // functions
|
||||
RequestContext(struct MHD_Connection* connection,
|
||||
std::string rootLocation,
|
||||
const std::string& url,
|
||||
const std::string& rootLocation, // URI-encoded
|
||||
const std::string& unrootedUrl, // URI-decoded
|
||||
const std::string& method,
|
||||
const std::string& version);
|
||||
~RequestContext();
|
||||
@@ -91,6 +91,7 @@ class RequestContext {
|
||||
std::string get_url() const;
|
||||
std::string get_url_part(int part) const;
|
||||
std::string get_full_url() const;
|
||||
std::string get_root_path() const;
|
||||
|
||||
std::string get_query() const { return queryString; }
|
||||
|
||||
@@ -98,7 +99,7 @@ class RequestContext {
|
||||
std::string get_query(F filter, bool mustEncode) const {
|
||||
std::string q;
|
||||
const char* sep = "";
|
||||
auto encode = [=](const std::string& value) { return mustEncode?urlEncode(value, true):value; };
|
||||
auto encode = [=](const std::string& value) { return mustEncode?urlEncode(value):value; };
|
||||
for ( const auto& a : arguments ) {
|
||||
if (!filter(a.first)) {
|
||||
continue;
|
||||
@@ -118,8 +119,25 @@ class RequestContext {
|
||||
std::string get_user_language() const;
|
||||
std::string get_requested_format() const;
|
||||
|
||||
bool user_language_comes_from_cookie() const;
|
||||
|
||||
private: // types
|
||||
struct UserLanguage
|
||||
{
|
||||
enum SelectorKind
|
||||
{
|
||||
QUERY_PARAM,
|
||||
COOKIE,
|
||||
ACCEPT_LANGUAGE_HEADER,
|
||||
DEFAULT
|
||||
};
|
||||
|
||||
SelectorKind selectedBy;
|
||||
std::string lang;
|
||||
};
|
||||
|
||||
private: // data
|
||||
std::string full_url;
|
||||
std::string rootLocation;
|
||||
std::string url;
|
||||
RequestMethod method;
|
||||
std::string version;
|
||||
@@ -130,10 +148,15 @@ class RequestContext {
|
||||
ByteRange byteRange_;
|
||||
std::map<std::string, std::string> headers;
|
||||
std::map<std::string, std::vector<std::string>> arguments;
|
||||
std::map<std::string, std::string> cookies;
|
||||
std::string queryString;
|
||||
UserLanguage userlang;
|
||||
|
||||
private: // functions
|
||||
UserLanguage determine_user_language() const;
|
||||
|
||||
static MHD_Result fill_header(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
static MHD_Result fill_cookie(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
static MHD_Result fill_argument(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
};
|
||||
|
||||
|
||||
@@ -64,7 +64,13 @@ bool is_compressible_mime_type(const std::string& mimeType)
|
||||
|| mimeType.find("application/javascript") != std::string::npos
|
||||
|| mimeType.find("application/atom") != std::string::npos
|
||||
|| mimeType.find("application/opensearchdescription") != std::string::npos
|
||||
|| mimeType.find("application/json") != std::string::npos;
|
||||
|| mimeType.find("application/json") != std::string::npos
|
||||
|
||||
// Web fonts
|
||||
|| mimeType.find("application/font-") != std::string::npos
|
||||
|| mimeType.find("application/x-font-") != std::string::npos
|
||||
|| mimeType.find("application/vnd.ms-fontobject") != std::string::npos
|
||||
|| mimeType.find("font/") != std::string::npos;
|
||||
}
|
||||
|
||||
bool compress(std::string &content) {
|
||||
@@ -194,7 +200,7 @@ HTTP404Response::HTTP404Response(const InternalServer& server,
|
||||
|
||||
HTTPErrorResponse& HTTP404Response::operator+(UrlNotFoundMsg /*unused*/)
|
||||
{
|
||||
const std::string requestUrl = m_request.get_full_url();
|
||||
const std::string requestUrl = urlDecode(m_request.get_full_url(), false);
|
||||
return *this + ParameterizedMessage("url-not-found", {{"url", requestUrl}});
|
||||
}
|
||||
|
||||
@@ -228,7 +234,7 @@ HTTP400Response::HTTP400Response(const InternalServer& server,
|
||||
|
||||
HTTPErrorResponse& HTTP400Response::operator+(InvalidUrlMsg /*unused*/)
|
||||
{
|
||||
std::string requestUrl = m_request.get_full_url();
|
||||
std::string requestUrl = urlDecode(m_request.get_full_url(), false);
|
||||
const auto query = m_request.get_query();
|
||||
if (!query.empty()) {
|
||||
requestUrl += "?" + encodeDiples(query);
|
||||
@@ -381,6 +387,13 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
|
||||
MHD_add_response_header(response, p.first.c_str(), p.second.c_str());
|
||||
}
|
||||
|
||||
if ( ! request.user_language_comes_from_cookie() ) {
|
||||
const std::string cookie = "userlang=" + request.get_user_language()
|
||||
+ ";Path=" + request.get_root_path()
|
||||
+ ";Max-Age=31536000";
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_SET_COOKIE, cookie.c_str());
|
||||
}
|
||||
|
||||
if (m_returnCode == MHD_HTTP_OK && m_byteRange.kind() == ByteRange::RESOLVED_PARTIAL_CONTENT)
|
||||
m_returnCode = MHD_HTTP_PARTIAL_CONTENT;
|
||||
|
||||
|
||||
@@ -322,9 +322,6 @@ kainjow::mustache::data kiwix::onlyAsNonEmptyMustacheValue(const std::string& s)
|
||||
std::string kiwix::render_template(const std::string& template_str, kainjow::mustache::data data)
|
||||
{
|
||||
kainjow::mustache::mustache tmpl(template_str);
|
||||
kainjow::mustache::data urlencode{kainjow::mustache::lambda2{
|
||||
[](const std::string& str,const kainjow::mustache::renderer& r) { return urlEncode(r(str), true); }}};
|
||||
data.set("urlencoded", urlencode);
|
||||
std::stringstream ss;
|
||||
tmpl.render(data, [&ss](const std::string& str) { ss << str; });
|
||||
return ss.str();
|
||||
|
||||
@@ -493,12 +493,14 @@ static std::map<std::string, std::string> extMimeTypes = {
|
||||
{ "jpeg", "image/jpeg"},
|
||||
{ "jpg", "image/jpeg"},
|
||||
{ "gif", "image/gif"},
|
||||
{ "ico", "image/x-icon"},
|
||||
{ "svg", "image/svg+xml"},
|
||||
{ "txt", "text/plain"},
|
||||
{ "xml", "text/xml"},
|
||||
{ "pdf", "application/pdf"},
|
||||
{ "ogg", "application/ogg"},
|
||||
{ "js", "application/javascript"},
|
||||
{ "json", "application/json"},
|
||||
{ "css", "text/css"},
|
||||
{ "otf", "application/vnd.ms-opentype"},
|
||||
{ "ttf", "application/font-ttf"},
|
||||
|
||||
@@ -161,15 +161,14 @@ std::string kiwix::encodeDiples(const std::string& str)
|
||||
return result;
|
||||
}
|
||||
|
||||
/* urlEncode() based on javascript encodeURI() &
|
||||
encodeURIComponent(). Mostly code from rstudio/httpuv (GPLv3) */
|
||||
namespace
|
||||
{
|
||||
|
||||
bool isReservedUrlChar(char c)
|
||||
{
|
||||
switch (c) {
|
||||
case ';':
|
||||
case ',':
|
||||
case '/':
|
||||
case '?':
|
||||
case ':':
|
||||
case '@':
|
||||
@@ -177,22 +176,22 @@ bool isReservedUrlChar(char c)
|
||||
case '=':
|
||||
case '+':
|
||||
case '$':
|
||||
case '#':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool needsEscape(char c, bool encodeReserved)
|
||||
bool isHarmlessUriChar(char c)
|
||||
{
|
||||
if (c >= 'a' && c <= 'z')
|
||||
return false;
|
||||
return true;
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
return false;
|
||||
return true;
|
||||
if (c >= '0' && c <= '9')
|
||||
return false;
|
||||
if (isReservedUrlChar(c))
|
||||
return encodeReserved;
|
||||
return true;
|
||||
|
||||
switch (c) {
|
||||
case '-':
|
||||
case '_':
|
||||
@@ -203,9 +202,10 @@ bool needsEscape(char c, bool encodeReserved)
|
||||
case '\'':
|
||||
case '(':
|
||||
case ')':
|
||||
return false;
|
||||
case '/':
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
int hexToInt(char c) {
|
||||
@@ -230,18 +230,18 @@ int hexToInt(char c) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string kiwix::urlEncode(const std::string& value, bool encodeReserved)
|
||||
} // unnamed namespace
|
||||
|
||||
std::string kiwix::urlEncode(const std::string& value)
|
||||
{
|
||||
std::ostringstream os;
|
||||
os << std::hex << std::uppercase;
|
||||
for (std::string::const_iterator it = value.begin();
|
||||
it != value.end();
|
||||
it++) {
|
||||
|
||||
if (!needsEscape(*it, encodeReserved)) {
|
||||
os << *it;
|
||||
for (const char c : value) {
|
||||
if (isHarmlessUriChar(c)) {
|
||||
os << c;
|
||||
} else {
|
||||
os << '%' << std::setw(2) << static_cast<unsigned int>(static_cast<unsigned char>(*it));
|
||||
const unsigned int charVal = static_cast<unsigned char>(c);
|
||||
os << '%' << std::setw(2) << std::setfill('0') << charVal;
|
||||
}
|
||||
}
|
||||
return os.str();
|
||||
@@ -267,15 +267,15 @@ std::string kiwix::urlDecode(const std::string& value, bool component)
|
||||
int iHi = hexToInt(hi);
|
||||
int iLo = hexToInt(lo);
|
||||
if (iHi < 0 || iLo < 0) {
|
||||
// Invalid escape sequence
|
||||
os << '%' << hi << lo;
|
||||
continue;
|
||||
// Invalid escape sequence
|
||||
os << '%' << hi << lo;
|
||||
continue;
|
||||
}
|
||||
char c = (char)(iHi << 4 | iLo);
|
||||
if (!component && isReservedUrlChar(c)) {
|
||||
os << '%' << hi << lo;
|
||||
os << '%' << hi << lo;
|
||||
} else {
|
||||
os << c;
|
||||
os << c;
|
||||
}
|
||||
} else {
|
||||
os << *it;
|
||||
@@ -415,6 +415,17 @@ bool kiwix::startsWith(const std::string& base, const std::string& start)
|
||||
&& std::equal(start.begin(), start.end(), base.begin());
|
||||
}
|
||||
|
||||
std::string kiwix::stripSuffix(const std::string& str, const std::string& suffix)
|
||||
{
|
||||
if (str.size() > suffix.size()) {
|
||||
const auto subStr = str.substr(str.size() - suffix.size(), str.size());
|
||||
if (subStr == suffix) {
|
||||
return str.substr(0, str.size() - suffix.size());
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
std::vector<std::string> kiwix::getTitleVariants(const std::string& title) {
|
||||
std::vector<std::string> variants;
|
||||
variants.push_back(title);
|
||||
|
||||
@@ -55,7 +55,9 @@ private:
|
||||
};
|
||||
|
||||
|
||||
std::string urlEncode(const std::string& value, bool encodeReserved = false);
|
||||
/* urlEncode() is the equivalent of JS encodeURIComponent(), with the only
|
||||
* difference that the slash (/) symbol is NOT encoded. */
|
||||
std::string urlEncode(const std::string& value);
|
||||
std::string urlDecode(const std::string& value, bool component = false);
|
||||
|
||||
std::string join(const std::vector<std::string>& list, const std::string& sep);
|
||||
@@ -91,6 +93,8 @@ std::string extractFromString(const std::string& str);
|
||||
|
||||
bool startsWith(const std::string& base, const std::string& start);
|
||||
|
||||
std::string stripSuffix(const std::string& str, const std::string& suffix);
|
||||
|
||||
std::vector<std::string> getTitleVariants(const std::string& title);
|
||||
} //namespace kiwix
|
||||
#endif
|
||||
|
||||
@@ -17,15 +17,41 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
script_path = Path(__file__)
|
||||
|
||||
resource_file = script_path.parent / "i18n_resources_list.txt"
|
||||
translation_dir = script_path.parent / "i18n"
|
||||
translation_dir = script_path.parent / "skin/i18n"
|
||||
language_list_relpath = "skin/languages.js"
|
||||
|
||||
def get_translation_info(filepath):
|
||||
lang_code = Path(filepath).stem
|
||||
with open(filepath, 'r', encoding="utf-8") as f:
|
||||
content = json.load(f)
|
||||
lang_name = content.get("name")
|
||||
return lang_code, lang_name
|
||||
|
||||
language_list = []
|
||||
json_files = translation_dir.glob("*.json")
|
||||
with open(resource_file, 'w', encoding="utf-8") as f:
|
||||
for json in sorted(translation_dir.glob("*.json")):
|
||||
if json.name == "qqq.json":
|
||||
for i18n_file in sorted(translation_dir.glob("*.json")):
|
||||
if i18n_file.name == "qqq.json":
|
||||
continue
|
||||
f.write(str(json.relative_to(script_path.parent)) + '\n')
|
||||
print("Processing", i18n_file.name)
|
||||
if i18n_file.name != "test.json":
|
||||
lang_code, lang_name = get_translation_info(i18n_file)
|
||||
if lang_name:
|
||||
language_list.append((lang_code, lang_name))
|
||||
else:
|
||||
print(f"Warning: missing 'name' in {i18n_file.name}")
|
||||
f.write(str(i18n_file.relative_to(script_path.parent)) + '\n')
|
||||
|
||||
language_list = [{name: code} for code, name in sorted(language_list)]
|
||||
language_list_jsobj_str = json.dumps(language_list,
|
||||
indent=2,
|
||||
ensure_ascii=False)
|
||||
print("Saving", language_list_relpath)
|
||||
fullpath = script_path.parent / language_list_relpath
|
||||
with open(fullpath, 'w', encoding="utf-8") as f:
|
||||
f.write("const uiLanguages = " + language_list_jsobj_str)
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"MathXplore"
|
||||
]
|
||||
},
|
||||
"no-query": "クエリを指定していません。",
|
||||
"400-page-title": "無効なリクエストです",
|
||||
"400-page-heading": "無効なリクエストです",
|
||||
"404-page-title": "コンテンツが見つかりませんでした",
|
||||
"404-page-heading": "見つかりません",
|
||||
"500-page-title": "内部サーバーエラー",
|
||||
"500-page-heading": "内部サーバーエラー",
|
||||
"fulltext-search-unavailable": "全文検索は利用できません",
|
||||
"no-search-results": "このコンテンツでは全文検索エンジンが利用できません",
|
||||
"library-button-text": "ウェルカムページに移動",
|
||||
"random-page-button-text": "無作為に選ばれたページに移動する"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Kareyac"
|
||||
]
|
||||
},
|
||||
"name": "Fake language for i18n testing"
|
||||
, "suggest-full-text-search": "[I18N TESTING] cOnTaInInG '{{{SEARCH_TERMS}}}'..."
|
||||
, "no-such-book": "[I18N TESTING] No such book: {{BOOK_NAME}}. Sorry."
|
||||
, "url-not-found": "[I18N TESTING] URL not found: {{url}}"
|
||||
, "suggest-search": "[I18N TESTING] Make a full text search for <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>"
|
||||
, "400-page-title": "[I18N TESTING] Invalid request ($400 fine must be paid)"
|
||||
, "400-page-heading": "[I18N TESTING] -400 karma for an invalid request"
|
||||
, "404-page-title": "[I18N TESTING] Not Found - Try Again"
|
||||
, "404-page-heading": "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
, "library-button-text": "[I18N TESTING] Navigate to the welcome page"
|
||||
, "home-button-text": "[I18N TESTING] Jump to the main page of '{{BOOK_TITLE}}'"
|
||||
, "random-page-button-text": "[I18N TESTING] I am tired of determinism"
|
||||
, "searchbox-tooltip": "[I18N TESTING] Let's search in '{{BOOK_TITLE}}'"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Kly",
|
||||
"Winston Sung"
|
||||
]
|
||||
},
|
||||
"name": "繁體中文",
|
||||
"suggest-full-text-search": "正在包含「{{{SEARCH_TERMS}}}」…",
|
||||
"no-such-book": "沒有這樣的書籍:{{BOOK_NAME}}",
|
||||
"too-many-books": "請求太多個書籍({{NB_BOOKS}}),上限是 {{LIMIT}} 個",
|
||||
"no-book-found": "沒有書籍符合選擇標準",
|
||||
"url-not-found": "在此伺服器上找不到請求的 URL「{{url}}」。",
|
||||
"suggest-search": "建立 <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> 使用的全文搜尋",
|
||||
"random-article-failure": "哎呀!隨機挑選條目失敗 :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}}不是原始內容的有效請求。",
|
||||
"no-value-for-arg": "沒有為引數 {{ARGUMENT}} 提供內容",
|
||||
"no-query": "未提供查詢。",
|
||||
"raw-entry-not-found": "找不到{{DATATYPE}}項目{{ENTRY}}",
|
||||
"400-page-title": "無效請求",
|
||||
"400-page-heading": "無效請求",
|
||||
"404-page-title": "查無內容",
|
||||
"404-page-heading": "查無頁面",
|
||||
"500-page-title": "內部伺服器錯誤",
|
||||
"500-page-heading": "內部伺服器錯誤",
|
||||
"fulltext-search-unavailable": "全文搜尋無效",
|
||||
"no-search-results": "全文搜尋引擎不適用此內容。",
|
||||
"library-button-text": "前往歡迎首頁",
|
||||
"home-button-text": "前往「{{BOOK_TITLE}}」的首頁",
|
||||
"random-page-button-text": "前往隨機選取頁面",
|
||||
"searchbox-tooltip": "在{{BOOK_TITLE}}搜尋"
|
||||
}
|
||||
@@ -1,22 +1,30 @@
|
||||
i18n/bn.json
|
||||
i18n/cs.json
|
||||
i18n/de.json
|
||||
i18n/en.json
|
||||
i18n/fr.json
|
||||
i18n/he.json
|
||||
i18n/hy.json
|
||||
i18n/it.json
|
||||
i18n/ja.json
|
||||
i18n/ko.json
|
||||
i18n/ku-latn.json
|
||||
i18n/mk.json
|
||||
i18n/nqo.json
|
||||
i18n/pl.json
|
||||
i18n/ru.json
|
||||
i18n/sc.json
|
||||
i18n/sk.json
|
||||
i18n/sv.json
|
||||
i18n/test.json
|
||||
i18n/tr.json
|
||||
i18n/zh-hans.json
|
||||
i18n/zh-hant.json
|
||||
skin/i18n/ar.json
|
||||
skin/i18n/bn.json
|
||||
skin/i18n/cs.json
|
||||
skin/i18n/de.json
|
||||
skin/i18n/dga.json
|
||||
skin/i18n/el.json
|
||||
skin/i18n/en.json
|
||||
skin/i18n/fi.json
|
||||
skin/i18n/fr.json
|
||||
skin/i18n/he.json
|
||||
skin/i18n/hy.json
|
||||
skin/i18n/ia.json
|
||||
skin/i18n/it.json
|
||||
skin/i18n/ja.json
|
||||
skin/i18n/ko.json
|
||||
skin/i18n/ku-latn.json
|
||||
skin/i18n/lb.json
|
||||
skin/i18n/mk.json
|
||||
skin/i18n/nl.json
|
||||
skin/i18n/nqo.json
|
||||
skin/i18n/pl.json
|
||||
skin/i18n/ru.json
|
||||
skin/i18n/sc.json
|
||||
skin/i18n/sk.json
|
||||
skin/i18n/sl.json
|
||||
skin/i18n/sv.json
|
||||
skin/i18n/test.json
|
||||
skin/i18n/tr.json
|
||||
skin/i18n/zh-hans.json
|
||||
skin/i18n/zh-hant.json
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
resource_files = run_command(res_manager,
|
||||
'--list-all',
|
||||
files('resources_list.txt')
|
||||
).stdout().strip().split('\n')
|
||||
if meson.version().version_compare('>=0.47.0')
|
||||
resource_files = run_command(
|
||||
res_manager,
|
||||
'--list-all',
|
||||
files('resources_list.txt'),
|
||||
check: true
|
||||
).stdout().strip().split('\n')
|
||||
else
|
||||
resource_files = run_command(
|
||||
res_manager,
|
||||
'--list-all',
|
||||
files('resources_list.txt')
|
||||
).stdout().strip().split('\n')
|
||||
endif
|
||||
|
||||
|
||||
preprocessed_resources = custom_target('preprocessed_resource_files',
|
||||
input: 'resources_list.txt',
|
||||
@@ -14,7 +25,7 @@ preprocessed_resources = custom_target('preprocessed_resource_files',
|
||||
)
|
||||
|
||||
lib_resources = custom_target('resources',
|
||||
input: preprocessed_resources,
|
||||
input: [preprocessed_resources, 'i18n_resources_list.txt'],
|
||||
output: ['libkiwix-resources.cpp', 'libkiwix-resources.h'],
|
||||
command:[res_compiler,
|
||||
'--cxxfile', '@OUTPUT0@',
|
||||
@@ -30,11 +41,24 @@ lib_resources = custom_target('resources',
|
||||
# i18n_resource_files = fs.read('i18n_resources_list.txt').strip().split('\n')
|
||||
# ```
|
||||
# once we move to meson >= 0.57.0
|
||||
i18n_resource_files = run_command(find_program('python3'),
|
||||
'-c',
|
||||
'import sys; f=open(sys.argv[1]); print(f.read())',
|
||||
files('i18n_resources_list.txt')
|
||||
).stdout().strip().split('\n')
|
||||
|
||||
if meson.version().version_compare('>=0.47.0')
|
||||
i18n_resource_files = run_command(
|
||||
find_program('python3'),
|
||||
'-c',
|
||||
'import sys; f=open(sys.argv[1]); print(f.read())',
|
||||
files('i18n_resources_list.txt'),
|
||||
check: true
|
||||
).stdout().strip().split('\n')
|
||||
else
|
||||
i18n_resource_files = run_command(
|
||||
find_program('python3'),
|
||||
'-c',
|
||||
'import sys; f=open(sys.argv[1]); print(f.read())',
|
||||
files('i18n_resources_list.txt'),
|
||||
).stdout().strip().split('\n')
|
||||
endif
|
||||
|
||||
|
||||
i18n_resources = custom_target('i18n_resources',
|
||||
input: i18n_resource_files,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
skin/caret.png
|
||||
skin/bittorrent.png
|
||||
skin/magnet.png
|
||||
skin/feed.svg
|
||||
skin/langSelector.svg
|
||||
skin/download.png
|
||||
skin/hash.png
|
||||
skin/search-icon.svg
|
||||
@@ -15,6 +17,9 @@ skin/fonts/Roboto.ttf
|
||||
skin/search_results.css
|
||||
skin/blank.html
|
||||
skin/viewer.js
|
||||
skin/i18n.js
|
||||
skin/languages.js
|
||||
skin/mustache.min.js
|
||||
viewer.html
|
||||
templates/search_result.html
|
||||
templates/search_result.xml
|
||||
@@ -32,6 +37,8 @@ templates/catalog_v2_categories.xml
|
||||
templates/catalog_v2_languages.xml
|
||||
templates/url_of_search_results_css
|
||||
templates/viewer_settings.js
|
||||
templates/no_js_library_page.html
|
||||
templates/no_js_download.html
|
||||
opensearchdescription.xml
|
||||
ft_opensearchdescription.xml
|
||||
catalog_v2_searchdescription.xml
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
static/skin/feed.png
Normal file
BIN
static/skin/feed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 497 B |
22
static/skin/feed.svg
Normal file
22
static/skin/feed.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="uiLanguageSelectorButton" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#CCCCCC;}
|
||||
.st2{fill:#F78422;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M2.9,29.6c-1.4,0-2.6-1.1-2.6-2.6V2.9c0-1.4,1.1-2.6,2.6-2.6h24.1c1.4,0,2.6,1.1,2.6,2.6v24.1
|
||||
c0,1.4-1.1,2.6-2.6,2.6H2.9z"/>
|
||||
<path class="st1" d="M27.1,0.6c1.3,0,2.3,1,2.3,2.3v24.1c0,1.3-1,2.3-2.3,2.3H2.9c-1.3,0-2.3-1-2.3-2.3V2.9c0-1.3,1-2.3,2.3-2.3
|
||||
H27.1 M27.1,0.1H2.9c-1.6,0-2.8,1.3-2.8,2.8v24.1c0,1.6,1.3,2.8,2.8,2.8h24.1c1.6,0,2.8-1.3,2.8-2.8V2.9
|
||||
C29.9,1.4,28.6,0.1,27.1,0.1L27.1,0.1z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st2" d="M18,24h-3c0-5.2-4.2-9.4-9.4-9.4v-3C12.4,11.6,18,17.2,18,24z"/>
|
||||
<path class="st2" d="M24.5,24h-3c-0.1-8.7-7.2-15.9-16-15.9v-3C16,5.1,24.5,13.6,24.5,24z"/>
|
||||
<circle class="st2" cx="8.1" cy="21.6" r="2.6"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
158
static/skin/i18n.js
Normal file
158
static/skin/i18n.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import mustache from '../skin/mustache.min.js?KIWIXCACHEID'
|
||||
|
||||
const Translations = {
|
||||
defaultLanguage: null,
|
||||
currentLanguage: null,
|
||||
promises: {},
|
||||
data: {},
|
||||
|
||||
load: function(lang, asDefault=false) {
|
||||
if ( asDefault ) {
|
||||
this.defaultLanguage = lang;
|
||||
this.loadTranslationsJSON(lang);
|
||||
} else {
|
||||
this.currentLanguage = lang;
|
||||
if ( lang != this.defaultLanguage ) {
|
||||
this.loadTranslationsJSON(lang);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
loadTranslationsJSON: function(lang) {
|
||||
if ( this.promises[lang] )
|
||||
return;
|
||||
|
||||
const errorMsg = `Error loading translations for language '${lang}': `;
|
||||
this.promises[lang] = fetch(`./skin/i18n/${lang}.json`).then(async (resp) => {
|
||||
if ( resp.ok ) {
|
||||
this.data[lang] = JSON.parse(await resp.text());
|
||||
} else {
|
||||
console.log(errorMsg + resp.statusText);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(errorMsg + err);
|
||||
});
|
||||
},
|
||||
|
||||
whenReady: function(callback) {
|
||||
const defaultLangPromise = this.promises[this.defaultLanguage];
|
||||
const currentLangPromise = this.promises[this.currentLanguage];
|
||||
Promise.all([defaultLangPromise, currentLangPromise]).then(callback);
|
||||
},
|
||||
|
||||
get: function(msgId) {
|
||||
const activeTranslation = this.data[this.currentLanguage];
|
||||
|
||||
const r = activeTranslation && activeTranslation[msgId];
|
||||
if ( r )
|
||||
return r;
|
||||
|
||||
const defaultMsgs = this.data[this.defaultLanguage];
|
||||
if ( defaultMsgs )
|
||||
return defaultMsgs[msgId];
|
||||
|
||||
throw "Translations are not loaded";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function $t(msgId, params={}) {
|
||||
try {
|
||||
const msgTemplate = Translations.get(msgId);
|
||||
if ( ! msgTemplate ) {
|
||||
return "Invalid message id: " + msgId;
|
||||
}
|
||||
|
||||
return mustache.render(msgTemplate, params);
|
||||
} catch (err) {
|
||||
return "ERROR: " + err;
|
||||
}
|
||||
}
|
||||
|
||||
function getCookie(cookieName) {
|
||||
const name = cookieName + "=";
|
||||
let result;
|
||||
decodeURIComponent(document.cookie).split('; ').forEach(val => {
|
||||
if (val.indexOf(name) === 0) {
|
||||
result = val.substring(name.length);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
const DEFAULT_UI_LANGUAGE = 'en';
|
||||
|
||||
Translations.load(DEFAULT_UI_LANGUAGE, /*asDefault=*/true);
|
||||
|
||||
function getUserLanguage() {
|
||||
return new URLSearchParams(window.location.search).get('userlang')
|
||||
|| getCookie('userlang')
|
||||
|| DEFAULT_UI_LANGUAGE;
|
||||
}
|
||||
|
||||
function setUserLanguage(lang, callback) {
|
||||
setPermanentGlobalCookie('userlang', lang);
|
||||
Translations.load(lang);
|
||||
Translations.whenReady(callback);
|
||||
}
|
||||
|
||||
function createModalUILanguageSelector() {
|
||||
document.body.insertAdjacentHTML('beforeend',
|
||||
`<div id="uiLanguageSelector" class="modal-wrapper">
|
||||
<div class="modal">
|
||||
<div class="modal-heading">
|
||||
<div class="modal-title">
|
||||
<div>
|
||||
Select UI language
|
||||
</div>
|
||||
</div>
|
||||
<div onclick="window.modalUILanguageSelector.close()" class="modal-close-button">
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7071 1.70711C14.0976 1.31658 14.0976
|
||||
0.683417 13.7071 0.292893C13.3166 -0.0976311 12.6834 -0.0976311 12.2929 0.292893L7 5.58579L1.70711
|
||||
0.292893C1.31658 -0.0976311 0.683417 -0.0976311 0.292893 0.292893C-0.0976311 0.683417
|
||||
-0.0976311 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976311 12.6834
|
||||
-0.0976311 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7
|
||||
8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166
|
||||
14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z" fill="black" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<select id="ui_language"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
window.modalUILanguageSelector = {
|
||||
show: () => {
|
||||
document.getElementById('uiLanguageSelector').style.display = 'flex';
|
||||
},
|
||||
|
||||
close: () => {
|
||||
document.getElementById('uiLanguageSelector').style.display = 'none';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function initUILanguageSelector(activeLanguage, languageChangeCallback) {
|
||||
if ( document.getElementById("ui_language") == null ) {
|
||||
createModalUILanguageSelector();
|
||||
}
|
||||
const languageSelector = document.getElementById("ui_language");
|
||||
for (const lang of uiLanguages ) {
|
||||
const lang_name = Object.getOwnPropertyNames(lang)[0];
|
||||
const lang_code = lang[lang_name];
|
||||
const is_selected = lang_code == activeLanguage;
|
||||
languageSelector.appendChild(new Option(lang_name, lang_code, is_selected, is_selected));
|
||||
}
|
||||
languageSelector.onchange = languageChangeCallback;
|
||||
}
|
||||
|
||||
window.$t = $t;
|
||||
window.getUserLanguage = getUserLanguage;
|
||||
window.setUserLanguage = setUserLanguage;
|
||||
window.initUILanguageSelector = initUILanguageSelector;
|
||||
33
static/skin/i18n/ar.json
Normal file
33
static/skin/i18n/ar.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Asma",
|
||||
"Ravan",
|
||||
"محمد أحمد عبد الفتاح"
|
||||
]
|
||||
},
|
||||
"name": "الإنجليزية",
|
||||
"no-such-book": "لا يوجد مثل هذا الكتاب: {{BOOK_NAME}}",
|
||||
"too-many-books": "طلب العديد من الكتب {{NB_BOOKS}} حيث الحد {{LIMIT}}",
|
||||
"no-book-found": "لا يوجد كتاب يطابق معايير الاختيار",
|
||||
"url-not-found": "لم يتم العثور على عنوان URL المطلوب \"{{url}}\" على هذا الخادم.",
|
||||
"suggest-search": "قم بإجراء بحث عن النص الكامل لـ <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "مع الأسف! فشل اختيار مقال عشوائي :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} ليس طلبًا صالحًا للمحتوى الأولي.",
|
||||
"no-value-for-arg": "لم يتم تقديم قيمة للوسيطة {{ARGUMENT}}",
|
||||
"no-query": "لم يتم تقديم ملخص.",
|
||||
"raw-entry-not-found": "لا يمكن العثور على إدخال {{DATATYPE}} {{ENTRY}}",
|
||||
"400-page-title": "طلب غير صالح",
|
||||
"400-page-heading": "طلب غير صالح",
|
||||
"404-page-title": "المحتوى غير موجود",
|
||||
"404-page-heading": "لم يتم العثور عليه",
|
||||
"500-page-title": "خطأ في الخادم الداخلي",
|
||||
"500-page-heading": "خطأ في الخادم الداخلي",
|
||||
"fulltext-search-unavailable": "البحث عن النص الكامل غير متاح",
|
||||
"no-search-results": "محرك البحث عن النص الكامل غير متاح لهذا المحتوى.",
|
||||
"library-button-text": "اذهب لصفحة الترحيب",
|
||||
"home-button-text": "انتقل إلى الصفحة الرئيسية لـ \"{{BOOK_TITLE}}\"",
|
||||
"random-page-button-text": "اذهب إلى صفحة عشوائية",
|
||||
"searchbox-tooltip": "بحث \"{{BOOK_TITLE}}\"",
|
||||
"confusion-of-tongues": "قد يشارك في البحث كتابان أو أكثر بلغات مختلفة، مما قد يؤدي إلى نتائج محيرة."
|
||||
}
|
||||
@@ -10,5 +10,9 @@
|
||||
"500-page-heading": "অভ্যন্তরীণ সার্ভার ত্রুটি",
|
||||
"library-button-text": "স্বাগত পাতায় চলুন",
|
||||
"home-button-text": "'{{BOOK_TITLE}}'-এর প্রধান পাতায় চলুন",
|
||||
"searchbox-tooltip": "'{{BOOK_TITLE}}' অনুসন্ধান করুন"
|
||||
"searchbox-tooltip": "'{{BOOK_TITLE}}' অনুসন্ধান করুন",
|
||||
"search": "অনুসন্ধান",
|
||||
"welcome-to-kiwix-server": "কিউইক্স সার্ভারে স্বাগতম",
|
||||
"download-links-title": "বই ডাউনলোড করুন",
|
||||
"preview-book": "প্রাকদর্শন"
|
||||
}
|
||||
21
static/skin/i18n/dga.json
Normal file
21
static/skin/i18n/dga.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Alhaji Yakubu",
|
||||
"Amire80"
|
||||
]
|
||||
},
|
||||
"welcome-page-overzealous-filter": "Duoro kyebe. <a href=\"{{URL}}\">E na boɔra ka fo</a>?",
|
||||
"search": "Bo",
|
||||
"book-filtering-all-categories": "Zagre zaa",
|
||||
"book-filtering-all-languages": "Kɔkɔrɛɛ zaa",
|
||||
"count-of-matching-books": "{{COUNT}} gama",
|
||||
"download": "Tagebo",
|
||||
"direct-download-link-text": "Toribu",
|
||||
"direct-download-alt-text": "Toribu tagebo",
|
||||
"hash-download-alt-text": "Tage bonmannaa",
|
||||
"magnet-link-text": "Kurimaraa sobie",
|
||||
"magnet-alt-text": "Tage kurimaraa",
|
||||
"filter-by-tag": "Guy yi kpuli {{TAG}}",
|
||||
"stop-filtering-by-tag": "Bare gyɛɛbo kpuli {{TAG}}"
|
||||
}
|
||||
22
static/skin/i18n/el.json
Normal file
22
static/skin/i18n/el.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Kelson",
|
||||
"Norhorn"
|
||||
]
|
||||
},
|
||||
"welcome-page-overzealous-filter": "Κανένα αποτέλεσμα. Θέλετε να <a href=\"{{URL}}\">επαναφέρετε το φίλτρο</a>;",
|
||||
"powered-by-kiwix-html": "Με την υποστήριξη by <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Αναζήτηση",
|
||||
"book-filtering-all-categories": "Όλες οι κατηγορίες",
|
||||
"book-filtering-all-languages": "Όλες οι γλώσσες",
|
||||
"count-of-matching-books": "{{COUNT}} βιβλίο(α)",
|
||||
"download": "Λήψη",
|
||||
"direct-download-link-text": "Απευθείας",
|
||||
"direct-download-alt-text": "άμεση λήψη",
|
||||
"hash-download-alt-text": "λήψη αναγνωριστικού",
|
||||
"torrent-download-link-text": "Αρχείο torrent",
|
||||
"torrent-download-alt-text": "λήψη torrent",
|
||||
"filter-by-tag": "Φίλτρο ανά ετικέτα \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Διακοπή φίλτρου ανά ετικέτα \"{{TAG}}\""
|
||||
}
|
||||
@@ -28,4 +28,27 @@
|
||||
, "random-page-button-text": "Go to a randomly selected page"
|
||||
, "searchbox-tooltip": "Search '{{BOOK_TITLE}}'"
|
||||
, "confusion-of-tongues": "Two or more books in different languages would participate in search, which may lead to confusing results."
|
||||
, "welcome-page-overzealous-filter": "No result. Would you like to <a href=\"{{URL}}\">reset filter</a>?"
|
||||
, "powered-by-kiwix-html": "Powered by <a href=\"https://kiwix.org\">Kiwix</a>"
|
||||
, "search": "Search"
|
||||
, "book-filtering-all-categories": "All categories"
|
||||
, "book-filtering-all-languages": "All languages"
|
||||
, "count-of-matching-books": "{{COUNT}} book(s)"
|
||||
, "download": "Download"
|
||||
, "direct-download-link-text": "Direct"
|
||||
, "direct-download-alt-text": "direct download"
|
||||
, "hash-download-link-text": "Sha256 hash"
|
||||
, "hash-download-alt-text": "download hash"
|
||||
, "magnet-link-text": "Magnet link"
|
||||
, "magnet-alt-text": "download magnet"
|
||||
, "torrent-download-link-text": "Torrent file"
|
||||
, "torrent-download-alt-text": "download torrent"
|
||||
, "library-opds-feed-all-entries": "Library OPDS Feed - All entries"
|
||||
, "filter-by-tag": "Filter by tag \"{{TAG}}\""
|
||||
, "stop-filtering-by-tag": "Stop filtering by tag \"{{TAG}}\""
|
||||
, "library-opds-feed-parameterised": "Library OPDS Feed - entries matching {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}"
|
||||
, "welcome-to-kiwix-server": "Welcome to Kiwix Server"
|
||||
, "download-links-heading": "Download links for <b><i>{{BOOK_TITLE}}</i></b>"
|
||||
, "download-links-title": "Download book"
|
||||
, "preview-book": "Preview"
|
||||
}
|
||||
30
static/skin/i18n/fi.json
Normal file
30
static/skin/i18n/fi.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"MITO",
|
||||
"Nike",
|
||||
"Pyscowicz"
|
||||
]
|
||||
},
|
||||
"name": "suomi",
|
||||
"suggest-full-text-search": "sisältää '{{{SEARCH_TERMS}}}'...",
|
||||
"url-not-found": "Pyydettyä URL-osoitetta \"{{url}}\" ei löytynyt tältä palvelimelta.",
|
||||
"400-page-title": "Virheellinen pyyntö",
|
||||
"400-page-heading": "Virheellinen pyyntö",
|
||||
"404-page-title": "Sisältöä ei löytynyt",
|
||||
"404-page-heading": "Ei löytynyt",
|
||||
"500-page-title": "Sisäinen palvelinvirhe",
|
||||
"500-page-heading": "Sisäinen palvelinvirhe",
|
||||
"library-button-text": "Siirry tervetulosivulle",
|
||||
"home-button-text": "Siirry kirjan '{{BOOK_TITLE}}' etusivulle",
|
||||
"random-page-button-text": "Siirry satunnaiselle sivulle",
|
||||
"searchbox-tooltip": "Hae '{{BOOK_TITLE}}'",
|
||||
"search": "Hae",
|
||||
"book-filtering-all-categories": "Kaikki luokat",
|
||||
"book-filtering-all-languages": "Kaikki kielet",
|
||||
"download": "Lataa",
|
||||
"torrent-download-link-text": "Torrent-tiedosto",
|
||||
"filter-by-tag": "Suodata tunnisteen ”{{TAG}}” mukaan",
|
||||
"download-links-title": "Lataa kirja",
|
||||
"preview-book": "Esikatsele"
|
||||
}
|
||||
@@ -30,5 +30,28 @@
|
||||
"home-button-text": "Aller à la page principale de « {{BOOK_TITLE}} »",
|
||||
"random-page-button-text": "Aller à une page sélectionnée aléatoirement",
|
||||
"searchbox-tooltip": "Rechercher « {{BOOK_TITLE}} »",
|
||||
"confusion-of-tongues": "Deux livres ou plus dans des langues différentes participeraient à la recherche, ce qui pourrait conduire à des résultats confus."
|
||||
"confusion-of-tongues": "Deux livres ou plus dans des langues différentes participeraient à la recherche, ce qui pourrait conduire à des résultats confus.",
|
||||
"welcome-page-overzealous-filter": "Aucun résultat. Souhaitez-vous <a href=\"{{URL}}\">réinitialiser le filtre</a> ?",
|
||||
"powered-by-kiwix-html": "Propulsé par <a href=\"https://kiwix.org/\">Kiwix</a>",
|
||||
"search": "Rechercher",
|
||||
"book-filtering-all-categories": "Toutes les catégories",
|
||||
"book-filtering-all-languages": "Toutes les langues",
|
||||
"count-of-matching-books": "{{COUNT}} livre(s)",
|
||||
"download": "Télécharger",
|
||||
"direct-download-link-text": "Direct",
|
||||
"direct-download-alt-text": "téléchargement direct",
|
||||
"hash-download-link-text": "Hachage sha256",
|
||||
"hash-download-alt-text": "télécharger le hachage",
|
||||
"magnet-link-text": "Lien Magnet",
|
||||
"magnet-alt-text": "télécharger le lien Magnet",
|
||||
"torrent-download-link-text": "Fichier torrent",
|
||||
"torrent-download-alt-text": "télécharger le torrent",
|
||||
"library-opds-feed-all-entries": "Flux OPDS de la bibliothèque – Toutes les entrées",
|
||||
"filter-by-tag": "Filtrer par la balise « {{TAG}} »",
|
||||
"stop-filtering-by-tag": "Arrêter le filtrage par la balise « {{TAG}} »",
|
||||
"library-opds-feed-parameterised": "Flux OPDS de la bibliothèque – Entrées correspondant à {{#LANG}} :\n ▪ Langue : {{LANG}} {{/LANG}}{{#CATEGORY}}\n ▪ Catégorie : {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\n ▪ Étiquette : {{TAG}} {{/TAG}}{{#Q}}\n ▪ Requête : {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Bienvenue sur le Serveur Kiwix",
|
||||
"download-links-heading": "Liens de téléchargement pour <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Télécharger le livre",
|
||||
"preview-book": "Aperçu"
|
||||
}
|
||||
@@ -29,5 +29,28 @@
|
||||
"home-button-text": "מעבר לדף הראשי של \"{{BOOK_TITLE}}\"",
|
||||
"random-page-button-text": "מעבר לדף שנבחר אקראית",
|
||||
"searchbox-tooltip": "חיפוש \"{{BOOK_TITLE}}\"",
|
||||
"confusion-of-tongues": "שני ספרים או יותר בשפות שונות ישתתפו בחיפוש, מה שעלול להוביל לתוצאות מבלבלות."
|
||||
"confusion-of-tongues": "שני ספרים או יותר בשפות שונות ישתתפו בחיפוש, מה שעלול להוביל לתוצאות מבלבלות.",
|
||||
"welcome-page-overzealous-filter": "אין תוצאות. האם <a href=\"{{URL}}\">לאפס את המסנן</a>?",
|
||||
"powered-by-kiwix-html": "מופעל על־ידי <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "חיפוש",
|
||||
"book-filtering-all-categories": "כל הקטגוריות",
|
||||
"book-filtering-all-languages": "כל השפות",
|
||||
"count-of-matching-books": "{{COUNT}} ספרים",
|
||||
"download": "הורדה",
|
||||
"direct-download-link-text": "ישירה",
|
||||
"direct-download-alt-text": "הורדה ישירה",
|
||||
"hash-download-link-text": "גיבוב Sha256",
|
||||
"hash-download-alt-text": "הורדת גיבוב",
|
||||
"magnet-link-text": "קישור Magnet",
|
||||
"magnet-alt-text": "הורדת magnet",
|
||||
"torrent-download-link-text": "קובץ טורנט",
|
||||
"torrent-download-alt-text": "הורדת טורנט",
|
||||
"library-opds-feed-all-entries": "הזנת ספריית OPDS - כל הרשומות",
|
||||
"filter-by-tag": "סינון לפי התג \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "להפסיק סינון לפי התג \"{{TAG}}\"",
|
||||
"library-opds-feed-parameterised": "הזנת ספריית OPDS - רשומות שתואמות ל{{#LANG}}\nשפה: {{LANG}} {{/LANG}}{{#CATEGORY}}\nקטגוריה: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nתג: {{TAG}} {{/TAG}}{{#Q}}\nשאילתה: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "ברוך בואך לשרת קיוויקס",
|
||||
"download-links-heading": "הורדת קישורים עבור <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "הורדת ספר",
|
||||
"preview-book": "תצוגה מקדימה"
|
||||
}
|
||||
@@ -16,5 +16,6 @@
|
||||
"library-button-text": "Գրադարանի էջ",
|
||||
"home-button-text": "Դեպի '{{BOOK_TITLE}}'֊ի գլխավոր էջը",
|
||||
"random-page-button-text": "Բացել պատահական էջ",
|
||||
"searchbox-tooltip": "Որոնել '{{BOOK_TITLE}}'֊ում"
|
||||
"searchbox-tooltip": "Որոնել '{{BOOK_TITLE}}'֊ում",
|
||||
"book-filtering-all-categories": "Բոլոր կատեգորիաներ"
|
||||
}
|
||||
56
static/skin/i18n/ia.json
Normal file
56
static/skin/i18n/ia.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Kelson",
|
||||
"McDutchie"
|
||||
]
|
||||
},
|
||||
"name": "interlingua",
|
||||
"suggest-full-text-search": "continente '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Necun tal libro: {{BOOK_NAME}}",
|
||||
"too-many-books": "Troppo de libros demandate ({{NB_BOOKS}}); le limite es {{LIMIT}}",
|
||||
"no-book-found": "Necun libro corresponde al criterios de selection",
|
||||
"url-not-found": "Le URL reuqestate \"{{url}}\" non ha essite trovate sur iste servitor.",
|
||||
"suggest-search": "Facer un recerca in texto complete de <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ups! Non poteva eliger un articulo aleatori :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} non es un request valide pro contento crude.",
|
||||
"no-value-for-arg": "Necun valor fornite pro le argumento {{ARGUMENT}}",
|
||||
"no-query": "Necun consulta fornite.",
|
||||
"raw-entry-not-found": "Non pote trovar le entrata {{ENTRY}} del typo {{DATATYPE}}",
|
||||
"400-page-title": "Requesta invalide",
|
||||
"400-page-heading": "Requesta invalide",
|
||||
"404-page-title": "Contento non trovate",
|
||||
"404-page-heading": "Non trovate",
|
||||
"500-page-title": "Error interne del servitor",
|
||||
"500-page-heading": "Error interne del servitor",
|
||||
"fulltext-search-unavailable": "Le recerca in texto complete es indisponibile",
|
||||
"no-search-results": "Le motor de recerca in texto complete non es disponibile pro iste contento.",
|
||||
"library-button-text": "Ir al pagina de benvenita",
|
||||
"home-button-text": "Ir al pagina principal de ''{{BOOK_TITLE}}",
|
||||
"random-page-button-text": "Ir a un pagina seligite aleatorimente",
|
||||
"searchbox-tooltip": "Cercar '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "Duo o plus libros in differente linguas participarea in le recerca, lo que pote menar a resultatos confuse.",
|
||||
"welcome-page-overzealous-filter": "Nulle resultato. Vole tu <a href=\"{{URL}}\">reinitialisar le filtro</a>?",
|
||||
"powered-by-kiwix-html": "Actionate per <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Cercar",
|
||||
"book-filtering-all-categories": "Tote le categorias",
|
||||
"book-filtering-all-languages": "Tote le linguas",
|
||||
"count-of-matching-books": "{{COUNT}} libro(s)",
|
||||
"download": "Discargar",
|
||||
"direct-download-link-text": "Directe",
|
||||
"direct-download-alt-text": "discargamento directe",
|
||||
"hash-download-link-text": "Hash SHA256",
|
||||
"hash-download-alt-text": "hash del discargamento",
|
||||
"magnet-link-text": "Ligamine Magnet",
|
||||
"magnet-alt-text": "ligamine \"magnet\" de discargamento",
|
||||
"torrent-download-link-text": "File Torrent",
|
||||
"torrent-download-alt-text": "discargar Torrent",
|
||||
"library-opds-feed-all-entries": "Fluxo OPDS del bibliotheca – Tote le entratas",
|
||||
"filter-by-tag": "Filtrar per etiquetta \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Non plus filtrar per etiquetta \"{{TAG}}\"",
|
||||
"library-opds-feed-parameterised": "Fluxo OPDS del bibliotheca – Entratas correspondente a {{#LANG}}\nLingua {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategoria: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nEtiquetta: {{TAG}} {{/TAG}}{{#Q}}\nConsulta: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Benvenite al servitor Kiwix",
|
||||
"download-links-heading": "Discargar ligamines pro <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Discargar libro",
|
||||
"preview-book": "Previsualisation"
|
||||
}
|
||||
@@ -2,12 +2,13 @@
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Albano",
|
||||
"Beta16"
|
||||
"Beta16",
|
||||
"McDutchie"
|
||||
]
|
||||
},
|
||||
"name": "italiano",
|
||||
"suggest-full-text-search": "contenente '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Nessun libro del genere: {{BOOK_NAME}}",
|
||||
"no-such-book": "Nessun libro con questo nome: {{BOOK_NAME}}",
|
||||
"too-many-books": "Troppi libri richiesti ({{NB_BOOKS}}) dove il limite è {{LIMIT}}",
|
||||
"no-book-found": "Nessun libro corrisponde ai criteri di selezione",
|
||||
"url-not-found": "L'URL richiesto \"{{url}}\" non è stato trovato in questo server.",
|
||||
@@ -23,5 +24,11 @@
|
||||
"library-button-text": "Vai alla pagina di benvenuto",
|
||||
"home-button-text": "Vai alla pagina principale di '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Vai a una pagina selezionata casualmente",
|
||||
"searchbox-tooltip": "Cerca '{{BOOK_TITLE}}'"
|
||||
"searchbox-tooltip": "Cerca '{{BOOK_TITLE}}'",
|
||||
"book-filtering-all-categories": "Tutte le categorie",
|
||||
"book-filtering-all-languages": "Tutte le lingue",
|
||||
"count-of-matching-books": "{{COUNT}} libro/i",
|
||||
"download": "Scarica",
|
||||
"download-links-title": "Scarica libro",
|
||||
"preview-book": "Anteprima"
|
||||
}
|
||||
33
static/skin/i18n/ja.json
Normal file
33
static/skin/i18n/ja.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"MathXplore",
|
||||
"もなー(偽物)"
|
||||
]
|
||||
},
|
||||
"name": "日本語",
|
||||
"no-query": "クエリを指定していません。",
|
||||
"400-page-title": "無効なリクエストです",
|
||||
"400-page-heading": "無効なリクエストです",
|
||||
"404-page-title": "コンテンツが見つかりませんでした",
|
||||
"404-page-heading": "見つかりません",
|
||||
"500-page-title": "内部サーバーエラー",
|
||||
"500-page-heading": "内部サーバーエラー",
|
||||
"fulltext-search-unavailable": "全文検索は利用できません",
|
||||
"no-search-results": "このコンテンツでは全文検索エンジンが利用できません",
|
||||
"library-button-text": "ウェルカムページに移動",
|
||||
"random-page-button-text": "無作為に選ばれたページに移動する",
|
||||
"search": "検索",
|
||||
"book-filtering-all-categories": "すべてのカテゴリー",
|
||||
"book-filtering-all-languages": "すべての言語",
|
||||
"download": "ダウンロード",
|
||||
"direct-download-link-text": "直ダウンロードリンク",
|
||||
"direct-download-alt-text": "直ダウンロード",
|
||||
"hash-download-link-text": "Sha256 ハッシュ",
|
||||
"hash-download-alt-text": "ハッシュをダウンロード",
|
||||
"magnet-link-text": "マグネットリンク",
|
||||
"magnet-alt-text": "マグネットをダウンロード",
|
||||
"torrent-download-link-text": "Torrentファイル",
|
||||
"torrent-download-alt-text": "Torrentをダウンロード",
|
||||
"preview-book": "プレビュー"
|
||||
}
|
||||
@@ -14,5 +14,6 @@
|
||||
"500-page-title": "내부 서버 오류",
|
||||
"500-page-heading": "내부 서버 오류",
|
||||
"fulltext-search-unavailable": "전문 검색을 사용할 수 없습니다",
|
||||
"random-page-button-text": "무작위로 선택된 문서로 이동"
|
||||
"random-page-button-text": "무작위로 선택된 문서로 이동",
|
||||
"preview-book": "미리 보기"
|
||||
}
|
||||
27
static/skin/i18n/lb.json
Normal file
27
static/skin/i18n/lb.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Kelson",
|
||||
"Robby",
|
||||
"Volvox"
|
||||
]
|
||||
},
|
||||
"name": "Lëtzebuergesch",
|
||||
"suggest-search": "Maacht eng Volltext-Sich fir <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ups! Et konnt keen zoufällegen Artikel ausgewielt ginn :(",
|
||||
"404-page-title": "Inhalt net fonnt",
|
||||
"404-page-heading": "Net fonnt",
|
||||
"500-page-title": "Interne Feeler um Server",
|
||||
"500-page-heading": "Interne Feeler um Server",
|
||||
"fulltext-search-unavailable": "Volltext-Sich net verfügbar",
|
||||
"home-button-text": "Gitt op d'Haaptsäit vun '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Gitt op eng zoufälleg gewielte Säit",
|
||||
"searchbox-tooltip": "No '{{BOOK_TITLE}}' sichen",
|
||||
"welcome-page-overzealous-filter": "Kee Resultat. Wëllt Dir <a href=\"{{URL}}\">de Filter zrécksetzen</a>?",
|
||||
"search": "Sichen",
|
||||
"book-filtering-all-categories": "All Kategorien",
|
||||
"book-filtering-all-languages": "All Sproochen",
|
||||
"count-of-matching-books": "{{COUNT}} Buch/Bicher",
|
||||
"download": "Eroflueden",
|
||||
"direct-download-link-text": "Direkt"
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Bjankuloski06"
|
||||
"Bjankuloski06",
|
||||
"Kelson"
|
||||
]
|
||||
},
|
||||
"name": "македонски",
|
||||
@@ -28,5 +29,28 @@
|
||||
"home-button-text": "Оди на главната страница на „{{BOOK_TITLE}}“",
|
||||
"random-page-button-text": "Оди на случајно избрана страница",
|
||||
"searchbox-tooltip": "Пребарај го „{{BOOK_TITLE}}“",
|
||||
"confusion-of-tongues": "Во пребарувањето ќе учествуваат две или повеќе книги на различни јазици, што може да довете до збунувачки исход."
|
||||
"confusion-of-tongues": "Во пребарувањето ќе учествуваат две или повеќе книги на различни јазици, што може да довете до збунувачки исход.",
|
||||
"welcome-page-overzealous-filter": "Нема исход. Дали би сакале да го <a href=\"{{URL}}\">поништите филтерот</a>?",
|
||||
"powered-by-kiwix-html": "Овозможено од <a href=\"https://kiwix.org\">Кивикс</a>",
|
||||
"search": "Пребарај",
|
||||
"book-filtering-all-categories": "Сите категории",
|
||||
"book-filtering-all-languages": "Сите јазици",
|
||||
"count-of-matching-books": "{{COUNT}} книги",
|
||||
"download": "Преземи",
|
||||
"direct-download-link-text": "Непосредно",
|
||||
"direct-download-alt-text": "непосредно преземање",
|
||||
"hash-download-link-text": "Sha256-тараба",
|
||||
"hash-download-alt-text": "преземи тараба",
|
||||
"magnet-link-text": "Магнетна врска",
|
||||
"magnet-alt-text": "преземи магнет",
|
||||
"torrent-download-link-text": "Торентна податотека",
|
||||
"torrent-download-alt-text": "преземи торент",
|
||||
"library-opds-feed-all-entries": "Библиотечен тековник на OPDS — Сите ставки",
|
||||
"filter-by-tag": "Филтрирај по ознаката „{{TAG}}“",
|
||||
"stop-filtering-by-tag": "Запри филтрирање по ознаката „{{TAG}}“",
|
||||
"library-opds-feed-parameterised": "Библиотечен тековник на OPDS — ставки што одговараат на {{#LANG}}\nЈазик: {{LANG}} {{/LANG}}{{#CATEGORY}}\nКатегорија: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nОзнака: {{TAG}} {{/TAG}}{{#Q}}\nБарање: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Добре дојдовте на Опслужувачот на Кивикс",
|
||||
"download-links-heading": "Врски за преземање на <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Преземи книга",
|
||||
"preview-book": "Преглед"
|
||||
}
|
||||
30
static/skin/i18n/nl.json
Normal file
30
static/skin/i18n/nl.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Kelson",
|
||||
"McDutchie",
|
||||
"Vistaus"
|
||||
]
|
||||
},
|
||||
"too-many-books": "Er zijn teveel boeken opgevraagd ({{NB_BOOKS}}). Het limiet is {{LIMIT}}.",
|
||||
"no-book-found": "Er zijn geen boeken die overeenkomen met de zoekcriteria",
|
||||
"no-value-for-arg": "Er is geen waarde opgegeven bij {{ARGUMENT}}",
|
||||
"no-query": "Er is geen zoekterm opgegeven.",
|
||||
"welcome-page-overzealous-filter": "Geen resultaat. Wilt u <a href=\"{{URL}}\">het filter resetten</a>?",
|
||||
"powered-by-kiwix-html": "Mogelijk gemaakt door <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Zoeken",
|
||||
"book-filtering-all-categories": "Alle categorieën",
|
||||
"book-filtering-all-languages": "Alle talen",
|
||||
"count-of-matching-books": "{{COUNT}} boek(en)",
|
||||
"download": "Downloaden",
|
||||
"direct-download-link-text": "Direct",
|
||||
"direct-download-alt-text": "directe download",
|
||||
"hash-download-link-text": "SHA256-hash",
|
||||
"hash-download-alt-text": "controlesom (hash) van de download",
|
||||
"magnet-link-text": "Magnet-link",
|
||||
"magnet-alt-text": "magnet-link van de download",
|
||||
"torrent-download-link-text": "Torrent-bestand",
|
||||
"torrent-download-alt-text": "torrent downloaden",
|
||||
"filter-by-tag": "Filteren op tag \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Stoppen met filteren op tag \"{{TAG}}\""
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Amire80",
|
||||
"Lancine.kounfantoh.fofana"
|
||||
]
|
||||
},
|
||||
@@ -27,5 +28,23 @@
|
||||
"library-button-text": "ߕߊ߯ ߟߊ߬ߛߣߍ߬ߟߌ߫ ߞߐߜߍ ߞߊ߲߬",
|
||||
"home-button-text": "ߕߊ߯ {{BOOK_TITLE}} ߓߏ߬ߟߏ߲߬ߘߊ ߞߐߜߍ ߞߊ߲߬",
|
||||
"random-page-button-text": "ߕߊ߯ ߓߍ߲߬ߛߋ߲߬ߡߊ߬ ߞߐߜߍ߫ ߛߎߥߊ߲ߘߌߣߍ߲ ߠߎ߬ ߞߊ߲߬",
|
||||
"searchbox-tooltip": "ߕߌߙߌ߲ߠߌ߲ {{BOOK_TITLE}}"
|
||||
"searchbox-tooltip": "ߕߌߙߌ߲ߠߌ߲ {{BOOK_TITLE}}",
|
||||
"confusion-of-tongues": "ߞߊ߬ߝߊ߫ ߝߌ߬ߟߊ߬ ߥߟߊ߫ ߦߙߌߞߊ ߞߊ߲߫ ߜߘߍ ߟߎ߬ ߘߐ߫߸ ߏ߬ ߟߎ߫ ߘߌߣߊ߬ ߕߘߍ߬ ߢߌߣߌ߲ߠߌ߲ ߘߐ߫߸ ߡߍ߲ ߠߎ߬ ߛߌ߫ ߞߣߐ߬ߝߟߌ ߟߊߘߏ߲߬ ߠߊ߫ ߞߐߝߟߌ ߘߐ߫.",
|
||||
"welcome-page-overzealous-filter": "ߞߐߝߟߌ߫ ߕߴߦߋ߲߬. ߊ߬ ߝߐ߫ ߌ ߦߴߊ߬ ߝߍ߬ ߞߊ߬ <a href=\"{{URL}}\">ߛߍ߲ߛߍ߲ߟߊ߲ ߘߐߛߌ߰ ߕߎ߲߯</a>؟",
|
||||
"search": "ߢߌߣߌ߲ߠߌ߲",
|
||||
"book-filtering-all-categories": "ߦߌߟߡߊ ߟߎ߬ ߓߍ߯",
|
||||
"book-filtering-all-languages": "ߞߊ߲ ߠߎ߬ ߓߍ߯",
|
||||
"count-of-matching-books": "ߞߊ߬ߝߊ {{COUNT}}(ߟߎ߫)",
|
||||
"download": "ߟߊ߬ߖߌ߰ߒ߬ߞߎ߲߬ߠߌ߲",
|
||||
"direct-download-link-text": "ߒߕߋߟߋ߲ߡߊ߬",
|
||||
"direct-download-alt-text": "ߟߊ߬ߖߌ߰ߒ߬ߞߎ߲߬ߠߌ߲߫ ߝߊ߲ߞߢߊ",
|
||||
"hash-download-alt-text": "ߤߊߛ߭ ߟߊߖߌ߰ ߌ ߞߎ߲߬",
|
||||
"magnet-link-text": "ߡߊߢߍߕ ߛߘߌ߬ߜߋ߲",
|
||||
"magnet-alt-text": "ߡߊߢߍߕ ߟߊߖߌ߰ ߌ ߞߎ߲߬",
|
||||
"torrent-download-link-text": "ߕߏߙߍ߲ߕ ߞߐߕߐ߮",
|
||||
"torrent-download-alt-text": "ߕߏߙߍ߲ߕ ߟߊߖߌ߰ ߌ ߞߎ߲߬",
|
||||
"welcome-to-kiwix-server": "ߌ ߣߌ߫ ߛߣߍ߫ ߞߥߌߞߛ ߡߊߛߐߟߊ߲ ߞߣߐ߫",
|
||||
"download-links-heading": "<b><i>{{BOOK_TITLE}}</i></b> ߛߘߌ߬ߜߋ߲ ߠߊߖߌ߰ ߌ ߞߎ߲߬",
|
||||
"download-links-title": "ߞߊ߬ߝߊ ߟߊߖߌ߰ ߌ ߞߎ߲߬",
|
||||
"preview-book": "ߊ߬ ߘߐߜߍ߫ ߡߎߣߎ߲߬"
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Strebski"
|
||||
"Strebski",
|
||||
"WaldiSt"
|
||||
]
|
||||
},
|
||||
"name": "Polski",
|
||||
@@ -23,5 +24,8 @@
|
||||
"library-button-text": "Przejdź do strony powitalnej",
|
||||
"home-button-text": "Przejdź do głównej strony '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Przejdź do losowo wybranej strony",
|
||||
"searchbox-tooltip": "Szukaj '{{BOOK_TITLE}}'"
|
||||
"searchbox-tooltip": "Szukaj '{{BOOK_TITLE}}'",
|
||||
"welcome-to-kiwix-server": "Witamy na serwerze Kiwix",
|
||||
"download-links-title": "Pobierz książkę",
|
||||
"preview-book": "Podgląd"
|
||||
}
|
||||
@@ -29,5 +29,28 @@
|
||||
"library-button-text": "Tooltip of the button leading to the welcome page",
|
||||
"home-button-text": "Tooltip of the button leading to the main page of a book",
|
||||
"random-page-button-text": "Tooltip of the button opening a randomly selected page",
|
||||
"searchbox-tooltip": "Tooltip displayed for the search box"
|
||||
"searchbox-tooltip": "Tooltip displayed for the search box",
|
||||
"welcome-page-overzealous-filter": "Text shown when book filtering on the welcome page produces zero results",
|
||||
"powered-by-kiwix-html": "Link to Kiwix website",
|
||||
"search": "A general search action (text displayed on search buttons or as aplaceholder in searchboxes)",
|
||||
"book-filtering-all-categories": "Choosing this filter will disable filtering of books by category",
|
||||
"book-filtering-all-languages": "Choosing this filter will disable filtering of books by language",
|
||||
"count-of-matching-books": "Reporting the count of books matching the filter",
|
||||
"download": "A general download action",
|
||||
"direct-download-link-text": "Link text for a direct download",
|
||||
"direct-download-alt-text": "Hint for a direct download icon",
|
||||
"hash-download-link-text": "Link text for downloading the hash",
|
||||
"hash-download-alt-text": "Hint for the icon of hash download",
|
||||
"magnet-link-text": "Link text for a magnet link",
|
||||
"magnet-alt-text": "Hint for the icon of a magnet link",
|
||||
"torrent-download-link-text": "Link text for downloading the torrent file",
|
||||
"torrent-download-alt-text": "Hint for the icon of torrent download",
|
||||
"library-opds-feed-all-entries": "Hint for the library OPDS feed for all entries",
|
||||
"filter-by-tag": "Hint for a link that would load results filtered by a single tag",
|
||||
"stop-filtering-by-tag": "Tooltip for the button that cancels filtering by tag",
|
||||
"library-opds-feed-parameterised": "Hint for the library OPDS feed for filtered entries",
|
||||
"welcome-to-kiwix-server": "Title shown in browser's title bar/page tab",
|
||||
"download-links-heading": "Heading for no-js download page",
|
||||
"download-links-title": "Title for no-js download page",
|
||||
"preview-book": "Tooltip of book-tile leading to the book"
|
||||
}
|
||||
@@ -31,5 +31,8 @@
|
||||
"home-button-text": "Перейти на главную страницу '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Перейти на случайно выбранную страницу",
|
||||
"searchbox-tooltip": "Искать '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "В поиске будут участвовать две или более книг на разных языках, что может привести к запутанным результатам."
|
||||
"confusion-of-tongues": "В поиске будут участвовать две или более книг на разных языках, что может привести к запутанным результатам.",
|
||||
"book-filtering-all-categories": "Все категории",
|
||||
"book-filtering-all-languages": "Все языки",
|
||||
"download": "Скачать"
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Kelson",
|
||||
"L2212"
|
||||
]
|
||||
},
|
||||
@@ -28,5 +29,28 @@
|
||||
"home-button-text": "Bae a sa pàgina printzipale de '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Bae a una pàgina seletzionada a manera casuale",
|
||||
"searchbox-tooltip": "Chirca '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "Duos o prus libros in limbas diferentes diant pigare parte a sa chirca, cosa chi diat pòdere causare resurtados confusionosos."
|
||||
"confusion-of-tongues": "Duos o prus libros in limbas diferentes diant pigare parte a sa chirca, cosa chi diat pòdere causare resurtados confusionosos.",
|
||||
"welcome-page-overzealous-filter": "Perunu resurtadu. Boles <a href=\"{{URL}}\">resetare su filtru</a>?",
|
||||
"powered-by-kiwix-html": "Alimentadu dae <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Chirca",
|
||||
"book-filtering-all-categories": "Totu sas categorias",
|
||||
"book-filtering-all-languages": "Totu sas limbas",
|
||||
"count-of-matching-books": "{{COUNT}} libru/os",
|
||||
"download": "Iscàrriga",
|
||||
"direct-download-link-text": "Diretu",
|
||||
"direct-download-alt-text": "iscarrigamentu diretu",
|
||||
"hash-download-link-text": "Hash SHA256",
|
||||
"hash-download-alt-text": "hash de s'iscarrigamentu",
|
||||
"magnet-link-text": "Ligàmene Magnet",
|
||||
"magnet-alt-text": "ligàmene \"magnet\" de iscarrigamentu",
|
||||
"torrent-download-link-text": "Documentu Torrent",
|
||||
"torrent-download-alt-text": "iscàrriga su torrent",
|
||||
"library-opds-feed-all-entries": "Flussu OPDS de sa biblioteca – Totu sos elementos",
|
||||
"filter-by-tag": "Filtra pro eticheta \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Non filtres prus pro eticheta \"{{TAG}}\"",
|
||||
"library-opds-feed-parameterised": "Flussu OPDS de sa biblioteca - elementos chi currispondet cun {{#LANG}}\nLimba: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategoria: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nEticheta: {{TAG}} {{/TAG}}{{#Q}}\nChirca: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Bene bènnidu a su serbidore de Kiwix",
|
||||
"download-links-heading": "Ligàmenes de iscarrigamentu pro <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Iscàrriga su libru",
|
||||
"preview-book": "Antiprima"
|
||||
}
|
||||
26
static/skin/i18n/skr-arab.json
Normal file
26
static/skin/i18n/skr-arab.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Saraiki"
|
||||
]
|
||||
},
|
||||
"name": "سرائیکی",
|
||||
"400-page-title": "غلط ارداس",
|
||||
"400-page-heading": "غلط ارداس",
|
||||
"404-page-title": "مواد کائنی لبھیا",
|
||||
"404-page-heading": "کائنی لبھا",
|
||||
"500-page-title": "اندرلا سرور نقص",
|
||||
"500-page-heading": "اندرلا سرور نقص",
|
||||
"search": "ڳولو",
|
||||
"book-filtering-all-categories": "ساریاں ونکیاں",
|
||||
"book-filtering-all-languages": "ساریاں زباناں",
|
||||
"count-of-matching-books": "{{COUNT}} کتاب(اں)",
|
||||
"download": "ڈاؤن لوڈ",
|
||||
"direct-download-link-text": "ڈائرکٹ",
|
||||
"direct-download-alt-text": "ڈائرکٹ ڈاؤن لوڈ",
|
||||
"hash-download-alt-text": "ڈاؤن لوڈ ہیش",
|
||||
"torrent-download-link-text": "ٹورنٹ فائل",
|
||||
"torrent-download-alt-text": "ٹورںٹ ݙاؤن لوڈ کرو",
|
||||
"download-links-title": "کتاب ڈاؤن لوڈ کرو",
|
||||
"preview-book": "پیشگی ݙکھالا"
|
||||
}
|
||||
56
static/skin/i18n/sl.json
Normal file
56
static/skin/i18n/sl.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Eleassar",
|
||||
"Kelson"
|
||||
]
|
||||
},
|
||||
"name": "slovenščina",
|
||||
"suggest-full-text-search": "vsebuje »{{{SEARCH_TERMS}}}« ...",
|
||||
"no-such-book": "Ni take knjige: {{BOOK_NAME}}",
|
||||
"too-many-books": "Preveč zahtevanih knjig ({{NB_BOOKS}}), omejitev je {{LIMIT}}",
|
||||
"no-book-found": "Izbirnim merilom ne ustreza nobena knjiga",
|
||||
"url-not-found": "Zahtevanega URL-ja »{{url}}« v tem strežniku ni bilo mogoče najti.",
|
||||
"suggest-search": "Preiščite celotno besedilo za <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ups! Ni bilo mogoče izbrati naključnega članka :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} ni veljaven zahtevek za neobdelano vsebino.",
|
||||
"no-value-for-arg": "Argument {{ARGUMENT}} nima določene nobene vrednosti",
|
||||
"no-query": "Poizvedba ni podana.",
|
||||
"raw-entry-not-found": "Ni mogoče najti vnosa {{ENTRY}} tipa {{DATATYPE}}",
|
||||
"400-page-title": "Neveljaven zahtevek",
|
||||
"400-page-heading": "Neveljaven zahtevek",
|
||||
"404-page-title": "Vsebine ni mogoče najti",
|
||||
"404-page-heading": "Ni najdeno",
|
||||
"500-page-title": "Notranja napaka strežnika",
|
||||
"500-page-heading": "Notranja napaka strežnika",
|
||||
"fulltext-search-unavailable": "Iskanje po celotnem besedilu ni na voljo",
|
||||
"no-search-results": "Iskalnik po celotnem besedilu za to vsebino ni na voljo.",
|
||||
"library-button-text": "Pojdite na pozdravno stran",
|
||||
"home-button-text": "Pojdite na glavno stran »{{BOOK_TITLE}}«",
|
||||
"random-page-button-text": "Pojdite na naključno izbrano stran",
|
||||
"searchbox-tooltip": "Poiščite »{{BOOK_TITLE}}«",
|
||||
"confusion-of-tongues": "V iskanju bi bili uporabljeni dve ali več knjig v različnih jezikih, kar lahko pripelje do nejasnih zadetkov.",
|
||||
"welcome-page-overzealous-filter": "Ni zadetkov. Želite <a href=\"{{URL}}\">ponastaviti filter</a>?",
|
||||
"powered-by-kiwix-html": "Omogoča <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Išči",
|
||||
"book-filtering-all-categories": "Vse kategorije",
|
||||
"book-filtering-all-languages": "Vsi jeziki",
|
||||
"count-of-matching-books": "{{COUNT}} knjiga(i/e)",
|
||||
"download": "Prenesi",
|
||||
"direct-download-link-text": "Neposredno",
|
||||
"direct-download-alt-text": "neposredni prenos",
|
||||
"hash-download-link-text": "Zgoščena vrednost SHA256",
|
||||
"hash-download-alt-text": "prenesi zgoščeno vrednost",
|
||||
"magnet-link-text": "Magnetna povezava",
|
||||
"magnet-alt-text": "prenesi magnet",
|
||||
"torrent-download-link-text": "Torrent datoteka",
|
||||
"torrent-download-alt-text": "prenesi torrent",
|
||||
"library-opds-feed-all-entries": "Knjižnični vir OPDS – Vsi vnosi",
|
||||
"filter-by-tag": "Filtriraj po oznaki »{{TAG}}«",
|
||||
"stop-filtering-by-tag": "Ustavi filtriranje po oznaki »{{TAG}}«",
|
||||
"library-opds-feed-parameterised": "Knjižnični vir OPDS – vnosi, ki se ujemajo z {{#LANG}}\nJezik: {{LANG}} {{/LANG}}{{#CATEGORY}}\nKategorija: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nOznaka: {{TAG}} {{/TAG}}{{#Q}}\nPoizvedba: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Pozdravljeni na strežniku Kiwix",
|
||||
"download-links-heading": "Povezave za prenos za <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Prenesi knjigo",
|
||||
"preview-book": "Predogled"
|
||||
}
|
||||
@@ -30,5 +30,28 @@
|
||||
"home-button-text": "Gå till huvudsidan för \"{{BOOK_TITLE}}\"",
|
||||
"random-page-button-text": "Gå till en slumpmässigt utvald sida",
|
||||
"searchbox-tooltip": "Sök efter \"{{BOOK_TITLE}}\"",
|
||||
"confusion-of-tongues": "Två eller fler böcker på olika språk skulle delta i sökningen, vilket kan ge förvirrande resultat."
|
||||
"confusion-of-tongues": "Två eller fler böcker på olika språk skulle delta i sökningen, vilket kan ge förvirrande resultat.",
|
||||
"welcome-page-overzealous-filter": "Inga resultat. Vill du <a href=\"{{URL}}\">återställa filtret</a>?",
|
||||
"powered-by-kiwix-html": "Drivs av <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Sök",
|
||||
"book-filtering-all-categories": "Alla kategorier",
|
||||
"book-filtering-all-languages": "Alla språk",
|
||||
"count-of-matching-books": "{{COUNT}} böcker",
|
||||
"download": "Ladda ned",
|
||||
"direct-download-link-text": "Direkt",
|
||||
"direct-download-alt-text": "direktnedladdning",
|
||||
"hash-download-link-text": "Sha256-hash",
|
||||
"hash-download-alt-text": "ladda ned hash",
|
||||
"magnet-link-text": "Magnetlänk",
|
||||
"magnet-alt-text": "ladda ned magnet",
|
||||
"torrent-download-link-text": "Torrent-fil",
|
||||
"torrent-download-alt-text": "ladda ned torrent",
|
||||
"library-opds-feed-all-entries": "Library OPDS Feed - Alla poster",
|
||||
"filter-by-tag": "Filtrera efter taggen \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Sluta filtrera efter taggen \"{{TAG}}\"",
|
||||
"library-opds-feed-parameterised": "Library OPDS Feed - poster som matchar {{#LANG}}\nSpråk: {{LANG}} {{/LANG}}{{#CATEGORY}}\nKategori: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTagg: {{TAG}} {{/TAG}}{{#Q}}\nFråga: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Välkommen till Kiwix Server",
|
||||
"download-links-heading": "Nedladdningslänkar för <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Ladda ned bok",
|
||||
"preview-book": "Förhandsgranska"
|
||||
}
|
||||
43
static/skin/i18n/test.json
Normal file
43
static/skin/i18n/test.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Kareyac"
|
||||
]
|
||||
},
|
||||
"name": "Fake language for i18n testing"
|
||||
, "suggest-full-text-search": "[I18N TESTING] cOnTaInInG '{{{SEARCH_TERMS}}}'..."
|
||||
, "no-such-book": "[I18N TESTING] No such book: {{BOOK_NAME}}. Sorry."
|
||||
, "url-not-found": "[I18N TESTING] URL not found: {{url}}"
|
||||
, "suggest-search": "[I18N TESTING] Make a full text search for <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>"
|
||||
, "400-page-title": "[I18N TESTING] Invalid request ($400 fine must be paid)"
|
||||
, "400-page-heading": "[I18N TESTING] -400 karma for an invalid request"
|
||||
, "404-page-title": "[I18N TESTING] Not Found - Try Again"
|
||||
, "404-page-heading": "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
, "library-button-text": "[I18N TESTING] Navigate to the welcome page"
|
||||
, "home-button-text": "[I18N TESTING] Jump to the main page of '{{BOOK_TITLE}}'"
|
||||
, "random-page-button-text": "[I18N TESTING] I am tired of determinism"
|
||||
, "searchbox-tooltip": "[I18N TESTING] Let's search in '{{BOOK_TITLE}}'"
|
||||
, "welcome-page-overzealous-filter": "[I18N TESTING] Nothing found. <a href=\"{{URL}}\">Reset filter</a>"
|
||||
, "powered-by-kiwix-html": "[I18N TESTING] Powered by <a href=\"https://kiwix.org\">Kiwix</a> (nominal power: 1.23 kW)"
|
||||
, "search": "[I18N Search TESTING]"
|
||||
, "book-filtering-all-categories": "All [I18N TESTING] categories"
|
||||
, "book-filtering-all-languages": "All [I18N TESTING] languages"
|
||||
, "count-of-matching-books": "[I18N TESTING] Number of matching books: {{COUNT}}"
|
||||
, "download": "[I18N Download TESTING]"
|
||||
, "direct-download-link-text": "[I18N TESTING] HTTP(S)"
|
||||
, "direct-download-alt-text": "[I18N TESTING] download directly"
|
||||
, "hash-download-link-text": "Sha256 [I18N TESTING] hash"
|
||||
, "hash-download-alt-text": "download [I18N TESTING] hash"
|
||||
, "magnet-link-text": "Magnet [I18N TESTING] link"
|
||||
, "magnet-alt-text": "download [I18N TESTING] magnet"
|
||||
, "torrent-download-link-text": "Torrent [I18N TESTING] file"
|
||||
, "torrent-download-alt-text": "download [I18N TESTING] torrent"
|
||||
, "library-opds-feed-all-entries": "[I18N] Library [TESTING] OPDS Feed - All entries [I18N TESTING]"
|
||||
, "filter-by-tag": "Filter [I18N] by [TESTING] tag \"{{TAG}}\""
|
||||
, "stop-filtering-by-tag": "[I18N] Stop filtering [TESTING] by tag \"{{TAG}}\""
|
||||
, "library-opds-feed-parameterised": "[I18N] Library OPDS Feed - [TESTING] entries matching {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}"
|
||||
, "welcome-to-kiwix-server": "[I18N] Welcome to Kiwix Server [TESTING]"
|
||||
, "download-links-heading": "[I18N] Download links for <b><i>{{BOOK_TITLE}}</i></b> [TESTING]"
|
||||
, "download-links-title": "[I18N TESTING]Download book"
|
||||
, "preview-book": "[I18N] Preview [TESTING]"
|
||||
}
|
||||
57
static/skin/i18n/zh-hant.json
Normal file
57
static/skin/i18n/zh-hant.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Kelson",
|
||||
"Kly",
|
||||
"Winston Sung"
|
||||
]
|
||||
},
|
||||
"name": "繁體中文",
|
||||
"suggest-full-text-search": "正在包含「{{{SEARCH_TERMS}}}」…",
|
||||
"no-such-book": "沒有這樣的書籍:{{BOOK_NAME}}",
|
||||
"too-many-books": "請求太多個書籍({{NB_BOOKS}}),上限是 {{LIMIT}} 個",
|
||||
"no-book-found": "沒有書籍符合選擇標準",
|
||||
"url-not-found": "在此伺服器上找不到請求的 URL「{{url}}」。",
|
||||
"suggest-search": "建立 <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> 使用的全文搜尋",
|
||||
"random-article-failure": "哎呀!隨機挑選條目失敗 :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}}不是原始內容的有效請求。",
|
||||
"no-value-for-arg": "沒有為引數 {{ARGUMENT}} 提供內容",
|
||||
"no-query": "未提供查詢。",
|
||||
"raw-entry-not-found": "找不到{{DATATYPE}}項目{{ENTRY}}",
|
||||
"400-page-title": "無效請求",
|
||||
"400-page-heading": "無效請求",
|
||||
"404-page-title": "查無內容",
|
||||
"404-page-heading": "查無頁面",
|
||||
"500-page-title": "內部伺服器錯誤",
|
||||
"500-page-heading": "內部伺服器錯誤",
|
||||
"fulltext-search-unavailable": "全文搜尋無效",
|
||||
"no-search-results": "全文搜尋引擎不適用此內容。",
|
||||
"library-button-text": "前往歡迎首頁",
|
||||
"home-button-text": "前往「{{BOOK_TITLE}}」的首頁",
|
||||
"random-page-button-text": "前往隨機選取頁面",
|
||||
"searchbox-tooltip": "在{{BOOK_TITLE}}搜尋",
|
||||
"confusion-of-tongues": "搜索裡有加入兩本或更多不同語言的書籍,這可能會導致混淆結果。",
|
||||
"welcome-page-overzealous-filter": "沒有結果。您想要<a href=\"{{URL}}\">重新設定篩選</a>嗎?",
|
||||
"powered-by-kiwix-html": "由 <a href=\"https://kiwix.org\">Kiwix</a> 提供技術支援",
|
||||
"search": "搜尋",
|
||||
"book-filtering-all-categories": "所有分類",
|
||||
"book-filtering-all-languages": "所有語言",
|
||||
"count-of-matching-books": "{{COUNT}} 本書籍",
|
||||
"download": "下載",
|
||||
"direct-download-link-text": "直接",
|
||||
"direct-download-alt-text": "直接下載",
|
||||
"hash-download-link-text": "Sha256 雜湊",
|
||||
"hash-download-alt-text": "下載雜湊",
|
||||
"magnet-link-text": "Magnet 連結",
|
||||
"magnet-alt-text": "下載 magnet",
|
||||
"torrent-download-link-text": "Torrent 檔案",
|
||||
"torrent-download-alt-text": "下載 torrent",
|
||||
"library-opds-feed-all-entries": "圖書館 OPDS 摘要 - 所有項目",
|
||||
"filter-by-tag": "依標籤「{{TAG}}」篩選",
|
||||
"stop-filtering-by-tag": "停止依標籤「{{TAG}}」篩選",
|
||||
"library-opds-feed-parameterised": "圖書館 OPDS 摘要 - 項目符合 {{#LANG}}\n語言:{{LANG}} {{/LANG}}{{#CATEGORY}}\n分類:{{CATEGORY}} {{/CATEGORY}}{{#TAG}}\n標籤:{{TAG}} {{/TAG}}{{#Q}}\n查詢:{{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "歡迎來到 Kiwix 伺服器",
|
||||
"download-links-heading": "下載<b><i>{{BOOK_TITLE}}</i></b>的連結",
|
||||
"download-links-title": "下載書籍",
|
||||
"preview-book": "預覽"
|
||||
}
|
||||
@@ -24,6 +24,10 @@ body {
|
||||
background-color: #f4f6f8;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.kiwixHomeBody__results {
|
||||
@@ -134,6 +138,7 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tagFilterLabel {
|
||||
@@ -157,6 +162,29 @@ body {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
#uiLanguageSelector {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#uiLanguageSelector .modal {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
#uiLanguageSelector .modal-heading {
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
#uiLanguageSelector .modal-content #ui_language {
|
||||
font-size: 1.6rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#uiLanguageSelectorButton {
|
||||
margin: 0 12px 0 0;
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.book__list {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
@@ -441,6 +469,11 @@ body {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.feedLogo, #uiLanguageSelectorButton {
|
||||
height: 30px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1100px) {
|
||||
|
||||
.kiwixHomeBody {
|
||||
@@ -458,18 +491,19 @@ body {
|
||||
|
||||
@media screen and (max-width: 590px) {
|
||||
|
||||
.kiwixNav {
|
||||
height: 285px;
|
||||
.kiwixNav__SearchForm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.kiwixHomeBody {
|
||||
min-height: calc(100vh - 287px);
|
||||
}
|
||||
|
||||
|
||||
.kiwixSearch {
|
||||
margin-top: 11px;
|
||||
}
|
||||
|
||||
|
||||
.kiwixButton {
|
||||
margin: 15px 0;
|
||||
width: 229px;
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
(function() {
|
||||
class FragmentParams extends URLSearchParams {
|
||||
constructor(fragment = '') {
|
||||
if (fragment[0] == '#')
|
||||
fragment = fragment.substring(1);
|
||||
super(fragment);
|
||||
}
|
||||
}
|
||||
|
||||
const root = document.querySelector(`link[type='root']`).getAttribute('href');
|
||||
const incrementalLoadingParams = {
|
||||
start: 0,
|
||||
@@ -14,21 +22,49 @@
|
||||
let isFetching = false;
|
||||
let noResultInjected = false;
|
||||
let filters = getCookie(filterCookieName);
|
||||
let params = new URLSearchParams(window.location.search || filters || '');
|
||||
let params = new FragmentParams(window.location.hash || filters || '');
|
||||
params.delete('userlang');
|
||||
let timer;
|
||||
let languages = {};
|
||||
let previousScrollTop = Infinity;
|
||||
|
||||
function updateFeedLink() {
|
||||
const inputParams = new FragmentParams(window.location.hash);
|
||||
const filteredParams = new FragmentParams();
|
||||
for (const [key, value] of inputParams) {
|
||||
if ( value != '' ) {
|
||||
filteredParams.set(key, value);
|
||||
}
|
||||
}
|
||||
const feedLink = `${root}/catalog/v2/entries?${filteredParams.toString()}`;
|
||||
document.querySelector('#headFeedLink').href = feedLink;
|
||||
document.querySelector('#feedLink').href = feedLink;
|
||||
setFeedToolTip();
|
||||
}
|
||||
|
||||
function changeUILanguage() {
|
||||
window.modalUILanguageSelector.close();
|
||||
const s = document.getElementById("ui_language");
|
||||
const lang = s.options[s.selectedIndex].value;
|
||||
setPermanentGlobalCookie('userlang', lang);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function queryUrlBuilder() {
|
||||
let url = `${root}/catalog/search?`;
|
||||
let url = `${root}/catalog/v2/entries?`;
|
||||
url += Object.keys(incrementalLoadingParams).map(key => `${key}=${incrementalLoadingParams[key]}`).join("&");
|
||||
params.forEach((value, key) => {url+= value ? `&${key}=${value}` : ''});
|
||||
return (url);
|
||||
}
|
||||
|
||||
function setCookie(cookieName, cookieValue) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + oneDayDelta);
|
||||
document.cookie = `${cookieName}=${cookieValue};expires=${date.toUTCString()};sameSite=Strict`;
|
||||
function setCookie(cookieName, cookieValue, ttl) {
|
||||
let exp = "";
|
||||
if ( ttl ) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + ttl);
|
||||
exp = `expires=${date.toUTCString()};`;
|
||||
}
|
||||
document.cookie = `${cookieName}=${cookieValue};${exp}sameSite=Strict`;
|
||||
}
|
||||
|
||||
function getCookie(cookieName) {
|
||||
@@ -80,7 +116,7 @@
|
||||
function generateTagLink(tagValue) {
|
||||
tagValue = tagValue.toLowerCase();
|
||||
const humanFriendlyTagValue = humanFriendlyTitle(tagValue);
|
||||
const tagMessage = `Filter by tag "${humanFriendlyTagValue}"`;
|
||||
const tagMessage = $t("filter-by-tag", {TAG: humanFriendlyTagValue});
|
||||
return `<span class='tag__link' aria-label='${tagMessage}' title='${tagMessage}' data-tag=${tagValue}>${humanFriendlyTagValue}</span>`
|
||||
}
|
||||
|
||||
@@ -95,8 +131,13 @@
|
||||
const title = getInnerHtml(book, 'title');
|
||||
const description = getInnerHtml(book, 'summary');
|
||||
const id = getInnerHtml(book, 'id');
|
||||
const langCode = getInnerHtml(book, 'language');
|
||||
const language = languages[langCode];
|
||||
const langCodesList = getInnerHtml(book, 'language').split(',');
|
||||
const langCode = langCodesList.length == 1 ? langCodesList[0] : 'mul';
|
||||
let language = languages[langCode];
|
||||
if (langCode == 'mul') {
|
||||
const mulLangList = langCodesList.filter(x => languages.hasOwnProperty(x)).map(x => languages[x]);
|
||||
language = mulLangList.join(', ');
|
||||
}
|
||||
const tags = getInnerHtml(book, 'tags');
|
||||
const tagList = tags.split(';').filter(tag => {return !(tag.startsWith('_'))});
|
||||
const tagFilterLinks = tagList.map((tagValue) => generateTagLink(tagValue));
|
||||
@@ -130,7 +171,7 @@
|
||||
<div class="book__icon" ${faviconAttr}></div>
|
||||
<div class="book__header">
|
||||
<div id="book__title">${title}</div>
|
||||
${downloadLink ? `<div class="book__download"><span data-link="${downloadLink}">Download ${humanFriendlyZimSize ? ` - ${humanFriendlyZimSize}</span></div>`: ''}` : ''}
|
||||
${downloadLink ? `<div class="book__download"><span data-link="${downloadLink}">${$t("download")} ${humanFriendlyZimSize ? ` - ${humanFriendlyZimSize}</span></div>`: ''}` : ''}
|
||||
</div>
|
||||
<div class="book__description" title="${description}">${description}</div>
|
||||
</div>
|
||||
@@ -196,27 +237,27 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-regular-download">
|
||||
<a href="${downloadLink}" download>
|
||||
<img src="../skin/download.png?KIWIXCACHEID" alt="direct download" />
|
||||
<div>Direct</div>
|
||||
<img src="${root}/skin/download.png?KIWIXCACHEID" alt="${$t("direct-download-alt-text")}" />
|
||||
<div>${$t("direct-download-link-text")}</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="modal-regular-download">
|
||||
<a href="${downloadLink}.sha256" download>
|
||||
<img src="../skin/hash.png?KIWIXCACHEID" alt="download hash" />
|
||||
<div>Sha256 hash</div>
|
||||
<img src="${root}/skin/hash.png?KIWIXCACHEID" alt="${$t("hash-download-alt-text")}" />
|
||||
<div>${$t("hash-download-link-text")}</div>
|
||||
</a>
|
||||
</div>
|
||||
${magnetLink ?
|
||||
`<div class="modal-regular-download">
|
||||
<a href="${magnetLink}" target="_blank">
|
||||
<img src="../skin/magnet.png?KIWIXCACHEID" alt="download magnet" />
|
||||
<div>Magnet link</div>
|
||||
<img src="${root}/skin/magnet.png?KIWIXCACHEID" alt="${$t("magnet-alt-text")}" />
|
||||
<div>${$t("magnet-link-text")}</div>
|
||||
</a>
|
||||
</div>` : ``}
|
||||
<div class="modal-regular-download">
|
||||
<a href="${downloadLink}.torrent" download>
|
||||
<img src="../skin/bittorrent.png?KIWIXCACHEID" alt="download torrent" />
|
||||
<div>Torrent file</div>
|
||||
<img src="${root}/skin/bittorrent.png?KIWIXCACHEID" alt="${$t("torrent-download-alt-text")}" />
|
||||
<div>${$t("torrent-download-link-text")}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -226,7 +267,7 @@
|
||||
}
|
||||
|
||||
async function getBookCount(query) {
|
||||
const url = `${root}/catalog/search?${query}`;
|
||||
const url = `${root}/catalog/v2/entries?${query}&count=0`;
|
||||
return await fetch(url).then(async (resp) => {
|
||||
const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml');
|
||||
return parseInt(data.querySelector('totalResults').innerHTML);
|
||||
@@ -249,16 +290,10 @@
|
||||
} else {
|
||||
toggleFooter();
|
||||
}
|
||||
const kiwixResultText = document.querySelector('.kiwixHomeBody__results')
|
||||
if (results) {
|
||||
let resultText = `${results} books`;
|
||||
if (results === 1) {
|
||||
resultText = `${results} book`;
|
||||
}
|
||||
kiwixResultText.innerHTML = resultText;
|
||||
} else {
|
||||
kiwixResultText.innerHTML = ``;
|
||||
}
|
||||
const text = results
|
||||
? $t("count-of-matching-books", {COUNT: results})
|
||||
: '';
|
||||
document.querySelector('.kiwixHomeBody__results').innerHTML = text;
|
||||
loader.style.display = 'none';
|
||||
return books;
|
||||
});
|
||||
@@ -285,7 +320,7 @@
|
||||
const kiwixHomeBody = document.querySelector('.kiwixHomeBody');
|
||||
const divTag = document.createElement('div');
|
||||
divTag.setAttribute('class', 'noResults');
|
||||
divTag.innerHTML = `No result. Would you like to <a href="?lang=">reset filter</a>?`;
|
||||
divTag.innerHTML = $t("welcome-page-overzealous-filter", {URL: '#lang='});
|
||||
kiwixHomeBody.append(divTag);
|
||||
kiwixHomeBody.setAttribute('style', 'display: flex; justify-content: center; align-items: center');
|
||||
loader.setAttribute('style', 'position: absolute; top: 50%');
|
||||
@@ -356,13 +391,14 @@
|
||||
incrementalLoadingParams.count = viewPortToCount();
|
||||
fadeOutDiv.style.display = 'none';
|
||||
bookOrderMap.clear();
|
||||
params = new URLSearchParams(window.location.search);
|
||||
params = new FragmentParams(window.location.hash);
|
||||
if (filterType) {
|
||||
params.set(filterType, filterValue);
|
||||
window.history.pushState({}, null, `?${params.toString()}`);
|
||||
setCookie(filterCookieName, params.toString());
|
||||
window.history.pushState({}, null, `#${params.toString()}`);
|
||||
setCookie(filterCookieName, params.toString(), oneDayDelta);
|
||||
}
|
||||
updateFilterColors();
|
||||
updateFeedLink();
|
||||
await loadAndDisplayBooks(true);
|
||||
}
|
||||
|
||||
@@ -397,7 +433,7 @@
|
||||
tagElement.style.display = 'inline-block';
|
||||
const humanFriendlyTagValue = humanFriendlyTitle(tagValue);
|
||||
tagElement.innerHTML = `${humanFriendlyTagValue}`;
|
||||
const tagMessage = `Stop filtering by tag "${humanFriendlyTagValue}"`;
|
||||
const tagMessage = $t("stop-filtering-by-tag", {TAG: humanFriendlyTagValue});
|
||||
tagElement.setAttribute('aria-label', tagMessage);
|
||||
tagElement.setAttribute('title', tagMessage);
|
||||
if (resetFilter)
|
||||
@@ -432,6 +468,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
function updateNavVisibilityState() {
|
||||
const st = window.scrollY;
|
||||
const enableAutoHiding = document.body.clientWidth < 590;
|
||||
if ((Math.abs(previousScrollTop - st) <= 5) || !enableAutoHiding)
|
||||
return;
|
||||
const kiwixNav = document.querySelector('.kiwixNav');
|
||||
if (st > previousScrollTop) {
|
||||
kiwixNav.style.position = 'fixed';
|
||||
kiwixNav.style.top = '-100%';
|
||||
} else {
|
||||
kiwixNav.style.position = 'sticky';
|
||||
kiwixNav.style.top = '0';
|
||||
}
|
||||
previousScrollTop = st;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', (event) => {
|
||||
if (timer) {clearTimeout(timer)}
|
||||
timer = setTimeout(() => {
|
||||
@@ -448,7 +500,42 @@
|
||||
}
|
||||
});
|
||||
|
||||
window.onload = async () => {
|
||||
window.addEventListener('hashchange', () => resetAndFilter());
|
||||
|
||||
function setFeedToolTip() {
|
||||
const feedLogoElem = document.getElementById('feedLogo');
|
||||
const libraryOpdsFeedHint = opdsFeedHintByParams();
|
||||
for (const attr of ["alt", "aria-label", "title"] ) {
|
||||
feedLogoElem.setAttribute(attr, libraryOpdsFeedHint);
|
||||
}
|
||||
}
|
||||
|
||||
function opdsFeedHintByParams() {
|
||||
const paramObj = {};
|
||||
const inputParams = new FragmentParams(window.location.hash);
|
||||
for (const [key, value] of inputParams) {
|
||||
if ( value != '' ) {
|
||||
paramObj[key.toUpperCase()] = value;
|
||||
}
|
||||
}
|
||||
if (!paramObj.LANG && !paramObj.CATEGORY && !paramObj.TAG && !paramObj.Q) {
|
||||
return $t('library-opds-feed-all-entries');
|
||||
}
|
||||
return $t('library-opds-feed-parameterised', paramObj);
|
||||
}
|
||||
|
||||
function updateUIText() {
|
||||
footer.innerHTML = $t("powered-by-kiwix-html");
|
||||
const searchText = $t("search");
|
||||
document.getElementById('searchFilter').placeholder = searchText;
|
||||
document.getElementById('searchButton').value = searchText;
|
||||
document.getElementById('categoryFilter').children[0].innerHTML = $t("book-filtering-all-categories");
|
||||
document.getElementById('languageFilter').children[0].innerHTML = $t("book-filtering-all-languages");
|
||||
setFeedToolTip();
|
||||
}
|
||||
|
||||
async function onload() {
|
||||
initUILanguageSelector(getUserLanguage(), changeUILanguage);
|
||||
iso = new Isotope( '.book__list', {
|
||||
itemSelector: '.book',
|
||||
getSortData:{
|
||||
@@ -464,6 +551,7 @@
|
||||
}
|
||||
});
|
||||
footer = document.getElementById('kiwixfooter');
|
||||
updateUIText();
|
||||
fadeOutDiv = document.getElementById('fadeOut');
|
||||
loader = document.querySelector('.loader');
|
||||
await loadAndDisplayOptions('#languageFilter', `${root}/catalog/v2/languages`, 'language');
|
||||
@@ -475,15 +563,15 @@
|
||||
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
|
||||
tagElement.addEventListener('click', () => removeTagElement(true));
|
||||
if (filters) {
|
||||
const currentLink = window.location.search;
|
||||
const newLink = `?${params.toString()}`;
|
||||
const currentLink = window.location.hash;
|
||||
const newLink = `#${params.toString()}`;
|
||||
if (currentLink != newLink) {
|
||||
window.history.pushState({}, null, newLink);
|
||||
}
|
||||
}
|
||||
updateVisibleParams();
|
||||
document.getElementById('kiwixSearchForm').onsubmit = (event) => {event.preventDefault()};
|
||||
if (!window.location.search) {
|
||||
if (!window.location.hash) {
|
||||
const browserLang = navigator.language.split('-')[0];
|
||||
const langFilter = document.getElementById('languageFilter');
|
||||
const lang = browserLang.length === 3 ? browserLang : iso6391To3[browserLang];
|
||||
@@ -492,7 +580,16 @@
|
||||
langFilter.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
setCookie(filterCookieName, params.toString());
|
||||
updateFeedLink();
|
||||
setCookie(filterCookieName, params.toString(), oneDayDelta);
|
||||
setInterval(updateNavVisibilityState, 250);
|
||||
};
|
||||
|
||||
// required by i18n.js:setUserLanguage()
|
||||
window.setPermanentGlobalCookie = function(name, value) {
|
||||
document.cookie = `${name}=${value};path=${root};max-age=31536000`;
|
||||
}
|
||||
|
||||
window.onload = () => { setUserLanguage(getUserLanguage(), onload); }
|
||||
})();
|
||||
|
||||
|
||||
25
static/skin/langSelector.svg
Normal file
25
static/skin/langSelector.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="uiLanguageSelectorButton" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#CCCCCC;}
|
||||
.st2{fill:#3366CC;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M2.9,29.6c-1.4,0-2.6-1.1-2.6-2.6V2.9c0-1.4,1.1-2.6,2.6-2.6h24.1c1.4,0,2.6,1.1,2.6,2.6v24.1
|
||||
c0,1.4-1.1,2.6-2.6,2.6H2.9z"/>
|
||||
<path class="st1" d="M27.1,0.6c1.3,0,2.3,1,2.3,2.3v24.1c0,1.3-1,2.3-2.3,2.3H2.9c-1.3,0-2.3-1-2.3-2.3V2.9c0-1.3,1-2.3,2.3-2.3
|
||||
H27.1 M27.1,0.1H2.9c-1.6,0-2.8,1.3-2.8,2.8v24.1c0,1.6,1.3,2.8,2.8,2.8h24.1c1.6,0,2.8-1.3,2.8-2.8V2.9
|
||||
C29.9,1.4,28.6,0.1,27.1,0.1L27.1,0.1z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st2" d="M26.4,24.1h-1.7c-0.2,0-0.3-0.1-0.5-0.2S24,23.7,24,23.6l-1.1-3h-5.7l-1.1,2.9c-0.1,0.2-0.2,0.2-0.2,0.3
|
||||
c-0.2,0.2-0.3,0.3-0.5,0.3h-1.6L19,11.1h2.1L26.4,24.1z M22.4,19.2l-1.8-4.8c-0.2-0.5-0.3-0.9-0.5-1.4c-0.1,0.3-0.2,0.5-0.2,0.8
|
||||
l-0.2,0.6l-1.8,4.8L22.4,19.2z M15.2,17.4c-1.1-0.4-2.3-0.9-3.3-1.6c1.6-1.7,2.7-3.8,3.2-6.2h2.2V8.2H12c-0.1-0.2-0.2-0.5-0.2-0.6
|
||||
c-0.3-0.8-0.6-1.7-0.6-1.7L9.5,6.5c0,0,0.5,1,0.7,1.7H3.6v1.5H6c0.5,2.3,1.6,4.4,3.3,6.2c-1.7,1.1-3.6,1.9-5.7,2.4
|
||||
c0.5,0.6,0.8,1.1,1,1.6c2.1-0.7,4.1-1.7,5.9-2.9c1.3,0.8,2.7,1.5,4,2.1L15.2,17.4z M7.7,9.7h5.6c-0.4,2-1.4,3.7-2.8,5.1
|
||||
C9.2,13.3,8.2,11.6,7.7,9.7z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
80
static/skin/languages.js
Normal file
80
static/skin/languages.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const uiLanguages = [
|
||||
{
|
||||
"الإنجليزية": "ar"
|
||||
},
|
||||
{
|
||||
"বাংলা": "bn"
|
||||
},
|
||||
{
|
||||
"Čeština": "cs"
|
||||
},
|
||||
{
|
||||
"Deutsch": "de"
|
||||
},
|
||||
{
|
||||
"English": "en"
|
||||
},
|
||||
{
|
||||
"suomi": "fi"
|
||||
},
|
||||
{
|
||||
"français": "fr"
|
||||
},
|
||||
{
|
||||
"עברית": "he"
|
||||
},
|
||||
{
|
||||
"Հայերեն": "hy"
|
||||
},
|
||||
{
|
||||
"interlingua": "ia"
|
||||
},
|
||||
{
|
||||
"italiano": "it"
|
||||
},
|
||||
{
|
||||
"日本語": "ja"
|
||||
},
|
||||
{
|
||||
"한국어": "ko"
|
||||
},
|
||||
{
|
||||
"kurdî": "ku-latn"
|
||||
},
|
||||
{
|
||||
"Lëtzebuergesch": "lb"
|
||||
},
|
||||
{
|
||||
"македонски": "mk"
|
||||
},
|
||||
{
|
||||
"ߒߞߏ": "nqo"
|
||||
},
|
||||
{
|
||||
"Polski": "pl"
|
||||
},
|
||||
{
|
||||
"русский": "ru"
|
||||
},
|
||||
{
|
||||
"Sardu": "sc"
|
||||
},
|
||||
{
|
||||
"slovenčina": "sk"
|
||||
},
|
||||
{
|
||||
"slovenščina": "sl"
|
||||
},
|
||||
{
|
||||
"Svenska": "sv"
|
||||
},
|
||||
{
|
||||
"Türkçe": "tr"
|
||||
},
|
||||
{
|
||||
"英语": "zh-hans"
|
||||
},
|
||||
{
|
||||
"繁體中文": "zh-hant"
|
||||
}
|
||||
]
|
||||
764
static/skin/mustache.js
Normal file
764
static/skin/mustache.js
Normal file
@@ -0,0 +1,764 @@
|
||||
/*!
|
||||
* mustache.js - Logic-less {{mustache}} templates with JavaScript
|
||||
* http://github.com/janl/mustache.js
|
||||
*/
|
||||
|
||||
var objectToString = Object.prototype.toString;
|
||||
var isArray = Array.isArray || function isArrayPolyfill (object) {
|
||||
return objectToString.call(object) === '[object Array]';
|
||||
};
|
||||
|
||||
function isFunction (object) {
|
||||
return typeof object === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* More correct typeof string handling array
|
||||
* which normally returns typeof 'object'
|
||||
*/
|
||||
function typeStr (obj) {
|
||||
return isArray(obj) ? 'array' : typeof obj;
|
||||
}
|
||||
|
||||
function escapeRegExp (string) {
|
||||
return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Null safe way of checking whether or not an object,
|
||||
* including its prototype, has a given property
|
||||
*/
|
||||
function hasProperty (obj, propName) {
|
||||
return obj != null && typeof obj === 'object' && (propName in obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe way of detecting whether or not the given thing is a primitive and
|
||||
* whether it has the given property
|
||||
*/
|
||||
function primitiveHasOwnProperty (primitive, propName) {
|
||||
return (
|
||||
primitive != null
|
||||
&& typeof primitive !== 'object'
|
||||
&& primitive.hasOwnProperty
|
||||
&& primitive.hasOwnProperty(propName)
|
||||
);
|
||||
}
|
||||
|
||||
// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
|
||||
// See https://github.com/janl/mustache.js/issues/189
|
||||
var regExpTest = RegExp.prototype.test;
|
||||
function testRegExp (re, string) {
|
||||
return regExpTest.call(re, string);
|
||||
}
|
||||
|
||||
var nonSpaceRe = /\S/;
|
||||
function isWhitespace (string) {
|
||||
return !testRegExp(nonSpaceRe, string);
|
||||
}
|
||||
|
||||
var entityMap = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
};
|
||||
|
||||
function escapeHtml (string) {
|
||||
return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
|
||||
return entityMap[s];
|
||||
});
|
||||
}
|
||||
|
||||
var whiteRe = /\s*/;
|
||||
var spaceRe = /\s+/;
|
||||
var equalsRe = /\s*=/;
|
||||
var curlyRe = /\s*\}/;
|
||||
var tagRe = /#|\^|\/|>|\{|&|=|!/;
|
||||
|
||||
/**
|
||||
* Breaks up the given `template` string into a tree of tokens. If the `tags`
|
||||
* argument is given here it must be an array with two string values: the
|
||||
* opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
|
||||
* course, the default is to use mustaches (i.e. mustache.tags).
|
||||
*
|
||||
* A token is an array with at least 4 elements. The first element is the
|
||||
* mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
|
||||
* did not contain a symbol (i.e. {{myValue}}) this element is "name". For
|
||||
* all text that appears outside a symbol this element is "text".
|
||||
*
|
||||
* The second element of a token is its "value". For mustache tags this is
|
||||
* whatever else was inside the tag besides the opening symbol. For text tokens
|
||||
* this is the text itself.
|
||||
*
|
||||
* The third and fourth elements of the token are the start and end indices,
|
||||
* respectively, of the token in the original template.
|
||||
*
|
||||
* Tokens that are the root node of a subtree contain two more elements: 1) an
|
||||
* array of tokens in the subtree and 2) the index in the original template at
|
||||
* which the closing tag for that section begins.
|
||||
*
|
||||
* Tokens for partials also contain two more elements: 1) a string value of
|
||||
* indendation prior to that tag and 2) the index of that tag on that line -
|
||||
* eg a value of 2 indicates the partial is the third tag on this line.
|
||||
*/
|
||||
function parseTemplate (template, tags) {
|
||||
if (!template)
|
||||
return [];
|
||||
var lineHasNonSpace = false;
|
||||
var sections = []; // Stack to hold section tokens
|
||||
var tokens = []; // Buffer to hold the tokens
|
||||
var spaces = []; // Indices of whitespace tokens on the current line
|
||||
var hasTag = false; // Is there a {{tag}} on the current line?
|
||||
var nonSpace = false; // Is there a non-space char on the current line?
|
||||
var indentation = ''; // Tracks indentation for tags that use it
|
||||
var tagIndex = 0; // Stores a count of number of tags encountered on a line
|
||||
|
||||
// Strips all whitespace tokens array for the current line
|
||||
// if there was a {{#tag}} on it and otherwise only space.
|
||||
function stripSpace () {
|
||||
if (hasTag && !nonSpace) {
|
||||
while (spaces.length)
|
||||
delete tokens[spaces.pop()];
|
||||
} else {
|
||||
spaces = [];
|
||||
}
|
||||
|
||||
hasTag = false;
|
||||
nonSpace = false;
|
||||
}
|
||||
|
||||
var openingTagRe, closingTagRe, closingCurlyRe;
|
||||
function compileTags (tagsToCompile) {
|
||||
if (typeof tagsToCompile === 'string')
|
||||
tagsToCompile = tagsToCompile.split(spaceRe, 2);
|
||||
|
||||
if (!isArray(tagsToCompile) || tagsToCompile.length !== 2)
|
||||
throw new Error('Invalid tags: ' + tagsToCompile);
|
||||
|
||||
openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*');
|
||||
closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1]));
|
||||
closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1]));
|
||||
}
|
||||
|
||||
compileTags(tags || mustache.tags);
|
||||
|
||||
var scanner = new Scanner(template);
|
||||
|
||||
var start, type, value, chr, token, openSection;
|
||||
while (!scanner.eos()) {
|
||||
start = scanner.pos;
|
||||
|
||||
// Match any text between tags.
|
||||
value = scanner.scanUntil(openingTagRe);
|
||||
|
||||
if (value) {
|
||||
for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
|
||||
chr = value.charAt(i);
|
||||
|
||||
if (isWhitespace(chr)) {
|
||||
spaces.push(tokens.length);
|
||||
indentation += chr;
|
||||
} else {
|
||||
nonSpace = true;
|
||||
lineHasNonSpace = true;
|
||||
indentation += ' ';
|
||||
}
|
||||
|
||||
tokens.push([ 'text', chr, start, start + 1 ]);
|
||||
start += 1;
|
||||
|
||||
// Check for whitespace on the current line.
|
||||
if (chr === '\n') {
|
||||
stripSpace();
|
||||
indentation = '';
|
||||
tagIndex = 0;
|
||||
lineHasNonSpace = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Match the opening tag.
|
||||
if (!scanner.scan(openingTagRe))
|
||||
break;
|
||||
|
||||
hasTag = true;
|
||||
|
||||
// Get the tag type.
|
||||
type = scanner.scan(tagRe) || 'name';
|
||||
scanner.scan(whiteRe);
|
||||
|
||||
// Get the tag value.
|
||||
if (type === '=') {
|
||||
value = scanner.scanUntil(equalsRe);
|
||||
scanner.scan(equalsRe);
|
||||
scanner.scanUntil(closingTagRe);
|
||||
} else if (type === '{') {
|
||||
value = scanner.scanUntil(closingCurlyRe);
|
||||
scanner.scan(curlyRe);
|
||||
scanner.scanUntil(closingTagRe);
|
||||
type = '&';
|
||||
} else {
|
||||
value = scanner.scanUntil(closingTagRe);
|
||||
}
|
||||
|
||||
// Match the closing tag.
|
||||
if (!scanner.scan(closingTagRe))
|
||||
throw new Error('Unclosed tag at ' + scanner.pos);
|
||||
|
||||
if (type == '>') {
|
||||
token = [ type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace ];
|
||||
} else {
|
||||
token = [ type, value, start, scanner.pos ];
|
||||
}
|
||||
tagIndex++;
|
||||
tokens.push(token);
|
||||
|
||||
if (type === '#' || type === '^') {
|
||||
sections.push(token);
|
||||
} else if (type === '/') {
|
||||
// Check section nesting.
|
||||
openSection = sections.pop();
|
||||
|
||||
if (!openSection)
|
||||
throw new Error('Unopened section "' + value + '" at ' + start);
|
||||
|
||||
if (openSection[1] !== value)
|
||||
throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
|
||||
} else if (type === 'name' || type === '{' || type === '&') {
|
||||
nonSpace = true;
|
||||
} else if (type === '=') {
|
||||
// Set the tags for the next time around.
|
||||
compileTags(value);
|
||||
}
|
||||
}
|
||||
|
||||
stripSpace();
|
||||
|
||||
// Make sure there are no open sections when we're done.
|
||||
openSection = sections.pop();
|
||||
|
||||
if (openSection)
|
||||
throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
|
||||
|
||||
return nestTokens(squashTokens(tokens));
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines the values of consecutive text tokens in the given `tokens` array
|
||||
* to a single token.
|
||||
*/
|
||||
function squashTokens (tokens) {
|
||||
var squashedTokens = [];
|
||||
|
||||
var token, lastToken;
|
||||
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
|
||||
token = tokens[i];
|
||||
|
||||
if (token) {
|
||||
if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
|
||||
lastToken[1] += token[1];
|
||||
lastToken[3] = token[3];
|
||||
} else {
|
||||
squashedTokens.push(token);
|
||||
lastToken = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return squashedTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forms the given array of `tokens` into a nested tree structure where
|
||||
* tokens that represent a section have two additional items: 1) an array of
|
||||
* all tokens that appear in that section and 2) the index in the original
|
||||
* template that represents the end of that section.
|
||||
*/
|
||||
function nestTokens (tokens) {
|
||||
var nestedTokens = [];
|
||||
var collector = nestedTokens;
|
||||
var sections = [];
|
||||
|
||||
var token, section;
|
||||
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
|
||||
token = tokens[i];
|
||||
|
||||
switch (token[0]) {
|
||||
case '#':
|
||||
case '^':
|
||||
collector.push(token);
|
||||
sections.push(token);
|
||||
collector = token[4] = [];
|
||||
break;
|
||||
case '/':
|
||||
section = sections.pop();
|
||||
section[5] = token[2];
|
||||
collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
|
||||
break;
|
||||
default:
|
||||
collector.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
return nestedTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple string scanner that is used by the template parser to find
|
||||
* tokens in template strings.
|
||||
*/
|
||||
function Scanner (string) {
|
||||
this.string = string;
|
||||
this.tail = string;
|
||||
this.pos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the tail is empty (end of string).
|
||||
*/
|
||||
Scanner.prototype.eos = function eos () {
|
||||
return this.tail === '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Tries to match the given regular expression at the current position.
|
||||
* Returns the matched text if it can match, the empty string otherwise.
|
||||
*/
|
||||
Scanner.prototype.scan = function scan (re) {
|
||||
var match = this.tail.match(re);
|
||||
|
||||
if (!match || match.index !== 0)
|
||||
return '';
|
||||
|
||||
var string = match[0];
|
||||
|
||||
this.tail = this.tail.substring(string.length);
|
||||
this.pos += string.length;
|
||||
|
||||
return string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Skips all text until the given regular expression can be matched. Returns
|
||||
* the skipped string, which is the entire tail if no match can be made.
|
||||
*/
|
||||
Scanner.prototype.scanUntil = function scanUntil (re) {
|
||||
var index = this.tail.search(re), match;
|
||||
|
||||
switch (index) {
|
||||
case -1:
|
||||
match = this.tail;
|
||||
this.tail = '';
|
||||
break;
|
||||
case 0:
|
||||
match = '';
|
||||
break;
|
||||
default:
|
||||
match = this.tail.substring(0, index);
|
||||
this.tail = this.tail.substring(index);
|
||||
}
|
||||
|
||||
this.pos += match.length;
|
||||
|
||||
return match;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a rendering context by wrapping a view object and
|
||||
* maintaining a reference to the parent context.
|
||||
*/
|
||||
function Context (view, parentContext) {
|
||||
this.view = view;
|
||||
this.cache = { '.': this.view };
|
||||
this.parent = parentContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new context using the given view with this context
|
||||
* as the parent.
|
||||
*/
|
||||
Context.prototype.push = function push (view) {
|
||||
return new Context(view, this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the value of the given name in this context, traversing
|
||||
* up the context hierarchy if the value is absent in this context's view.
|
||||
*/
|
||||
Context.prototype.lookup = function lookup (name) {
|
||||
var cache = this.cache;
|
||||
|
||||
var value;
|
||||
if (cache.hasOwnProperty(name)) {
|
||||
value = cache[name];
|
||||
} else {
|
||||
var context = this, intermediateValue, names, index, lookupHit = false;
|
||||
|
||||
while (context) {
|
||||
if (name.indexOf('.') > 0) {
|
||||
intermediateValue = context.view;
|
||||
names = name.split('.');
|
||||
index = 0;
|
||||
|
||||
/**
|
||||
* Using the dot notion path in `name`, we descend through the
|
||||
* nested objects.
|
||||
*
|
||||
* To be certain that the lookup has been successful, we have to
|
||||
* check if the last object in the path actually has the property
|
||||
* we are looking for. We store the result in `lookupHit`.
|
||||
*
|
||||
* This is specially necessary for when the value has been set to
|
||||
* `undefined` and we want to avoid looking up parent contexts.
|
||||
*
|
||||
* In the case where dot notation is used, we consider the lookup
|
||||
* to be successful even if the last "object" in the path is
|
||||
* not actually an object but a primitive (e.g., a string, or an
|
||||
* integer), because it is sometimes useful to access a property
|
||||
* of an autoboxed primitive, such as the length of a string.
|
||||
**/
|
||||
while (intermediateValue != null && index < names.length) {
|
||||
if (index === names.length - 1)
|
||||
lookupHit = (
|
||||
hasProperty(intermediateValue, names[index])
|
||||
|| primitiveHasOwnProperty(intermediateValue, names[index])
|
||||
);
|
||||
|
||||
intermediateValue = intermediateValue[names[index++]];
|
||||
}
|
||||
} else {
|
||||
intermediateValue = context.view[name];
|
||||
|
||||
/**
|
||||
* Only checking against `hasProperty`, which always returns `false` if
|
||||
* `context.view` is not an object. Deliberately omitting the check
|
||||
* against `primitiveHasOwnProperty` if dot notation is not used.
|
||||
*
|
||||
* Consider this example:
|
||||
* ```
|
||||
* Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"})
|
||||
* ```
|
||||
*
|
||||
* If we were to check also against `primitiveHasOwnProperty`, as we do
|
||||
* in the dot notation case, then render call would return:
|
||||
*
|
||||
* "The length of a football field is 9."
|
||||
*
|
||||
* rather than the expected:
|
||||
*
|
||||
* "The length of a football field is 100 yards."
|
||||
**/
|
||||
lookupHit = hasProperty(context.view, name);
|
||||
}
|
||||
|
||||
if (lookupHit) {
|
||||
value = intermediateValue;
|
||||
break;
|
||||
}
|
||||
|
||||
context = context.parent;
|
||||
}
|
||||
|
||||
cache[name] = value;
|
||||
}
|
||||
|
||||
if (isFunction(value))
|
||||
value = value.call(this.view);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* A Writer knows how to take a stream of tokens and render them to a
|
||||
* string, given a context. It also maintains a cache of templates to
|
||||
* avoid the need to parse the same template twice.
|
||||
*/
|
||||
function Writer () {
|
||||
this.templateCache = {
|
||||
_cache: {},
|
||||
set: function set (key, value) {
|
||||
this._cache[key] = value;
|
||||
},
|
||||
get: function get (key) {
|
||||
return this._cache[key];
|
||||
},
|
||||
clear: function clear () {
|
||||
this._cache = {};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all cached templates in this writer.
|
||||
*/
|
||||
Writer.prototype.clearCache = function clearCache () {
|
||||
if (typeof this.templateCache !== 'undefined') {
|
||||
this.templateCache.clear();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses and caches the given `template` according to the given `tags` or
|
||||
* `mustache.tags` if `tags` is omitted, and returns the array of tokens
|
||||
* that is generated from the parse.
|
||||
*/
|
||||
Writer.prototype.parse = function parse (template, tags) {
|
||||
var cache = this.templateCache;
|
||||
var cacheKey = template + ':' + (tags || mustache.tags).join(':');
|
||||
var isCacheEnabled = typeof cache !== 'undefined';
|
||||
var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined;
|
||||
|
||||
if (tokens == undefined) {
|
||||
tokens = parseTemplate(template, tags);
|
||||
isCacheEnabled && cache.set(cacheKey, tokens);
|
||||
}
|
||||
return tokens;
|
||||
};
|
||||
|
||||
/**
|
||||
* High-level method that is used to render the given `template` with
|
||||
* the given `view`.
|
||||
*
|
||||
* The optional `partials` argument may be an object that contains the
|
||||
* names and templates of partials that are used in the template. It may
|
||||
* also be a function that is used to load partial templates on the fly
|
||||
* that takes a single argument: the name of the partial.
|
||||
*
|
||||
* If the optional `config` argument is given here, then it should be an
|
||||
* object with a `tags` attribute or an `escape` attribute or both.
|
||||
* If an array is passed, then it will be interpreted the same way as
|
||||
* a `tags` attribute on a `config` object.
|
||||
*
|
||||
* The `tags` attribute of a `config` object must be an array with two
|
||||
* string values: the opening and closing tags used in the template (e.g.
|
||||
* [ "<%", "%>" ]). The default is to mustache.tags.
|
||||
*
|
||||
* The `escape` attribute of a `config` object must be a function which
|
||||
* accepts a string as input and outputs a safely escaped string.
|
||||
* If an `escape` function is not provided, then an HTML-safe string
|
||||
* escaping function is used as the default.
|
||||
*/
|
||||
Writer.prototype.render = function render (template, view, partials, config) {
|
||||
var tags = this.getConfigTags(config);
|
||||
var tokens = this.parse(template, tags);
|
||||
var context = (view instanceof Context) ? view : new Context(view, undefined);
|
||||
return this.renderTokens(tokens, context, partials, template, config);
|
||||
};
|
||||
|
||||
/**
|
||||
* Low-level method that renders the given array of `tokens` using
|
||||
* the given `context` and `partials`.
|
||||
*
|
||||
* Note: The `originalTemplate` is only ever used to extract the portion
|
||||
* of the original template that was contained in a higher-order section.
|
||||
* If the template doesn't use higher-order sections, this argument may
|
||||
* be omitted.
|
||||
*/
|
||||
Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, config) {
|
||||
var buffer = '';
|
||||
|
||||
var token, symbol, value;
|
||||
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
|
||||
value = undefined;
|
||||
token = tokens[i];
|
||||
symbol = token[0];
|
||||
|
||||
if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate, config);
|
||||
else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate, config);
|
||||
else if (symbol === '>') value = this.renderPartial(token, context, partials, config);
|
||||
else if (symbol === '&') value = this.unescapedValue(token, context);
|
||||
else if (symbol === 'name') value = this.escapedValue(token, context, config);
|
||||
else if (symbol === 'text') value = this.rawValue(token);
|
||||
|
||||
if (value !== undefined)
|
||||
buffer += value;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
};
|
||||
|
||||
Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate, config) {
|
||||
var self = this;
|
||||
var buffer = '';
|
||||
var value = context.lookup(token[1]);
|
||||
|
||||
// This function is used to render an arbitrary template
|
||||
// in the current context by higher-order sections.
|
||||
function subRender (template) {
|
||||
return self.render(template, context, partials, config);
|
||||
}
|
||||
|
||||
if (!value) return;
|
||||
|
||||
if (isArray(value)) {
|
||||
for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
|
||||
buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate, config);
|
||||
}
|
||||
} else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {
|
||||
buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate, config);
|
||||
} else if (isFunction(value)) {
|
||||
if (typeof originalTemplate !== 'string')
|
||||
throw new Error('Cannot use higher-order sections without the original template');
|
||||
|
||||
// Extract the portion of the original template that the section contains.
|
||||
value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
|
||||
|
||||
if (value != null)
|
||||
buffer += value;
|
||||
} else {
|
||||
buffer += this.renderTokens(token[4], context, partials, originalTemplate, config);
|
||||
}
|
||||
return buffer;
|
||||
};
|
||||
|
||||
Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate, config) {
|
||||
var value = context.lookup(token[1]);
|
||||
|
||||
// Use JavaScript's definition of falsy. Include empty arrays.
|
||||
// See https://github.com/janl/mustache.js/issues/186
|
||||
if (!value || (isArray(value) && value.length === 0))
|
||||
return this.renderTokens(token[4], context, partials, originalTemplate, config);
|
||||
};
|
||||
|
||||
Writer.prototype.indentPartial = function indentPartial (partial, indentation, lineHasNonSpace) {
|
||||
var filteredIndentation = indentation.replace(/[^ \t]/g, '');
|
||||
var partialByNl = partial.split('\n');
|
||||
for (var i = 0; i < partialByNl.length; i++) {
|
||||
if (partialByNl[i].length && (i > 0 || !lineHasNonSpace)) {
|
||||
partialByNl[i] = filteredIndentation + partialByNl[i];
|
||||
}
|
||||
}
|
||||
return partialByNl.join('\n');
|
||||
};
|
||||
|
||||
Writer.prototype.renderPartial = function renderPartial (token, context, partials, config) {
|
||||
if (!partials) return;
|
||||
var tags = this.getConfigTags(config);
|
||||
|
||||
var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
|
||||
if (value != null) {
|
||||
var lineHasNonSpace = token[6];
|
||||
var tagIndex = token[5];
|
||||
var indentation = token[4];
|
||||
var indentedValue = value;
|
||||
if (tagIndex == 0 && indentation) {
|
||||
indentedValue = this.indentPartial(value, indentation, lineHasNonSpace);
|
||||
}
|
||||
var tokens = this.parse(indentedValue, tags);
|
||||
return this.renderTokens(tokens, context, partials, indentedValue, config);
|
||||
}
|
||||
};
|
||||
|
||||
Writer.prototype.unescapedValue = function unescapedValue (token, context) {
|
||||
var value = context.lookup(token[1]);
|
||||
if (value != null)
|
||||
return value;
|
||||
};
|
||||
|
||||
Writer.prototype.escapedValue = function escapedValue (token, context, config) {
|
||||
var escape = this.getConfigEscape(config) || mustache.escape;
|
||||
var value = context.lookup(token[1]);
|
||||
if (value != null)
|
||||
return (typeof value === 'number' && escape === mustache.escape) ? String(value) : escape(value);
|
||||
};
|
||||
|
||||
Writer.prototype.rawValue = function rawValue (token) {
|
||||
return token[1];
|
||||
};
|
||||
|
||||
Writer.prototype.getConfigTags = function getConfigTags (config) {
|
||||
if (isArray(config)) {
|
||||
return config;
|
||||
}
|
||||
else if (config && typeof config === 'object') {
|
||||
return config.tags;
|
||||
}
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
Writer.prototype.getConfigEscape = function getConfigEscape (config) {
|
||||
if (config && typeof config === 'object' && !isArray(config)) {
|
||||
return config.escape;
|
||||
}
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
var mustache = {
|
||||
name: 'mustache.js',
|
||||
version: '4.2.0',
|
||||
tags: [ '{{', '}}' ],
|
||||
clearCache: undefined,
|
||||
escape: undefined,
|
||||
parse: undefined,
|
||||
render: undefined,
|
||||
Scanner: undefined,
|
||||
Context: undefined,
|
||||
Writer: undefined,
|
||||
/**
|
||||
* Allows a user to override the default caching strategy, by providing an
|
||||
* object with set, get and clear methods. This can also be used to disable
|
||||
* the cache by setting it to the literal `undefined`.
|
||||
*/
|
||||
set templateCache (cache) {
|
||||
defaultWriter.templateCache = cache;
|
||||
},
|
||||
/**
|
||||
* Gets the default or overridden caching object from the default writer.
|
||||
*/
|
||||
get templateCache () {
|
||||
return defaultWriter.templateCache;
|
||||
}
|
||||
};
|
||||
|
||||
// All high-level mustache.* functions use this writer.
|
||||
var defaultWriter = new Writer();
|
||||
|
||||
/**
|
||||
* Clears all cached templates in the default writer.
|
||||
*/
|
||||
mustache.clearCache = function clearCache () {
|
||||
return defaultWriter.clearCache();
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses and caches the given template in the default writer and returns the
|
||||
* array of tokens it contains. Doing this ahead of time avoids the need to
|
||||
* parse templates on the fly as they are rendered.
|
||||
*/
|
||||
mustache.parse = function parse (template, tags) {
|
||||
return defaultWriter.parse(template, tags);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the `template` with the given `view`, `partials`, and `config`
|
||||
* using the default writer.
|
||||
*/
|
||||
mustache.render = function render (template, view, partials, config) {
|
||||
if (typeof template !== 'string') {
|
||||
throw new TypeError('Invalid template! Template should be a "string" ' +
|
||||
'but "' + typeStr(template) + '" was given as the first ' +
|
||||
'argument for mustache#render(template, view, partials)');
|
||||
}
|
||||
|
||||
return defaultWriter.render(template, view, partials, config);
|
||||
};
|
||||
|
||||
// Export the escaping function so that the user may override it.
|
||||
// See https://github.com/janl/mustache.js/issues/244
|
||||
mustache.escape = escapeHtml;
|
||||
|
||||
// Export these mainly for testing, but also for advanced usage.
|
||||
mustache.Scanner = Scanner;
|
||||
mustache.Context = Context;
|
||||
mustache.Writer = Writer;
|
||||
|
||||
export default mustache;
|
||||
1
static/skin/mustache.min.js
vendored
Normal file
1
static/skin/mustache.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user