Compare commits

...

57 Commits

Author SHA1 Message Date
Nikhil Tanwar
fc87def18b Add documentation for widget
Added documentation for current widget usage, currently supported arguments, using custom CSS/JS
2022-08-15 18:22:23 +05:30
Nikhil Tanwar
09dc6f90fd Add custom CSS and JS support
Added an event listener for message event.
The idea is the website which embeds the widget will send a message using postMessage().
The expected message is:
{
css: // custom CSS code
js: // custom JS code
}
2022-08-15 18:22:22 +05:30
Nikhil Tanwar
58c04b3f77 Basic widget handling
Adds handling for parameters:
disablefilter - disable search filters
disableclick - disable book click action
disabledownload - disable download button
disabledesc - disable description
2022-08-15 18:21:41 +05:30
Nikhil Tanwar
d27220f65d Give name to IIFE in index.js and expose updateBookCount
Gives a name to the IIFE wrapping code in index.js - kiwixServe
and exposes updateBookCount through it
2022-08-11 21:18:21 +05:30
Nikhil Tanwar
efe42c9bbe Add widget endpoint
Adds an endpoint /widget to provide kiwix serve widget.
2022-08-11 21:18:21 +05:30
Nikhil Tanwar
489dfc1123 Give a name to function for updating book count
Extracts function updateBookCount() from the unnamed function in resize event.
2022-08-11 21:10:47 +05:30
Matthieu Gautier
9f545718c2 Merge pull request #806 from kiwix/content_endpoint
/content endpoint
2022-08-11 17:04:02 +02:00
Veloman Yunkan
e323dcf6c9 Redirecting /nonendpoint URLs to /content/nonendpoint 2022-08-11 18:04:05 +04:00
Veloman Yunkan
3b98987cb3 More robust handling of endpoint URLs
The next goal is to redirect old-style /book/path/to/entry URLs to
/content/book/path/to/entry, which seemed pretty trivial.

However, given the current handling of some endpoint URLs, more work was
required to ensure that invalid endpoint URLs (e.g.  "/random/number" or
"/suggest/fr") are not interpreted as content URLs. Previously, that was
not a user-observable issue, since the result would be an immediate 404
error (except in certain edge cases, like handling the request for
"/random/number" when there is a book with name "random" containing an
article at path "/number"). With redirection of URLs that were assumed
to refer to content a 404 error would be issued for the
transformed URL ("/content/random/number") which may be confusing.

Therefore this change is to ensure the correct routing of endpoint URL
handling.
2022-08-11 18:04:05 +04:00
Veloman Yunkan
fd36d11ccf Search results now use the /content URL scheme 2022-08-11 18:04:05 +04:00
Veloman Yunkan
dc56f82c29 Using /content/... URLs in OPDS output 2022-08-11 18:04:05 +04:00
Veloman Yunkan
1b1c1e352e Introduced /content endpoint
Book content is now served under /content/book/...

The old access to book content via a top-level URL /book/... is so far
preserved for backward compatibility.

Redirects were changed to use the new URL scheme. Links in the search results
still use the old scheme.
2022-08-11 18:04:05 +04:00
Veloman Yunkan
a4b18893aa Moved handling of the "/" URL 2022-08-11 18:04:05 +04:00
Matthieu Gautier
d737db666a Merge pull request #802 from kiwix/include_tags_in_free_text_library_search
Included tags in free text catalog search
2022-08-10 16:43:48 +02:00
Veloman Yunkan
cff143b4ec Included tags in free text catalog search 2022-08-06 07:39:45 +02:00
Matthieu Gautier
8e6d893f7f Merge pull request #804 from kiwix/illustration_url_uses_the_book_uuid
Illustration URL uses the book UUID
2022-08-04 15:43:22 +02:00
Veloman Yunkan
111aab0c23 Illustration URL uses the book UUID
If the server is initialized with a library.xml file, then the id
specified in the XML file is used (rather than the UUID recorded in the
ZIM file).

Note that in test/data/library.xml the book ids are fake and
different from the real ZIM IDs; that file was created for testing
of the /catalog endpoint which doesn't access ZIM content, so the
the same ZIM file zimfile.zim was added to library.xml three times as
three different books (with unique human-friendly ids). This explains
the diff in test/library_server.cpp.
2022-08-03 16:13:21 +02:00
Kelson
dd90ca1018 Merge pull request #805 from kiwix/fixFavicon
Add favicons (for different devices) to kiwix-serve welcome page
2022-08-03 16:11:54 +02:00
Nikhil Tanwar
3facd594f6 Add favicon for different devices.
Added favicon files for a number of devices.
All files and html code is generated by: https://realfavicongenerator.net/
The file used to generate favicons can be found at: https://upload.wikimedia.org/wikipedia/commons/b/b0/Kiwix_logo_v3.svg
2022-08-03 18:52:13 +05:30
Kelson
4cd52b0809 Merge pull request #796 from kiwix/noJquery
No jquery
2022-08-01 15:15:02 +02:00
Nikhil Tanwar
baf22c2516 So long, jQuery
Now after porting index.js and taskbar.js to vanilla JS, it is time to remove files.
Deleted static/skin/jquery-ui
Updated customIndexPage template in README.md.
Thank you for your service, jQuery :)
2022-07-31 19:16:46 +05:30
Nikhil Tanwar
f8a530100f Implement taskbar scroll actions in vanilla JS
Completes the porting of remaining jQuery code in taskbar.js - scroll function, blur and focus events and the cybook hack.
2022-07-31 19:16:02 +05:30
Nikhil Tanwar
a0db199388 Turn suggestions into hyperlinks
The suggestions are now clickable hyperlinks.
2022-07-31 19:11:46 +05:30
Matthieu Gautier
f0f473b829 Show suggestions using autoComplete.js
This change only shows suggestions. Clicking them does nothing.
2022-07-31 17:15:08 +05:30
Nikhil Tanwar
1e247d75bb Welcome, autoComplete.js
Added autoComplete.css and .js files.
Linked files in head_taskbar.html
2022-07-31 16:16:07 +05:30
Matthieu Gautier
4f3ec817db Update index.js to not use jquery anymore. 2022-07-31 01:06:27 +05:30
Kelson
98bcf8acd6 Merge pull request #791 from kiwix/ci_pull_request
CI triggered on pull_request event
2022-07-27 21:28:30 +02:00
Emmanuel Engelhart
b69bf4d062 Simplify branch retrieval 2022-07-20 21:21:31 +02:00
Emmanuel Engelhart
6891ce3b57 Use actions/checkout@v2 2022-07-20 21:21:31 +02:00
Emmanuel Engelhart
16197afc95 CI triggered on pull_request event 2022-07-20 21:21:31 +02:00
Kelson
abccd9d706 Merge pull request #800 from kiwix/escClose
Exit download modal on pressing escape key.
2022-07-20 21:19:01 +02:00
Nikhil Tanwar
d0adb4e722 Exit download modal on pressing escape key.
Adds an event listener to call closeModal() when Escape key is pressed.
2022-07-21 00:39:26 +05:30
Kelson
88c25b3a6c Merge pull request #786 from kiwix/optimizedWelcome
More tiles on kiwix-serve welcome page (optimised)
2022-07-20 19:41:50 +02:00
Emmanuel Engelhart
5aa74c62d6 Better align kiwix-serve welcome page filters 2022-07-20 19:18:18 +02:00
Nikhil Tanwar
2b6da38c46 Center tiles on welcome page
This change centers tiles on welcome page to give a more consistent whitespace look on both sides.
For this, the layout in Isotope JS is changed to masonry.
2022-07-20 19:18:18 +02:00
Matthieu Gautier
dfc6cad9c2 Merge pull request #795 from kiwix/icu_data_check 2022-07-19 11:23:11 +02:00
Veloman Yunkan
28f8dbcf20 New unit-test stringTools.ICULanguageInfo 2022-07-07 16:13:49 +04:00
Matthieu Gautier
81865c0f0e Merge pull request #794 from kiwix/fixHeader 2022-07-06 17:05:40 +02:00
Nikhil Tanwar
538a46f262 Include iostream header in include/version.h
This is needed for kiwix-tools compilation.
2022-07-05 20:47:14 +05:30
Kelson
e1d1d202bd Merge pull request #789 from kiwix/remove_wrappers
Remove libzim's wrapper.
2022-07-03 19:34:58 +02:00
Matthieu Gautier
71e2df7406 Explicit std
Removed headers were `using namespace std`.
So we have to be explicit everywhere.
2022-07-02 16:33:32 +02:00
Matthieu Gautier
69931fb347 Remove libzim's wrapper.
It is time to remove them. They are deprecated since 10.0.0
2022-07-02 16:33:32 +02:00
Veloman Yunkan
12e0fb6934 Merge pull request #711 from kiwix/tagFilter
Add tag filtering in kiwix-serve
2022-06-25 18:22:06 +04:00
Nikhil Tanwar
43ab6dfb6a Add ability to filter by tags in kiwix serve
This change introduces filtering by tags.
To filter, the user can click on the tag name and it will filter it.
A label is added (clickable) to show the tag filter, it can be clicked to remove the filter
2022-06-25 18:10:01 +04:00
Nikhil Tanwar
93f2686a94 Refactoring kiwixButton
Move hover behaviour as a different class - kiwixButtonHover
Add cursor:pointer to kiwixButton
2022-06-25 18:10:01 +04:00
Nikhil Tanwar
19a9c84e13 Change class name "searchButton" to "kiwixButton"
This is done to retain the button design in more button designs (ex: tags)
2022-06-25 18:10:01 +04:00
Nikhil Tanwar
f034018b5c Extract setNoResultsContent() from checkAndInjectEmptyMessage()
Extracted the code from the un-named function in setTimeout for easier understanding.
2022-06-25 18:10:01 +04:00
Nikhil Tanwar
596b223a9d Drop onclick handler for reset-filter link
This removes the onclick handler around the reset-filter link which redirected to '/?lang='
Everything under the handler was already done on window.onload
2022-06-25 18:10:01 +04:00
Nikhil Tanwar
0c549af307 Add check to not add same link in session history
Previously, if the following steps were executed:
1. Click a book tile/visit an unrelated link from the address bar
2. Press back button
Then forward history was discarded (forward button gets disabled).
This happened because of the window.history.pushState on every window.onload event. This led to the same link being added in history and thus discarding the previous "forward-history"
This change adds a condition to only push the current state if the queries are not same.
2022-06-25 18:10:01 +04:00
Nikhil Tanwar
37b39430d1 Use shortened URL in pushState
Earlier we were using the full URL, now only query string is passed in pushState, much cleaner!
2022-06-25 18:10:01 +04:00
Nikhil Tanwar
947744caea Introduce updateVisibleParams()
Adds a function to wrap logic to update select boxes on history change
2022-06-25 18:10:01 +04:00
Kelson
b9e03d2772 Merge pull request #790 from kiwix/docTypo 2022-06-25 07:38:28 +02:00
Nikhil Tanwar
e9b7eeb3c9 Fix documentation typos
Replace wrong  mentions of libzim with libkiwix
Remove libkiwix deprecated functions mention in usage.rst - they are removed now
2022-06-25 09:44:51 +05:30
Matthieu Gautier
15cb9025bb Merge pull request #779 from kiwix/serving_customized_resources 2022-06-22 15:31:20 +02:00
Veloman Yunkan
1139f2cb4c Testing of static front-end resource customization
One important missing test is that the content of the customized
resource is read from storage every time rather than once. Testing
that requirement would involve creating temporary files which is a
little more work.
2022-06-22 17:11:08 +04:00
Veloman Yunkan
0086049d4f Extracted LibraryServerTest into a file of its own 2022-06-22 15:22:12 +04:00
Veloman Yunkan
e3e4bfa533 Support for serving customized resources
During work on the kiwix-serve front-end, the edit-save-test cycle is
a multistep procedure:

1. build and install libkiwix
2. build kiwix-tools
3. run kiwix-serve
4. reload the web-page in the browser

When making changes in static resources that are served by kiwix-serve
unmodified, the steps 1-3 can be eliminated if kiwix-serve is capable of
serving resources from the file-system. This commit adds such a
functionality to kiwix-serve. Now, if during startup of kiwix-serve the
environment variable `KIWIX_SERVE_CUSTOMIZED_RESOURCES` is defined it is
assumed to point to a file where every line has the following format:

URL MIMETYPE RESOURCE_FILE_PATH

When a request is received by kiwix-serve and its URL matches any of the
URLs read from the customized resource file, then the resource data is
read from the respective file RESOURCE_FILE_PATH and served with
mime-type MIMETYPE.

Though this feature was introduced in order to facilitate the
development of the iframe-based content viewer, it can also be useful to
users who would like to customize the kiwix-serve front-end on their own
(without re-building all of kiwix-serve).

There is some overlap with a feature of the kiwix-compile-resources
script that also allows to override resources. The differences are:

1. The new way of customizing front-end resources has all such resources
   listed in a text file and there is a single environment variable
   from which the path of that file is read. kiwix-compile-resources
   associates a separate environment variable with each resource.

2. The new way uses regular paths to identify a resource. The
   kiwix-compile-resources method encodes the resource path by replacing
   any non-alphanumeric characters (including the path separator) with
   underscores (so that the resulting resource identifier can be used
   to construct the name of the environment variable controlling that
   resource).

3. The new method allows adding new front-end resources. The old method
   only allows to modify existing resources.

4. The new method allows (actually requires) to specify the URL at which
   the overriden resource should be served (similarly, the MIME-type can/must
   be specified, too). The old method only allows to override the contents of
   a resource.

5. The new method only allows to override front-end resources that are
   served without any preprocessing by kiwix-serve at runtime. The old
   method allows to override template resources as well (note that
   internationalization/translation resources cannot be overriden using the
   old method, either).
2022-06-22 10:59:41 +02:00
91 changed files with 2116 additions and 28989 deletions

View File

@@ -1,13 +1,17 @@
name: CI
on: [push]
on:
push:
branches:
- master
pull_request:
jobs:
Macos:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Setup python 3.10
uses: actions/setup-python@v2
with:
@@ -82,10 +86,6 @@ jobs:
container:
image: "kiwix/kiwix-build_ci:${{matrix.image_variant}}-31"
steps:
- name: Extract branch name
shell: bash
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
id: extract_branch
- name: Checkout code
shell: python
run: |
@@ -95,7 +95,7 @@ jobs:
'git', 'clone',
'https://github.com/${{github.repository}}',
'--depth=1',
'--branch', '${{steps.extract_branch.outputs.branch}}'
'--branch', '${{ github.head_ref || github.ref_name }}'
]
check_call(command, cwd=environ['HOME'])
- name: Install deps

View File

@@ -173,14 +173,6 @@ To use JS provided by kiwix-serve you can use the following template to start wi
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title><-- Custom Tittle --></title>
<script
type="text/javascript"
src="{{root}}/skin/jquery-ui/external/jquery/jquery.js"
></script>
<script
type="text/javascript"
src="{{root}}/skin/jquery-ui/jquery-ui.min.js"
></script>
<script src="{{root}}/skin/isotope.pkgd.min.js" defer></script>
<script src="{{root}}/skin/iso6391To3.js"></script>
<script type="text/javascript" src="{{root}}/skin/index.js" defer></script>
@@ -198,7 +190,7 @@ To use JS provided by kiwix-serve you can use the following template to start wi
```
<form id='kiwixSearchForm'>
<input type="text" name="q" placeholder="Search" id="searchFilter" class='kiwixSearch filter'>
<input type="submit" class="searchButton" value="Search"/>
<input type="submit" class="kiwixButton" value="Search"/>
</form>
```

View File

@@ -19,7 +19,7 @@ import os
project = 'libkiwix'
copyright = '2022, libkiwix-team'
author = 'libzim-team'
author = 'libkiwix-team'
# -- General configuration ---------------------------------------------------

View File

@@ -3,7 +3,7 @@
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to libzim's documentation!
Welcome to libkiwix's documentation!
==================================
.. toctree::
@@ -12,3 +12,4 @@ Welcome to libzim's documentation!
usage
api/ref_api
widget

View File

@@ -7,11 +7,9 @@ Introduction
libkiwix is written in C++. To use the library, you need the include files of libkiwix have
to link against libzim.
Errors are handled with exceptions. When something goes wrong, libzim throws an error,
Errors are handled with exceptions. When something goes wrong, libkiwix throws an error,
which is always derived from std::exception.
All classes are defined in the namespace kiwix.
libkiwix is a set of tools to manage zim files and provide some common functionnality.
While libkiwix has some wrappers around libzim classes, they are deprecated and will be removed
in the future.

82
docs/widget.rst Normal file
View File

@@ -0,0 +1,82 @@
Kiwix serve widget
====================
Introduction
------------
The kiwix-serve widget provides an easy to embed way to show the `kiwix-serve` homepage.
Usage
-----
To use the widget, simply add an iframe with its `src` attribute set to the `widget` endpoint.
Example HTML Page ::
<!DOCTYPE html>
<html lang="en">
<head>
<title>Widget Test</title>
</head>
<body>
<iframe src="http://192.168.18.8:8080/widget?disabledesc&disablefilter&disabledownload" width=1000 height=1000></iframe>
</body>
</html>
This creates an iframe with the kiwix-serve homepage contents.
Arguments are explained below.
Possible Arguments
-------------------
Currently, the following arguments are supported.
disabledesc (value = N/A)
Disables the description part of a tile.
disablefilter (value = N/A)
Disables the search filters: language, category, tag and search function.
disableclick (value = N/A)
Disables clicking the book to open it for reading.
disabledownload (value = N/A)
Disables the download button (if avaialable at all) on the tile.
Custom CSS and JS
-----------------
You can add your custom CSS rules and Javascript code to the widget.
To do that, use the following code as template::
<iframe id="receiver" src="http://192.168.18.8:8080/widget?disabledesc=&disablefilter=&disabledownload=" width="1000" height="1000">
<p>Your browser does not support iframes.</p>
</iframe>
<script>
window.onload = function() {
var receiver = document.getElementById('receiver').contentWindow;
function sendMessage() {
let msg = {
css: `
.book__header {
color:red;
}`,
js: `
function widgetTest() {
console.log("Testing widget");
}
widgetTest();
`
}
receiver.postMessage(msg, 'http://192.168.18.8:8080/widget');
}
sendMessage();
}
</script>
The CSS/JS fields are optional, you may send both or only one.

View File

@@ -38,7 +38,6 @@ namespace kiwix
{
class OPDSDumper;
class Reader;
/**
* A class to store information about a book (a zim file)
@@ -69,7 +68,6 @@ class Book
~Book();
bool update(const Book& other);
DEPRECATED void update(const Reader& reader);
void update(const zim::Archive& archive);
void updateFromXml(const pugi::xml_node& node, const std::string& baseDir);
void updateFromOpds(const pugi::xml_node& node, const std::string& urlHost);

View File

@@ -1,193 +0,0 @@
/*
* Copyright 2018-2020 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_ENTRY_H
#define KIWIX_ENTRY_H
#include <stdio.h>
#include <zim/entry.h>
#include <zim/item.h>
#include <exception>
#include <string>
#include "common.h"
using namespace std;
namespace kiwix
{
class NoEntry : public std::exception {};
/**
* A entry represent an.. entry in a zim file.
*/
class Entry
{
public:
/**
* Construct an entry making reference to an zim article.
*
* @param article a zim::Article object
*/
DEPRECATED Entry(zim::Entry entry) : Entry(entry, true) {};
virtual ~Entry() = default;
/**
* Get the path of the entry.
*
* The path is the "key" of an entry.
*
* @return the path of the entry.
*/
std::string getPath() const { return entry.getPath(); }
/**
* Get the title of the entry.
*
* @return the title of the entry.
*/
std::string getTitle() const { return entry.getTitle(); }
/**
* Get the content of the entry.
*
* The string is a copy of the content.
* If you don't want to do a copy, use get_blob.
*
* @return the content of the entry.
*/
std::string getContent() const { return entry.getItem().getData(); }
/**
* Get the blob of the entry.
*
* A blob make reference to the content without copying it.
*
* @param offset The starting offset of the blob.
* @return the blob of the entry.
*/
zim::Blob getBlob(offset_type offset = 0) const { return entry.getItem().getData(offset); }
/**
* Get the blob of the entry.
*
* A blob make reference to the content without copying it.
*
* @param offset The starting offset of the blob.
* @param size The size of the blob.
* @return the blob of the entry.
*/
zim::Blob getBlob(offset_type offset, size_type size) const { return entry.getItem().getData(offset, size); }
/**
* Get the info for direct access to the content of the entry.
*
* Some entry (ie binary ones) have their content plain stored
* in the zim file. Knowing the offset where the content is stored
* an user can directly read the content in the zim file bypassing the
* libkiwix/libzim.
*
* @return A pair specifying where to read the content.
* The string is the real file to read (may be different that .zim
* file if zim is cut).
* The offset is the offset to read in the file.
* Return <"",0> if is not possible to read directly.
*/
zim::Item::DirectAccessInfo getDirectAccessInfo() const { return entry.getItem().getDirectAccessInformation(); }
/**
* Get the size of the entry.
*
* @return the size of the entry.
*/
size_type getSize() const;
/**
* Get the mime_type of the entry.
*
* @return the mime_type of the entry.
*/
std::string getMimetype() const;
/**
* Get if the entry is a redirect entry.
*
* @return True if the entry is a redirect.
*/
bool isRedirect() const;
/**
* Get if the entry is a link target entry.
*
* @return True if the entry is a link target.
*/
bool isLinkTarget() const;
/**
* Get if the entry is a deleted entry.
*
* @return True if the entry is a deleted entry.
*/
bool isDeleted() const;
/**
* Get the entry pointed by this entry.
*
* @return the entry pointed.
* @throw NoEntry if the entry is not a redirected entry.
*/
Entry getRedirectEntry() const;
/**
* Get the final entry pointed by this entry.
*
* Follow the redirection until a "not redirecting" entry is found.
* If the entry is not a redirected entry, return the entry itself.
*
* @return the final entry.
*/
Entry getFinalEntry() const;
/**
* Get the zim entry wrapped by this (kiwix) entry
*
* @return the zim entry
*/
const zim::Entry& getZimEntry() const { return entry; }
private:
zim::Entry entry;
private:
// Entry is deprecated, so we've marked the constructor as deprecated.
// But we still need to construct the entry (in our deprecated code)
// To avoid warning because we use deprecated function, we create a second
// constructor not deprecated. The `bool marker` is unused, it sole purpose
// is to change the signature to have two different constructor.
// This one is not deprecated and we must use it in our private code.
Entry(zim::Entry entry, bool marker);
friend class Reader;
};
}
#endif // KIWIX_ENTRY_H

View File

@@ -223,7 +223,6 @@ class Library
Book getBookByIdThreadSafe(const std::string& id) const;
DEPRECATED std::shared_ptr<Reader> getReaderById(const std::string& id);
std::shared_ptr<zim::Archive> getArchiveById(const std::string& id);
std::shared_ptr<ZimSearcher> getSearcherById(const std::string& id) {
return getSearcherByIds(BookIdSet{id});

View File

@@ -22,7 +22,6 @@
#include "book.h"
#include "library.h"
#include "reader.h"
#include <string>
#include <vector>

View File

@@ -7,9 +7,6 @@ headers = [
'libxml_dumper.h',
'opds_dumper.h',
'downloader.h',
'reader.h',
'entry.h',
'searcher.h',
'search_renderer.h',
'server.h',
'kiwixserve.h',

View File

@@ -27,7 +27,6 @@
#include <pugixml.hpp>
#include "library.h"
#include "reader.h"
using namespace std;

View File

@@ -1,506 +0,0 @@
/*
* Copyright 2011 Emmanuel Engelhart <kelson@kiwix.org>
*
* 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_READER_H
#define KIWIX_READER_H
#include <stdio.h>
#include <zim/zim.h>
#include <zim/archive.h>
#include <exception>
#include <map>
#include <sstream>
#include <string>
#include "common.h"
#include "entry.h"
using namespace std;
namespace kiwix
{
/**
* The SuggestionItem is a helper class that contains the info about a single
* suggestion item.
*/
class SuggestionItem
{
// Functions
public:
// Create a sugggestion item.
explicit SuggestionItem(const std::string& title, const std::string& normalizedTitle,
const std::string& path, const std::string& snippet = "") :
title(title),
normalizedTitle(normalizedTitle),
path(path),
snippet(snippet) {}
public:
const std::string& getTitle() const { return title;}
const std::string& getNormalizedTitle() const { return normalizedTitle;}
const std::string& getPath() const { return path;}
const std::string& getSnippet() const { return snippet;}
bool hasSnippet() const { return !snippet.empty();}
// Data
private:
std::string title;
std::string normalizedTitle;
std::string path;
std::string snippet;
};
/**
* The Reader class is the class who allow to get an entry content from a zim
* file.
*
* Reader is now deprecated. Directly use `zim::Archive`.
*/
using SuggestionsList_t = std::vector<SuggestionItem>;
class Reader
{
public:
/**
* Create a Reader to read a zim file specified by zimFilePath.
*
* @param zimFilePath The path to the zim file to read.
* The zim file can be splitted (.zimaa, .zimab, ...).
* In this case, the file path must still point to the
* unsplitted path as if the file were not splitted
* (.zim extesion).
*/
explicit DEPRECATED Reader(const string zimFilePath);
/**
* Create a Reader to read a zim file given by the Archive.
*
* @param archive The shared pointer to the Archive object.
*/
explicit DEPRECATED Reader(const std::shared_ptr<zim::Archive> archive)
: Reader(archive, true) {};
#ifndef _WIN32
explicit DEPRECATED Reader(int fd);
DEPRECATED Reader(int fd, zim::offset_type offset, zim::size_type size);
#endif
~Reader() = default;
/**
* Get the number of "displayable" entries in the zim file.
*
* @return If the zim file has a /M/Counter metadata, return the number of
* entries with the 'text/html' MIMEtype specified in the metadata.
* Else return the number of entries in the 'A' namespace.
*/
unsigned int getArticleCount() const;
/**
* Get the number of media in the zim file.
*
* @return If the zim file has a /M/Counter metadata, return the number of
* entries with the 'image/jpeg', 'image/gif' and 'image/png' in
* the metadata.
* Else return the number of entries in the 'I' namespace.
*/
unsigned int getMediaCount() const;
/**
* Get the number of all entries in the zim file.
*
* @return Return the number of all the entries, whatever their MIMEtype or
* their namespace.
*/
unsigned int getGlobalCount() const;
/**
* Get the path of the zim file.
*
* @return the path of the zim file as given in the constructor.
*/
string getZimFilePath() const;
/**
* Get the Id of the zim file.
*
* @return The uuid stored in the zim file.
*/
string getId() const;
/**
* Get a random page.
*
* @return A random Entry. The entry is picked from all entries in
* the 'A' namespace.
* The main entry is excluded from the potential results.
*/
Entry getRandomPage() const;
/**
* Get the entry of the main page.
*
* @return Entry of the main page as specified in the zim file.
*/
Entry getMainPage() const;
/**
* Get the content of a metadata.
*
* @param[in] name The name of the metadata.
* @param[out] value The value will be set to the content of the metadata.
* @return True if it was possible to get the content of the metadata.
*/
bool getMetadata(const string& name, string& value) const;
/**
* Get the name of the zim file.
*
* @return The name of the zim file as specified in the zim metadata.
*/
string getName() const;
/**
* Get the title of the zim file.
*
* @return The title of zim file as specified in the zim metadata.
* If no title has been set, return a title computed from the
* file path.
*/
string getTitle() const;
/**
* Get the creator of the zim file.
*
* @return The creator of the zim file as specified in the zim metadata.
*/
string getCreator() const;
/**
* Get the publisher of the zim file.
*
* @return The publisher of the zim file as specified in the zim metadata.
*/
string getPublisher() const;
/**
* Get the date of the zim file.
*
* @return The date of the zim file as specified in the zim metadata.
*/
string getDate() const;
/**
* Get the description of the zim file.
*
* @return The description of the zim file as specified in the zim metadata.
* If no description has been set, return the subtitle.
*/
string getDescription() const;
/**
* Get the long description of the zim file.
*
* @return The long description of the zim file as specifed in the zim metadata.
*/
string getLongDescription() const;
/**
* Get the language of the zim file.
*
* @return The language of the zim file as specified in the zim metadata.
*/
string getLanguage() const;
/**
* Get the license of the zim file.
*
* @return The license of the zim file as specified in the zim metadata.
*/
string getLicense() const;
/**
* Get the tags of the zim file.
*
* @param original If true, return the original tags as specified in the zim metadata.
* Else, try to convert it to the new 'normalized' format.
* @return The tags of the zim file.
*/
string getTags(bool original=false) const;
/**
* Get the value (as a string) of a specific tag.
*
* According to https://wiki.openzim.org/wiki/Tags
*
* @return The value of the specified tag.
* @throw std::out_of_range if the specified tag is not found.
*/
string getTagStr(const std::string& tagName) const;
/**
* Get the boolean value of a specific tag.
*
* According to https://wiki.openzim.org/wiki/Tags
*
* @return The boolean value of the specified tag.
* @throw std::out_of_range if the specified tag is not found.
* std::domain_error if the value of the tag cannot be convert to bool.
*/
bool getTagBool(const std::string& tagName) const;
/**
* Get the relations of the zim file.
*
* @return The relation of the zim file as specified in the zim metadata.
*/
string getRelation() const;
/**
* Get the flavour of the zim file.
*
* @return The flavour of the zim file as specified in the zim metadata.
*/
string getFlavour() const;
/**
* Get the source of the zim file.
*
* @return The source of the zim file as specified in the zim metadata.
*/
string getSource() const;
/**
* Get the scraper of the zim file.
*
* @return The scraper of the zim file as specified in the zim metadata.
*/
string getScraper() const;
/**
* Get the favicon of the zim file.
*
* @param[out] content The content of the favicon.
* @param[out] mimeType The mimeType of the favicon.
* @return True if a favicon has been found.
*/
bool getFavicon(string& content, string& mimeType) const;
/**
* Get an entry associated to an path.
*
* @param path The path of the entry.
* @return The entry.
* @throw NoEntry If no entry correspond to the path.
*/
Entry getEntryFromPath(const std::string& path) const;
/**
* Get an entry associated to an url encoded path.
*
* Equivalent to `getEntryFromPath(urlDecode(path));`
*
* @param path The url encoded path.
* @return The entry.
* @throw NoEntry If no entry correspond to the path.
*/
Entry getEntryFromEncodedPath(const std::string& path) const;
/**
* Get un entry associated to a title.
*
* @param title The title.
* @return The entry
* throw NoEntry If no entry correspond to the url.
*/
Entry getEntryFromTitle(const std::string& title) const;
/**
* Search for entries with title starting with prefix (case sensitive).
*
* Suggestions are stored in an internal vector and can be retrieved using
* `getNextSuggestion` method.
* This method is not thread safe and is deprecated. Use :
* bool searchSuggestions(const string& prefix,
* unsigned int suggestionsCount,
* SuggestionsList_t& results);
*
* @param prefix The prefix to search.
* @param suggestionsCount How many suggestions to search for.
* @param reset If true, remove previous suggestions in the internal vector.
* If false, add suggestions to the internal vector
* (until internal vector size is suggestionCount (or no more
* suggestion))
* @return True if some suggestions have been added to the internal vector.
*/
DEPRECATED bool searchSuggestions(const string& prefix,
unsigned int suggestionsCount,
const bool reset = true);
/**
* Search for entries with title starting with prefix (case sensitive).
*
* Suggestions are added to the `result` vector.
*
* @param prefix The prefix to search.
* @param suggestionsCount How many suggestions to search for.
* @param result The vector where to store the suggestions.
* @return True if some suggestions have been added to the vector.
*/
bool searchSuggestions(const string& prefix,
unsigned int suggestionsCount,
SuggestionsList_t& resuls);
/**
* Search for entries for the given prefix.
*
* If the zim file has a internal fulltext index, the suggestions will be
* searched using it.
* Else the suggestions will be search using `searchSuggestions` while trying
* to be smart about case sensitivity (using `getTitleVariants`).
*
* In any case, suggestions are stored in an internal vector and can be
* retrieved using `getNextSuggestion` method.
* The internal vector will be reset.
* This method is not thread safe and is deprecated. Use :
* bool searchSuggestionsSmart(const string& prefix,
* unsigned int suggestionsCount,
* SuggestionsList_t& results);
*
* @param prefix The prefix to search for.
* @param suggestionsCount How many suggestions to search for.
*/
DEPRECATED bool searchSuggestionsSmart(const string& prefix,
unsigned int suggestionsCount);
/**
* Search for entries for the given prefix.
*
* If the zim file has a internal fulltext index, the suggestions will be
* searched using it.
* Else the suggestions will be search using `searchSuggestions` while trying
* to be smart about case sensitivity (using `getTitleVariants`).
*
* In any case, suggestions are stored in an internal vector and can be
* retrieved using `getNextSuggestion` method.
* The internal vector will be reset.
*
* @param prefix The prefix to search for.
* @param suggestionsCount How many suggestions to search for.
* @param results The vector where to store the suggestions
* @return True if some suggestions have been added to the results.
*/
bool searchSuggestionsSmart(const string& prefix,
unsigned int suggestionsCount,
SuggestionsList_t& results);
/**
* Check if the path exists in the zim file.
*
* @param path the path to check.
* @return True if the path exists in the zim file.
*/
bool pathExists(const string& path) const;
/**
* Check if the zim file has a embedded fulltext index.
*
* @return True if the zim file has a embedded fulltext index
* and is not split (else the fulltext is not accessible).
*/
bool hasFulltextIndex() const;
/**
* Get potential case title variations for a title.
*
* @param title a title.
* @return the list of variantions.
*/
std::vector<std::string> getTitleVariants(const std::string& title) const;
/**
* Get the next suggestion title.
*
* @param[out] title the title of the suggestion.
* @return True if title has been set.
*/
DEPRECATED bool getNextSuggestion(string& title);
/**
* Get the next suggestion title and url.
*
* @param[out] title the title of the suggestion.
* @param[out] url the url of the suggestion.
* @return True if title and url have been set.
*/
DEPRECATED bool getNextSuggestion(string& title, string& url);
/**
* Get if we can check zim file integrity (has a checksum).
*
* @return True if zim file have a checksum.
*/
bool canCheckIntegrity() const;
/**
* Check is zim file is corrupted.
*
* @return True if zim file is corrupted.
*/
bool isCorrupted() const;
/**
* Return the total size of the zim file.
*
* If zim file is split, return the sum of all parts' size.
*
* @return Size of the size file is KiB.
*/
unsigned int getFileSize() const;
/**
* Get the zim file handler.
*
* @return The libzim file handler.
*/
zim::Archive* getZimArchive() const;
protected:
std::shared_ptr<zim::Archive> zimArchive;
std::string zimFilePath;
SuggestionsList_t suggestions;
SuggestionsList_t::iterator suggestionsOffset;
private:
std::map<const std::string, unsigned int> parseCounterMetadata() const;
// Reader is deprecated, so we've marked the constructor as deprecated.
// But we still need to construct the reader (in our deprecated code)
// To avoid warning because we use deprecated function, we create a
// constructor not deprecated. The `bool marker` is unused, it sole purpose
// is to change the signature to have a different constructor.
// This one is not deprecated and we must use it in our private code.
Reader(const std::shared_ptr<zim::Archive> archive, bool marker);
friend class Library;
};
}
#endif

View File

@@ -27,7 +27,6 @@
namespace kiwix
{
class Searcher;
class NameMapper;
/**
* The SearcherRenderer class is used to render a search result to a html page.
@@ -35,17 +34,6 @@ class NameMapper;
class SearchRenderer
{
public:
/**
* Construct a SearchRenderer from a Searcher.
*
* This method is now deprecated. Construct the renderer from a
* `zim::SearchResultSet`
*
* @param searcher The `Searcher` to render.
* @param mapper The `NameMapper` to use to do the rendering.
*/
DEPRECATED SearchRenderer(Searcher* searcher, NameMapper* mapper);
/**
* Construct a SearchRenderer from a SearchResultSet.
*

View File

@@ -1,180 +0,0 @@
/*
* Copyright 2011 Emmanuel Engelhart <kelson@kiwix.org>
*
* 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_SEARCHER_H
#define KIWIX_SEARCHER_H
#include <stdio.h>
#include <stdlib.h>
#include <unicode/putil.h>
#include <algorithm>
#include <cctype>
#include <locale>
#include <string>
#include <memory>
#include <vector>
#include <zim/search.h>
using namespace std;
namespace kiwix
{
class Reader;
class Result
{
public:
virtual ~Result(){};
virtual std::string get_url() = 0;
virtual std::string get_title() = 0;
virtual int get_score() = 0;
virtual std::string get_snippet() = 0;
virtual std::string get_content() = 0;
virtual int get_wordCount() = 0;
virtual int get_size() = 0;
virtual std::string get_zimId() = 0;
};
struct SearcherInternal;
struct SuggestionInternal;
/**
* The Searcher class is reponsible to do different kind of search using the
* fulltext index.
*
* The Searcher is now deprecated. Use libzim search feature.
*/
class Searcher
{
public:
/**
* The default constructor.
*/
DEPRECATED Searcher();
~Searcher();
/**
* Add a reader (containing embedded fulltext index) to the search.
*
* @param reader The Reader for the zim containing the fulltext index.
* @return true if the reader has been added.
* false if the reader cannot be added (no embedded fulltext index present)
*/
bool add_reader(std::shared_ptr<Reader> reader);
std::shared_ptr<Reader> get_reader(int index);
/**
* Start a search on the zim associated to the Searcher.
*
* Search results should be retrived using the getNextResult method.
*
* @param search The search query.
* @param resultStart the start offset of the search results (used for pagination).
* @param maxResultCount Maximum results to get from start (used for pagination).
* @param verbose print some info on stdout if true.
*/
void search(const std::string& search,
unsigned int resultStart,
unsigned int maxResultCount,
const bool verbose = false);
/**
* Start a geographique search.
* The search return result for entry in a disc of center latitude/longitude
* and radius distance.
*
* Search results should be retrived using the getNextResult method.
*
* @param latitude The latitude of the center point.
* @param longitude The longitude of the center point.
* @param distance The radius of the disc.
* @param resultStart the start offset of the search results (used for pagination).
* @param maxResultCount Maximum number of results to get from start (used for pagination).
* @param verbose print some info on stdout if true.
*/
void geo_search(float latitude, float longitude, float distance,
unsigned int resultStart,
unsigned int maxResultCount,
const bool verbose = false);
/**
* Start a suggestion search.
* The search made depend of the "version" of the embedded index.
* - If the index is newer enough and have a title namespace, the search is
* made in the titles only.
* - Else the search is made on the whole article content.
* In any case, the search is made "partial" (as adding '*' at the end of the query)
*
* @param search The search query.
* @param verbose print some info on stdout if true.
*/
void suggestions(std::string& search, const bool verbose = false);
/**
* Get the next result of a started search.
* This is the method to use to loop hover the search results.
*/
Result* getNextResult();
/**
* Restart the previous search.
* Next call to getNextResult will return the first result.
*/
void restart_search();
/**
* Get a estimation of the result count.
*/
unsigned int getEstimatedResultCount();
/**
* Get a SearchResultSet object for current search
*/
zim::SearchResultSet getSearchResultSet();
unsigned int getResultStart() { return resultStart; }
unsigned int getMaxResultCount() { return maxResultCount; }
protected:
std::string beautifyInteger(const unsigned int number);
void closeIndex();
void searchInIndex(string& search,
const unsigned int resultStart,
const unsigned int maxResultCount,
const bool verbose = false);
std::vector<std::shared_ptr<Reader>> readers;
std::unique_ptr<SearcherInternal> internal;
std::unique_ptr<SuggestionInternal> suggestionInternal;
std::string searchPattern;
unsigned int estimatedResultCount;
unsigned int resultStart;
unsigned int maxResultCount;
private:
void reset();
};
}
#endif

View File

@@ -22,6 +22,7 @@
#include <string>
#include <vector>
#include <iostream>
namespace kiwix
{

View File

@@ -18,7 +18,6 @@
*/
#include "book.h"
#include "reader.h"
#include "tools.h"
#include "tools/base64.h"
@@ -30,7 +29,7 @@
#include "tools/archiveTools.h"
#include <zim/archive.h>
#include <zim/item.h>
#include <pugixml.hpp>
namespace kiwix
@@ -64,11 +63,6 @@ bool Book::update(const kiwix::Book& other)
return true;
}
void Book::update(const kiwix::Reader& reader)
{
update(*reader.getZimArchive());
}
void Book::update(const zim::Archive& archive) {
m_path = archive.getFilename();
m_pathValid = true;

View File

@@ -1,73 +0,0 @@
/*
* Copyright 2018-2020 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.
*/
#include "reader.h"
#include <time.h>
namespace kiwix
{
Entry::Entry(zim::Entry entry, bool _marker)
: entry(entry)
{
}
size_type Entry::getSize() const
{
if (entry.isRedirect()) {
return 0;
} else {
return entry.getItem().getSize();
}
}
std::string Entry::getMimetype() const
{
return entry.getItem(true).getMimetype();
}
bool Entry::isRedirect() const
{
return entry.isRedirect();
}
Entry Entry::getRedirectEntry() const
{
if ( !entry.isRedirect() ) {
throw NoEntry();
}
return Entry(entry.getRedirectEntry(), true);
}
Entry Entry::getFinalEntry() const
{
int loopCounter = 42;
auto final_entry = entry;
while (final_entry.isRedirect() && loopCounter--) {
final_entry = final_entry.getRedirectEntry();
}
// Prevent infinite loops.
if (final_entry.isRedirect()) {
throw NoEntry();
}
return Entry(final_entry, true);
}
}

View File

@@ -19,7 +19,6 @@
#include "library.h"
#include "book.h"
#include "reader.h"
#include "libxml_dumper.h"
#include "tools.h"
@@ -278,16 +277,6 @@ const Book& Library::getBookByPath(const std::string& path) const
throw std::out_of_range(ss.str());
}
std::shared_ptr<Reader> Library::getReaderById(const std::string& id)
{
auto archive = getArchiveById(id);
if(archive) {
return std::shared_ptr<Reader>(new Reader(archive, true));
} else {
return nullptr;
}
}
std::shared_ptr<zim::Archive> Library::getArchiveById(const std::string& id)
{
try {
@@ -473,8 +462,13 @@ void Library::updateBookDB(const Book& book)
indexer.index_text(normalizeText(book.getName()), 1, "XN");
indexer.index_text(normalizeText(book.getCategory()), 1, "XC");
for ( const auto& tag : split(normalizeText(book.getTags()), ";") )
for ( const auto& tag : split(normalizeText(book.getTags()), ";") ) {
doc.add_boolean_term("XT" + tag);
if ( tag[0] != '_' ) {
indexer.increase_termpos();
indexer.index_text(tag);
}
}
const std::string idterm = "Q" + book.getId();
doc.add_boolean_term(idterm);

View File

@@ -6,10 +6,7 @@ kiwix_sources = [
'libxml_dumper.cpp',
'opds_dumper.cpp',
'downloader.cpp',
'reader.cpp',
'entry.cpp',
'server.cpp',
'searcher.cpp',
'search_renderer.cpp',
'subprocess.cpp',
'aria2.cpp',

View File

@@ -22,7 +22,6 @@
#include "kiwixlib-resources.h"
#include <mustache.hpp>
#include <unicode/locid.h>
#include "tools/stringTools.h"
#include "tools/otherTools.h"
@@ -163,14 +162,8 @@ std::once_flag fillLanguagesFlag;
void fillLanguagesMap()
{
for (auto icuLangPtr = icu::Locale::getISOLanguages(); *icuLangPtr != NULL; ++icuLangPtr) {
auto lang = *icuLangPtr;
const icu::Locale locale(lang);
icu::UnicodeString ustring;
locale.getDisplayLanguage(locale, ustring);
std::string displayLanguage;
ustring.toUTF8String(displayLanguage);
std::string iso3LangCode = locale.getISO3Language();
iso639_3.insert({iso3LangCode, displayLanguage});
const ICULanguageInfo lang(*icuLangPtr);
iso639_3.insert({lang.iso3Code(), lang.selfName()});
}
}

View File

@@ -1,472 +0,0 @@
/*
* Copyright 2011 Emmanuel Engelhart <kelson@kiwix.org>
*
* 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.
*/
#include "reader.h"
#include <time.h>
#include <zim/search.h>
#include <zim/suggestion.h>
#include <zim/item.h>
#include <zim/error.h>
#include "tools.h"
#include "tools/stringTools.h"
#include "tools/otherTools.h"
#include "tools/archiveTools.h"
namespace kiwix
{
/* Constructor */
Reader::Reader(const string zimFilePath)
: zimArchive(nullptr),
zimFilePath(zimFilePath)
{
string tmpZimFilePath = zimFilePath;
/* Remove potential trailing zimaa */
size_t found = tmpZimFilePath.rfind("zimaa");
if (found != string::npos && tmpZimFilePath.size() > 5
&& found == tmpZimFilePath.size() - 5) {
tmpZimFilePath.resize(tmpZimFilePath.size() - 2);
}
zimArchive.reset(new zim::Archive(tmpZimFilePath));
/* initialize random seed: */
srand(time(nullptr));
}
Reader::Reader(const std::shared_ptr<zim::Archive> archive, bool _marker)
: zimArchive(archive),
zimFilePath(archive->getFilename())
{}
#ifndef _WIN32
Reader::Reader(int fd)
: zimArchive(new zim::Archive(fd)),
zimFilePath("")
{
/* initialize random seed: */
srand(time(nullptr));
}
Reader::Reader(int fd, zim::offset_type offset, zim::size_type size)
: zimArchive(new zim::Archive(fd, offset, size)),
zimFilePath("")
{
/* initialize random seed: */
srand(time(nullptr));
}
#endif // #ifndef _WIN32
zim::Archive* Reader::getZimArchive() const
{
return zimArchive.get();
}
MimeCounterType Reader::parseCounterMetadata() const
{
return kiwix::parseArchiveCounter(*zimArchive);
}
/* Get the count of articles which can be indexed/displayed */
unsigned int Reader::getArticleCount() const
{
std::map<const std::string, unsigned int> counterMap
= this->parseCounterMetadata();
unsigned int counter = 0;
for(auto &pair:counterMap) {
if (startsWith(pair.first, "text/html")) {
counter += pair.second;
}
}
return counter;
}
/* Get the count of medias content in the ZIM file */
unsigned int Reader::getMediaCount() const
{
return kiwix::getArchiveMediaCount(*zimArchive);
}
/* Get the total of all items of a ZIM file, redirects included */
unsigned int Reader::getGlobalCount() const
{
return zimArchive->getEntryCount();
}
/* Return the UID of the ZIM file */
string Reader::getId() const
{
return kiwix::getArchiveId(*zimArchive);
}
Entry Reader::getRandomPage() const
{
try {
return Entry(zimArchive->getRandomEntry(), true);
} catch(...) {
throw NoEntry();
}
}
Entry Reader::getMainPage() const
{
return Entry(zimArchive->getMainEntry(), true);
}
bool Reader::getFavicon(string& content, string& mimeType) const
{
return kiwix::getArchiveFavicon(*zimArchive, 48, content, mimeType);
}
string Reader::getZimFilePath() const
{
return zimFilePath;
}
/* Return a metatag value */
bool Reader::getMetadata(const string& name, string& value) const
{
try {
value = zimArchive->getMetadata(name);
return true;
} catch(zim::EntryNotFound& e) {
return false;
}
}
#define METADATA(NAME) std::string v; getMetadata(NAME, v); return v;
string Reader::getName() const
{
return kiwix::getMetaName(*zimArchive);
}
string Reader::getTitle() const
{
return kiwix::getArchiveTitle(*zimArchive);
}
string Reader::getCreator() const
{
return kiwix::getMetaCreator(*zimArchive);
}
string Reader::getPublisher() const
{
return kiwix::getMetaPublisher(*zimArchive);
}
string Reader::getDate() const
{
return kiwix::getMetaDate(*zimArchive);
}
string Reader::getDescription() const
{
return kiwix::getMetaDescription(*zimArchive);
}
string Reader::getLongDescription() const
{
METADATA("LongDescription")
}
string Reader::getLanguage() const
{
return kiwix::getMetaLanguage(*zimArchive);
}
string Reader::getLicense() const
{
METADATA("License")
}
string Reader::getTags(bool original) const
{
return kiwix::getMetaTags(*zimArchive, original);
}
string Reader::getTagStr(const std::string& tagName) const
{
string tags_str;
getMetadata("Tags", tags_str);
return getTagValueFromTagList(convertTags(tags_str), tagName);
}
bool Reader::getTagBool(const std::string& tagName) const
{
return convertStrToBool(getTagStr(tagName));
}
string Reader::getRelation() const
{
METADATA("Relation")
}
string Reader::getFlavour() const
{
return kiwix::getMetaFlavour(*zimArchive);
}
string Reader::getSource() const
{
METADATA("Source")
}
string Reader::getScraper() const
{
METADATA("Scraper")
}
#undef METADATA
Entry Reader::getEntryFromPath(const std::string& path) const
{
try {
return Entry(kiwix::getEntryFromPath(*zimArchive, path), true);
} catch (zim::EntryNotFound& e) {
throw NoEntry();
}
}
Entry Reader::getEntryFromEncodedPath(const std::string& path) const
{
return getEntryFromPath(urlDecode(path, true));
}
Entry Reader::getEntryFromTitle(const std::string& title) const
{
try {
return Entry(zimArchive->getEntryByTitle(title), true);
} catch(zim::EntryNotFound& e) {
throw NoEntry();
}
}
bool Reader::pathExists(const string& path) const
{
return zimArchive->hasEntryByPath(path);
}
/* Does the ZIM file has a fulltext index */
bool Reader::hasFulltextIndex() const
{
return zimArchive->hasFulltextIndex();
}
/* Search titles by prefix */
bool Reader::searchSuggestions(const string& prefix,
unsigned int suggestionsCount,
const bool reset)
{
/* Reset the suggestions otherwise check if the suggestions number is less
* than the suggestionsCount */
if (reset) {
this->suggestions.clear();
this->suggestionsOffset = this->suggestions.begin();
} else {
if (this->suggestions.size() > suggestionsCount) {
return false;
}
}
auto ret = searchSuggestions(prefix, suggestionsCount, this->suggestions);
/* Set the cursor to the begining */
this->suggestionsOffset = this->suggestions.begin();
return ret;
}
bool Reader::searchSuggestions(const string& prefix,
unsigned int suggestionsCount,
SuggestionsList_t& results)
{
bool retVal = false;
/* Return if no prefix */
if (prefix.size() == 0) {
return false;
}
for (auto& entry: zimArchive->findByTitle(prefix)) {
if (results.size() >= suggestionsCount) {
break;
}
/* Extract the interesting part of article title & url */
std::string normalizedArticleTitle
= kiwix::normalize(entry.getTitle());
// Get the final path.
auto item = entry.getItem(true);
std::string articleFinalUrl = item.getPath();
/* Go through all already found suggestions and skip if this
article is already in the suggestions list (with an other
title) */
bool insert = true;
std::vector<SuggestionItem>::iterator suggestionItr;
for (suggestionItr = results.begin();
suggestionItr != results.end();
suggestionItr++) {
int result = normalizedArticleTitle.compare((*suggestionItr).getNormalizedTitle());
if (result == 0 && articleFinalUrl.compare((*suggestionItr).getPath()) == 0) {
insert = false;
break;
} else if (result < 0) {
break;
}
}
/* Insert if possible */
if (insert) {
SuggestionItem suggestion(entry.getTitle(), normalizedArticleTitle, articleFinalUrl);
results.insert(suggestionItr, suggestion);
}
/* Suggestions where found */
retVal = true;
}
return retVal;
}
std::vector<std::string> Reader::getTitleVariants(
const std::string& title) const
{
return kiwix::getTitleVariants(title);
}
bool Reader::searchSuggestionsSmart(const string& prefix,
unsigned int suggestionsCount)
{
this->suggestions.clear();
this->suggestionsOffset = this->suggestions.begin();
auto ret = searchSuggestionsSmart(prefix, suggestionsCount, this->suggestions);
this->suggestionsOffset = this->suggestions.begin();
return ret;
}
/* Try also a few variations of the prefix to have better results */
bool Reader::searchSuggestionsSmart(const string& prefix,
unsigned int suggestionsCount,
SuggestionsList_t& results)
{
std::vector<std::string> variants = this->getTitleVariants(prefix);
auto suggestionSearcher = zim::SuggestionSearcher(*zimArchive);
if (zimArchive->hasTitleIndex()) {
auto suggestionSearch = suggestionSearcher.suggest(prefix);
const auto suggestions = suggestionSearch.getResults(0, suggestionsCount);
for (auto current : suggestions) {
SuggestionItem suggestion(current.getTitle(), kiwix::normalize(current.getTitle()),
current.getPath(), current.getSnippet());
results.push_back(suggestion);
}
} else {
// Check some of the variants of the prefix
for (std::vector<std::string>::iterator variantsItr = variants.begin();
variantsItr != variants.end();
variantsItr++) {
auto suggestionSearch = suggestionSearcher.suggest(*variantsItr);
for (auto current : suggestionSearch.getResults(0, suggestionsCount)) {
if (results.size() >= suggestionsCount) {
break;
}
SuggestionItem suggestion(current.getTitle(), kiwix::normalize(current.getTitle()),
current.getPath(), current.getSnippet());
results.push_back(suggestion);
}
}
}
return results.size() > 0;
}
/* Get next suggestion */
bool Reader::getNextSuggestion(string& title)
{
if (this->suggestionsOffset != this->suggestions.end()) {
/* title */
title = (*(this->suggestionsOffset)).getTitle();
/* increment the cursor for the next call */
this->suggestionsOffset++;
return true;
}
return false;
}
bool Reader::getNextSuggestion(string& title, string& url)
{
if (this->suggestionsOffset != this->suggestions.end()) {
/* title */
title = (*(this->suggestionsOffset)).getTitle();
url = (*(this->suggestionsOffset)).getPath();
/* increment the cursor for the next call */
this->suggestionsOffset++;
return true;
}
return false;
}
/* Check if the file has as checksum */
bool Reader::canCheckIntegrity() const
{
return zimArchive->hasChecksum();
}
/* Return true if corrupted, false otherwise */
bool Reader::isCorrupted() const
{
try {
if (zimArchive->check() == true) {
return false;
}
} catch (exception& e) {
cerr << e.what() << endl;
return true;
}
return true;
}
/* Return the file size, works also for splitted files */
unsigned int Reader::getFileSize() const
{
return kiwix::getArchiveFileSize(*zimArchive);
}
}

View File

@@ -21,8 +21,6 @@
#include <cmath>
#include "search_renderer.h"
#include "searcher.h"
#include "reader.h"
#include "library.h"
#include "name_mapper.h"
@@ -38,16 +36,6 @@ namespace kiwix
{
/* Constructor */
SearchRenderer::SearchRenderer(Searcher* searcher, NameMapper* mapper)
: SearchRenderer(
/* srs */ searcher->getSearchResultSet(),
/* mapper */ mapper,
/* library */ nullptr,
/* start */ searcher->getResultStart(),
/* estimatedResultCount */ searcher->getEstimatedResultCount()
)
{}
SearchRenderer::SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper,
unsigned int start, unsigned int estimatedResultCount)
: SearchRenderer(srs, mapper, nullptr, start, estimatedResultCount)
@@ -142,7 +130,7 @@ kainjow::mustache::data buildPagination(
auto nbPages = lastPage + 1;
auto firstPageGenerated = currentPage > 4 ? currentPage-4 : 0;
auto lastPageGenerated = min(currentPage+4, lastPage);
auto lastPageGenerated = std::min(currentPage+4, lastPage);
if (nbPages != 1) {
if (firstPageGenerated!=0) {
@@ -178,13 +166,14 @@ kainjow::mustache::data buildPagination(
std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
{
const std::string absPathPrefix = protocolPrefix + "content/";
// Build the results list
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());
result.set("title", it.getTitle());
result.set("absolutePath", protocolPrefix + urlEncode(mp_nameMapper->getNameForId(zim_id), true) + "/" + urlEncode(it.getPath()));
result.set("absolutePath", absPathPrefix + urlEncode(mp_nameMapper->getNameForId(zim_id), true) + "/" + urlEncode(it.getPath()));
result.set("snippet", it.getSnippet());
if (mp_library) {
result.set("bookTitle", mp_library->getBookById(zim_id).getTitle());
@@ -200,7 +189,7 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
results.set("count", kiwix::beautifyInteger(estimatedResultCount));
results.set("hasResults", estimatedResultCount != 0);
results.set("start", kiwix::beautifyInteger(resultStart));
results.set("end", kiwix::beautifyInteger(min(resultStart+pageLength-1, estimatedResultCount)));
results.set("end", kiwix::beautifyInteger(std::min(resultStart+pageLength-1, estimatedResultCount)));
// pagination
auto pagination = buildPagination(

View File

@@ -1,330 +0,0 @@
/*
* Copyright 2011 Emmanuel Engelhart <kelson@kiwix.org>
*
* 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.
*/
#include "searcher.h"
#include "reader.h"
#include <zim/search.h>
#include <zim/suggestion.h>
#include <mustache.hpp>
#include <cmath>
#include "tools/stringTools.h"
#include "kiwixlib-resources.h"
#define MAX_SEARCH_LEN 140
namespace kiwix
{
class _Result : public Result
{
public:
_Result(zim::SearchResultSet::iterator iterator);
_Result(SuggestionItem suggestionItem);
virtual ~_Result(){};
virtual std::string get_url();
virtual std::string get_title();
virtual int get_score();
virtual std::string get_snippet();
virtual std::string get_content();
virtual int get_wordCount();
virtual int get_size();
virtual std::string get_zimId();
private:
zim::SearchResultSet::iterator iterator;
SuggestionItem suggestionItem;
bool isSuggestion;
};
struct SearcherInternal : zim::SearchResultSet {
explicit SearcherInternal(const zim::SearchResultSet& srs)
: zim::SearchResultSet(srs)
, current_iterator(srs.begin())
{
}
zim::SearchResultSet::iterator current_iterator;
};
struct SuggestionInternal : zim::SuggestionResultSet {
explicit SuggestionInternal(const zim::SuggestionResultSet& srs)
: zim::SuggestionResultSet(srs),
currentIterator(srs.begin()) {}
zim::SuggestionResultSet::iterator currentIterator;
};
/* Constructor */
Searcher::Searcher()
: searchPattern(""),
estimatedResultCount(0),
resultStart(0),
maxResultCount(0)
{
loadICUExternalTables();
}
/* Destructor */
Searcher::~Searcher()
{
}
bool Searcher::add_reader(std::shared_ptr<Reader> reader)
{
if (!reader->hasFulltextIndex()) {
return false;
}
for ( auto existing_reader : readers ) {
if ( existing_reader->getZimArchive()->getUuid() == reader->getZimArchive()->getUuid() )
return false;
}
this->readers.push_back(reader);
return true;
}
std::shared_ptr<Reader> Searcher::get_reader(int readerIndex)
{
return readers.at(readerIndex);
}
/* Search strings in the database */
void Searcher::search(const std::string& search,
unsigned int resultStart,
unsigned int maxResultCount,
const bool verbose)
{
this->reset();
if (verbose == true) {
cout << "Performing query `" << search << "'" << endl;
}
this->searchPattern = search;
this->resultStart = resultStart;
this->maxResultCount = maxResultCount;
/* Try to find results */
if (maxResultCount != 0) {
/* Perform the search */
string unaccentedSearch = removeAccents(search);
std::vector<zim::Archive> archives;
for (auto current = this->readers.begin(); current != this->readers.end();
current++) {
if ( (*current)->hasFulltextIndex() ) {
archives.push_back(*(*current)->getZimArchive());
}
}
zim::Searcher searcher(archives);
searcher.setVerbose(verbose);
zim::Query query;
query.setQuery(unaccentedSearch);
zim::Search search = searcher.search(query);
internal.reset(new SearcherInternal(search.getResults(resultStart, maxResultCount)));
this->estimatedResultCount = search.getEstimatedMatches();
}
return;
}
void Searcher::geo_search(float latitude, float longitude, float distance,
unsigned int resultStart,
unsigned int maxResultCount,
const bool verbose)
{
this->reset();
if (verbose == true) {
cout << "Performing geo query `" << distance << "&(" << latitude << ";" << longitude << ")'" << endl;
}
/* Perform the search */
std::ostringstream oss;
oss << "Articles located less than " << distance << " meters of " << latitude << ";" << longitude;
this->searchPattern = oss.str();
this->resultStart = resultStart;
this->maxResultCount = maxResultCount;
/* Try to find results */
if (maxResultCount == 0) {
return;
}
std::vector<zim::Archive> archives;
for (auto current = this->readers.begin(); current != this->readers.end();
current++) {
archives.push_back(*(*current)->getZimArchive());
}
zim::Searcher searcher(archives);
searcher.setVerbose(verbose);
zim::Query query;
query.setQuery("");
query.setGeorange(latitude, longitude, distance);
zim::Search search = searcher.search(query);
internal.reset(new SearcherInternal(search.getResults(resultStart, maxResultCount)));
this->estimatedResultCount = search.getEstimatedMatches();
}
void Searcher::restart_search()
{
if (internal.get()) {
internal->current_iterator = internal->begin();
}
}
Result* Searcher::getNextResult()
{
if (internal.get() && internal->current_iterator != internal->end()) {
Result* result = new _Result(internal->current_iterator);
internal->current_iterator++;
return result;
} else if (suggestionInternal.get() &&
suggestionInternal->currentIterator != suggestionInternal->end()) {
SuggestionItem item(
suggestionInternal->currentIterator->getTitle(),
normalize(suggestionInternal->currentIterator->getTitle()),
suggestionInternal->currentIterator->getPath(),
suggestionInternal->currentIterator->getSnippet()
);
Result* result = new _Result(item);
suggestionInternal->currentIterator++;
return result;
}
return NULL;
}
/* Reset the results */
void Searcher::reset()
{
this->estimatedResultCount = 0;
this->searchPattern = "";
return;
}
void Searcher::suggestions(std::string& searchPattern, const bool verbose)
{
this->reset();
if (verbose == true) {
cout << "Performing suggestion query `" << searchPattern << "`" << endl;
}
this->searchPattern = searchPattern;
this->resultStart = 0;
this->maxResultCount = 10;
string unaccentedSearch = removeAccents(searchPattern);
// Multizim suggestion is not supported as of now! taking only one archive
zim::Archive archive = *(*this->readers.begin())->getZimArchive();
zim::SuggestionSearcher searcher(archive);
searcher.setVerbose(verbose);
zim::SuggestionSearch search = searcher.suggest(searchPattern);
suggestionInternal.reset(new SuggestionInternal(search.getResults(resultStart, maxResultCount)));
this->estimatedResultCount = search.getEstimatedMatches();
}
/* Return the result count estimation */
unsigned int Searcher::getEstimatedResultCount()
{
return this->estimatedResultCount;
}
zim::SearchResultSet Searcher::getSearchResultSet()
{
return *(this->internal);
}
_Result::_Result(zim::SearchResultSet::iterator iterator)
: iterator(iterator),
suggestionItem("", "", ""),
isSuggestion(false)
{}
_Result::_Result(SuggestionItem item)
: iterator(),
suggestionItem(item.getTitle(), item.getNormalizedTitle(), item.getPath(), item.getSnippet()),
isSuggestion(true)
{}
std::string _Result::get_url()
{
if (isSuggestion) {
return suggestionItem.getPath();
}
return iterator.getPath();
}
std::string _Result::get_title()
{
if (isSuggestion) {
return suggestionItem.getTitle();
}
return iterator.getTitle();
}
int _Result::get_score()
{
if (isSuggestion) {
return 0;
}
return iterator.getScore();
}
std::string _Result::get_snippet()
{
if (isSuggestion) {
return suggestionItem.getSnippet();
}
return iterator.getSnippet();
}
std::string _Result::get_content()
{
if (isSuggestion) return "";
return iterator->getItem(true).getData();
}
int _Result::get_size()
{
if (isSuggestion) {
return 0;
}
return iterator.getSize();
}
int _Result::get_wordCount()
{
if (isSuggestion) {
return 0;
}
return iterator.getWordCount();
}
std::string _Result::get_zimId()
{
if (isSuggestion) {
return "";
}
std::ostringstream s;
s << iterator.getZimId();
return s.str();
}
}

View File

@@ -24,6 +24,7 @@
#include <string>
#include <zim/item.h>
#include "server/internalServer.h"
namespace kiwix {

View File

@@ -51,8 +51,6 @@ extern "C" {
#include "tools/networkTools.h"
#include "library.h"
#include "name_mapper.h"
#include "entry.h"
#include "searcher.h"
#include "search_renderer.h"
#include "opds_dumper.h"
#include "i18n.h"
@@ -61,6 +59,7 @@ extern "C" {
#include <zim/error.h>
#include <zim/entry.h>
#include <zim/item.h>
#include <zim/suggestion.h>
#include <mustache.hpp>
@@ -68,6 +67,7 @@ extern "C" {
#include <string>
#include <vector>
#include <chrono>
#include <fstream>
#include "kiwixlib-resources.h"
#ifndef _WIN32
@@ -212,6 +212,12 @@ void checkBookNumber(const Library::BookIdSet& bookIds, size_t limit) {
}
}
struct CustomizedResourceData
{
std::string mimeType;
std::string resourceFilePath;
};
} // unnamed namespace
std::pair<std::string, Library::BookIdSet> InternalServer::selectBooks(const RequestContext& request) const
@@ -327,7 +333,6 @@ zim::Query SearchInfo::getZimQuery(bool verbose) const {
return query;
}
static IdNameMapper defaultNameMapper;
static MHD_Result staticHandlerCallback(void* cls,
@@ -339,6 +344,27 @@ static MHD_Result staticHandlerCallback(void* cls,
size_t* upload_data_size,
void** cont_cls);
class InternalServer::CustomizedResources : public std::map<std::string, CustomizedResourceData>
{
public:
CustomizedResources()
{
const char* fname = ::getenv("KIWIX_SERVE_CUSTOMIZED_RESOURCES");
if ( fname )
{
std::cout << "Populating customized resources" << std::endl;
std::ifstream file(fname);
std::string url, mimeType, resourceFilePath;
while ( file >> url >> mimeType >> resourceFilePath )
{
std::cout << "Got " << url << " " << mimeType << " " << resourceFilePath << std::endl;
(*this)[url] = CustomizedResourceData{mimeType, resourceFilePath};
}
std::cout << "Done populating customized resources" << std::endl;
}
}
};
InternalServer::InternalServer(Library* library,
NameMapper* nameMapper,
@@ -368,9 +394,12 @@ InternalServer::InternalServer(Library* library,
mp_library(library),
mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper),
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)))
suggestionSearcherCache(getEnvVar<int>("KIWIX_SUGGESTION_SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U))),
m_customizedResources(new CustomizedResources)
{}
InternalServer::~InternalServer() = default;
bool InternalServer::start() {
#ifdef _WIN32
int flags = MHD_USE_SELECT_INTERNALLY;
@@ -495,6 +524,16 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
return ret;
}
namespace
{
bool isEndpointUrl(const std::string& url, const std::string& endpoint)
{
return startsWith(url, "/" + endpoint + "/") || url == "/" + endpoint;
};
} // unnamed namespace
std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& request)
{
try {
@@ -507,36 +546,45 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
if ( etag )
return Response::build_304(*this, etag);
if (startsWith(request.get_url(), "/skin/"))
const auto url = request.get_url();
if ( isLocallyCustomizedResource(url) )
return handle_locally_customized_resource(request);
if (url == "/" )
return build_homepage(request);
if (isEndpointUrl(url, "skin"))
return handle_skin(request);
if (startsWith(request.get_url(), "/catalog/"))
if (isEndpointUrl(url, "content"))
return handle_content(request);
if (isEndpointUrl(url, "catalog"))
return handle_catalog(request);
if (startsWith(request.get_url(), "/raw/"))
if (isEndpointUrl(url, "raw"))
return handle_raw(request);
if (request.get_url() == "/search")
if (isEndpointUrl(url, "search"))
return handle_search(request);
if (request.get_url() == "/search/searchdescription.xml") {
return ContentResponse::build(
*this,
RESOURCE::ft_opensearchdescription_xml,
get_default_data(),
"application/opensearchdescription+xml");
}
if (request.get_url() == "/suggest")
if (isEndpointUrl(url, "suggest"))
return handle_suggest(request);
if (request.get_url() == "/random")
if (isEndpointUrl(url, "random"))
return handle_random(request);
if (request.get_url() == "/catch/external")
return handle_captured_external(request);
if (isEndpointUrl(url, "catch"))
return handle_catch(request);
return handle_content(request);
if (isEndpointUrl(url, "widget"))
return handle_widget(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);
} catch (std::exception& e) {
fprintf(stderr, "===== Unhandled error : %s\n", e.what());
return HTTP500Response(*this, request)
@@ -585,46 +633,17 @@ std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& r
* Archive and Zim handlers begin
**/
SuggestionsList_t getSuggestions(SuggestionSearcherCache& cache, const zim::Archive* const archive,
const std::string& bookId, const std::string& queryString, int start, int suggestionCount)
{
SuggestionsList_t suggestions;
std::shared_ptr<zim::SuggestionSearcher> searcher;
searcher = cache.getOrPut(bookId, [=](){ return make_shared<zim::SuggestionSearcher>(*archive); });
if (archive->hasTitleIndex()) {
auto search = searcher->suggest(queryString);
auto srs = search.getResults(start, suggestionCount);
for (auto it : srs) {
SuggestionItem suggestion(it.getTitle(), kiwix::normalize(it.getTitle()),
it.getPath(), it.getSnippet());
suggestions.push_back(suggestion);
}
} else {
// TODO: This case should be handled by libzim
std::vector<std::string> variants = getTitleVariants(queryString);
int currCount = 0;
for (auto it = variants.begin(); it != variants.end() && currCount < suggestionCount; it++) {
auto search = searcher->suggest(queryString);
auto srs = search.getResults(0, suggestionCount);
for (auto it : srs) {
SuggestionItem suggestion(it.getTitle(), kiwix::normalize(it.getTitle()),
it.getPath());
suggestions.push_back(suggestion);
currCount++;
}
}
}
return suggestions;
}
std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& request)
{
if (m_verbose.load()) {
printf("** running handle_suggest\n");
}
if ( startsWith(request.get_url(), "/suggest/") ) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
std::string bookName, bookId;
std::shared_ptr<zim::Archive> archive;
try {
@@ -657,9 +676,13 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
bool first = true;
/* Get the suggestions */
SuggestionsList_t suggestions = getSuggestions(suggestionSearcherCache, archive.get(),
bookId, queryString, start, count);
for(auto& suggestion:suggestions) {
auto searcher = suggestionSearcherCache.getOrPut(bookId,
[=](){ return make_shared<zim::SuggestionSearcher>(*archive); }
);
auto search = searcher->suggest(queryString);
auto srs = search.getResults(start, count);
for(auto& suggestion: srs) {
MustacheData result;
result.set("label", suggestion.getTitle());
@@ -720,6 +743,18 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
printf("** running handle_search\n");
}
if ( startsWith(request.get_url(), "/search/") ) {
if (request.get_url() == "/search/searchdescription.xml") {
return ContentResponse::build(
*this,
RESOURCE::ft_opensearchdescription_xml,
get_default_data(),
"application/opensearchdescription+xml");
}
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
try {
auto searchInfo = getSearchInfo(request);
auto bookIds = searchInfo.getBookIds();
@@ -803,6 +838,11 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
printf("** running handle_random\n");
}
if ( startsWith(request.get_url(), "/random/") ) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
std::string bookName;
std::shared_ptr<zim::Archive> archive;
try {
@@ -829,6 +869,11 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
}
}
std::unique_ptr<Response> InternalServer::handle_widget(const RequestContext& request)
{
return ContentResponse::build(*this, RESOURCE::templates::widget_html, get_default_data(), "text/html; charset=utf-8", true);
}
std::unique_ptr<Response> InternalServer::handle_captured_external(const RequestContext& request)
{
std::string source = "";
@@ -846,6 +891,20 @@ std::unique_ptr<Response> InternalServer::handle_captured_external(const Request
return ContentResponse::build(*this, RESOURCE::templates::captured_external_html, data, "text/html; charset=utf-8");
}
std::unique_ptr<Response> InternalServer::handle_catch(const RequestContext& request)
{
if (m_verbose.load()) {
printf("** running handle_catch\n");
}
if ( request.get_url() == "/catch/external" ) {
return handle_captured_external(request);
}
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& request)
{
if (m_verbose.load()) {
@@ -917,15 +976,6 @@ InternalServer::search_catalog(const RequestContext& request,
namespace
{
std::string get_book_name(const RequestContext& request)
{
try {
return request.get_url_part(0);
} catch (const std::out_of_range& e) {
return std::string();
}
}
ParameterizedMessage suggestSearchMsg(const std::string& searchURL, const std::string& pattern)
{
return ParameterizedMessage("suggest-search",
@@ -940,7 +990,8 @@ ParameterizedMessage suggestSearchMsg(const std::string& searchURL, const std::s
std::unique_ptr<Response>
InternalServer::build_redirect(const std::string& bookName, const zim::Item& item) const
{
auto redirectUrl = m_root + "/" + bookName + "/" + kiwix::urlEncode(item.getPath());
const auto path = kiwix::urlEncode(item.getPath());
const auto redirectUrl = m_root + "/content/" + bookName + "/" + path;
return Response::build_redirect(*this, redirectUrl);
}
@@ -952,9 +1003,10 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
printf("** running handle_content\n");
}
const std::string bookName = get_book_name(request);
if (bookName.empty())
return build_homepage(request);
const std::string contentPrefix = "/content/";
const bool isContentPrefixedUrl = startsWith(url, contentPrefix);
const size_t prefixLength = isContentPrefixedUrl ? contentPrefix.size() : 1;
const std::string bookName = request.get_url_part(isContentPrefixedUrl);
std::shared_ptr<zim::Archive> archive;
try {
@@ -970,7 +1022,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
+ TaskbarInfo(bookName);
}
auto urlStr = request.get_url().substr(bookName.size()+1);
auto urlStr = url.substr(prefixLength + bookName.size());
if (urlStr[0] == '/') {
urlStr = urlStr.substr(1);
}
@@ -1067,4 +1119,34 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
}
}
bool InternalServer::isLocallyCustomizedResource(const std::string& url) const
{
return m_customizedResources->find(url) != m_customizedResources->end();
}
std::unique_ptr<Response> InternalServer::handle_locally_customized_resource(const RequestContext& request)
{
if (m_verbose.load()) {
printf("** running handle_locally_customized_resource\n");
}
const CustomizedResourceData& crd = m_customizedResources->at(request.get_url());
if (m_verbose.load()) {
std::cout << "Reading " << crd.resourceFilePath << std::endl;
}
const auto resourceData = getFileContent(crd.resourceFilePath);
auto byteRange = request.get_range().resolve(resourceData.size());
if (byteRange.kind() != ByteRange::RESOLVED_FULL_CONTENT) {
return Response::build_416(*this, resourceData.size());
}
return ContentResponse::build(*this,
resourceData,
crd.mimeType,
/*isHomePage=*/false,
/*raw=*/true);
}
}

View File

@@ -89,9 +89,8 @@ class SearchInfo {
typedef kainjow::mustache::data MustacheData;
typedef ConcurrentCache<SearchInfo, std::shared_ptr<zim::Search>> SearchCache;
typedef ConcurrentCache<string, std::shared_ptr<zim::SuggestionSearcher>> SuggestionSearcherCache;
typedef ConcurrentCache<std::string, std::shared_ptr<zim::SuggestionSearcher>> SuggestionSearcherCache;
class Entry;
class OPDSDumper;
class InternalServer {
@@ -109,7 +108,7 @@ class InternalServer {
bool blockExternalLinks,
std::string indexTemplateString,
int ipConnectionLimit);
virtual ~InternalServer() = default;
virtual ~InternalServer();
MHD_Result handlerCallback(struct MHD_Connection* connection,
const char* url,
@@ -139,9 +138,12 @@ class InternalServer {
std::unique_ptr<Response> handle_search(const RequestContext& request);
std::unique_ptr<Response> handle_suggest(const RequestContext& request);
std::unique_ptr<Response> handle_random(const RequestContext& request);
std::unique_ptr<Response> handle_catch(const RequestContext& request);
std::unique_ptr<Response> handle_captured_external(const RequestContext& request);
std::unique_ptr<Response> handle_content(const RequestContext& request);
std::unique_ptr<Response> handle_raw(const RequestContext& request);
std::unique_ptr<Response> handle_locally_customized_resource(const RequestContext& request);
std::unique_ptr<Response> handle_widget(const RequestContext& request);
std::vector<std::string> search_catalog(const RequestContext& request,
kiwix::OPDSDumper& opdsDumper);
@@ -153,6 +155,8 @@ class InternalServer {
std::pair<std::string, Library::BookIdSet> selectBooks(const RequestContext& r) const;
SearchInfo getSearchInfo(const RequestContext& r) const;
bool isLocallyCustomizedResource(const std::string& url) const;
private: // data
std::string m_addr;
int m_port;
@@ -176,6 +180,9 @@ class InternalServer {
std::string m_server_id;
std::string m_library_id;
class CustomizedResources;
std::unique_ptr<CustomizedResources> m_customizedResources;
friend std::unique_ptr<Response> Response::build(const InternalServer& server);
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage, bool raw);
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item, bool raw);

View File

@@ -154,8 +154,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const Requ
std::unique_ptr<Response> InternalServer::handle_catalog_v2_illustration(const RequestContext& request)
{
try {
const auto bookName = request.get_url_part(3);
const auto bookId = mp_nameMapper->getIdForName(bookName);
const auto bookId = request.get_url_part(3);
auto book = mp_library->getBookByIdThreadSafe(bookId);
auto size = request.get_argument<unsigned int>("size");
auto illustration = book.getIllustration(size);

View File

@@ -53,18 +53,18 @@ std::string get_mime_type(const zim::Item& item)
{
try {
return item.getMimetype();
} catch (exception& e) {
} catch (std::exception& e) {
return "application/octet-stream";
}
}
bool is_compressible_mime_type(const std::string& mimeType)
{
return mimeType.find("text/") != string::npos
|| mimeType.find("application/javascript") != string::npos
|| mimeType.find("application/atom") != string::npos
|| mimeType.find("application/opensearchdescription") != string::npos
|| mimeType.find("application/json") != string::npos;
return mimeType.find("text/") != std::string::npos
|| 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;
}
bool compress(std::string &content) {
@@ -307,7 +307,7 @@ static ssize_t callback_reader_from_item(void* cls,
{
RunningResponse* response = static_cast<RunningResponse*>(cls);
size_t max_size_to_set = min<size_t>(
size_t max_size_to_set = std::min<size_t>(
max,
response->item.getSize() - pos - response->range_start);

View File

@@ -26,10 +26,11 @@
#include <mustache.hpp>
#include "byte_range.h"
#include "entry.h"
#include "etag.h"
#include "i18n.h"
#include <zim/item.h>
extern "C" {
#include "microhttpd_wrapper.h"
}

View File

@@ -49,6 +49,24 @@ void kiwix::loadICUExternalTables()
#endif
}
kiwix::ICULanguageInfo::ICULanguageInfo(const std::string& langCode)
: locale(langCode.c_str())
{}
std::string kiwix::ICULanguageInfo::iso3Code() const
{
return locale.getISO3Language();
}
std::string kiwix::ICULanguageInfo::selfName() const
{
icu::UnicodeString langSelfNameICUString;
locale.getDisplayLanguage(locale, langSelfNameICUString);
std::string langSelfName;
langSelfNameICUString.toUTF8String(langSelfName);
return langSelfName;
}
std::string kiwix::removeAccents(const std::string& text)
{
loadICUExternalTables();

View File

@@ -21,6 +21,7 @@
#define KIWIX_STRINGTOOLS_H
#include <unicode/unistr.h>
#include <unicode/locid.h>
#include <string>
#include <vector>
@@ -41,6 +42,19 @@ std::string encodeDiples(const std::string& str);
std::string removeAccents(const std::string& text);
void loadICUExternalTables();
class ICULanguageInfo
{
public:
explicit ICULanguageInfo(const std::string& langCode);
std::string iso3Code() const;
std::string selfName() const;
private:
const icu::Locale locale;
};
std::string urlEncode(const std::string& value, bool encodeReserved = false);
std::string urlDecode(const std::string& value, bool component = false);

View File

@@ -1,22 +1,3 @@
skin/jquery-ui/jquery-ui.structure.min.css
skin/jquery-ui/jquery-ui.min.js
skin/jquery-ui/external/jquery/jquery.js
skin/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png
skin/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png
skin/jquery-ui/images/ui-icons_222222_256x240.png
skin/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png
skin/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png
skin/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png
skin/jquery-ui/images/ui-icons_2e83ff_256x240.png
skin/jquery-ui/images/ui-icons_cd0a0a_256x240.png
skin/jquery-ui/images/ui-icons_888888_256x240.png
skin/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png
skin/jquery-ui/images/animated-overlay.gif
skin/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png
skin/jquery-ui/images/ui-icons_454545_256x240.png
skin/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png
skin/jquery-ui/jquery-ui.theme.min.css
skin/jquery-ui/jquery-ui.min.css
skin/caret.png
skin/bittorrent.png
skin/magnet.png
@@ -27,6 +8,8 @@ skin/taskbar.js
skin/iso6391To3.js
skin/isotope.pkgd.min.js
skin/index.js
skin/autoComplete.min.js
skin/widget.js
skin/taskbar.css
skin/index.css
skin/fonts/Poppins.ttf
@@ -38,6 +21,7 @@ templates/search_result.xml
templates/error.html
templates/error.xml
templates/index.html
templates/widget.html
templates/suggestion.json
templates/head_taskbar.html
templates/taskbar_part.html
@@ -53,3 +37,19 @@ templates/url_of_search_results_css
opensearchdescription.xml
ft_opensearchdescription.xml
catalog_v2_searchdescription.xml
skin/css/autoComplete.css
skin/css/images/search.svg
skin/favicon/android-chrome-192x192.png
skin/favicon/android-chrome-512x512.png
skin/favicon/apple-touch-icon.png
skin/favicon/browserconfig.xml
skin/favicon/favicon-16x16.png
skin/favicon/favicon-32x32.png
skin/favicon/favicon.ico
skin/favicon/mstile-70x70.png
skin/favicon/mstile-144x144.png
skin/favicon/mstile-150x150.png
skin/favicon/mstile-310x150.png
skin/favicon/mstile-310x310.png
skin/favicon/safari-pinned-tab.svg
skin/favicon/site.webmanifest

1
static/skin/autoComplete.min.js vendored Normal file
View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,91 @@
.autoComplete_wrapper {
display: inline-block;
position: relative;
}
.autoComplete_wrapper > input {
width: 370px;
height: 40px;
padding-left: 20px;
font-size: 1rem;
color: rgba(123, 123, 123, 1);
border-radius: 8px;
border: 0;
outline: none;
background-color: #f1f3f4;
}
.autoComplete_wrapper > input::placeholder {
color: rgba(123, 123, 123, 0.5);
transition: all 0.3s ease;
}
.autoComplete_wrapper > ul {
position: absolute;
max-height: 226px;
overflow-y: scroll;
top: 100%;
left: 0;
right: 0;
padding: 0;
margin: 0.5rem 0 0;
border-radius: 0.6rem;
background-color: #fff;
box-shadow: 0 3px 6px rgba(149, 157, 165, 0.15);
border: 1px solid rgba(33, 33, 33, 0.07);
z-index: 1000;
outline: none;
}
.autoComplete_wrapper > ul[hidden],
.autoComplete_wrapper > ul:empty {
display: block;
opacity: 0;
transform: scale(0);
}
.autoComplete_wrapper > ul > li {
margin: 0.3rem;
padding: 0.3rem 0.5rem;
list-style: none;
text-align: left;
font-size: 1rem;
color: #212121;
border-radius: 0.35rem;
background-color: rgba(255, 255, 255, 1);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.2s ease;
}
.autoComplete_wrapper > ul > li::selection {
color: rgba(#ffffff, 0);
background-color: rgba(#ffffff, 0);
}
.autoComplete_wrapper > ul > li:hover {
cursor: pointer;
background-color: rgba(123, 123, 123, 0.1);
}
.autoComplete_wrapper > ul > li mark {
background-color: transparent;
color: rgba(255, 122, 122, 1);
font-weight: bold;
}
.autoComplete_wrapper > ul > li mark::selection {
color: rgba(#ffffff, 0);
background-color: rgba(#ffffff, 0);
}
.autoComplete_wrapper > ul > li[aria-selected="true"] {
background-color: rgba(123, 123, 123, 0.1);
}
@media only screen and (max-width: 600px) {
.autoComplete_wrapper > input {
width: 18rem;
}
}

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" focusable="false" x="0px" y="0px" width="30" height="30" viewBox="0 0 171 171" style=" fill:#000000;">
<g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal">
<path d="M0,171.99609v-171.99609h171.99609v171.99609z" fill="none"></path>
<g fill="#ff7a7a">
<path d="M74.1,17.1c-31.41272,0 -57,25.58728 -57,57c0,31.41272 25.58728,57 57,57c13.6601,0 26.20509,-4.85078 36.03692,-12.90293l34.03301,34.03301c1.42965,1.48907 3.55262,2.08891 5.55014,1.56818c1.99752,-0.52073 3.55746,-2.08067 4.07819,-4.07819c0.52073,-1.99752 -0.0791,-4.12049 -1.56818,-5.55014l-34.03301,-34.03301c8.05215,-9.83182 12.90293,-22.37682 12.90293,-36.03692c0,-31.41272 -25.58728,-57 -57,-57zM74.1,28.5c25.2517,0 45.6,20.3483 45.6,45.6c0,25.2517 -20.3483,45.6 -45.6,45.6c-25.2517,0 -45.6,-20.3483 -45.6,-45.6c0,-25.2517 20.3483,-45.6 45.6,-45.6z"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="skin/favicon/mstile-70x70.png"/>
<square144x144logo src="skin/favicon/mstile-144x144.png"/>
<square150x150logo src="skin/favicon/mstile-150x150.png"/>
<square310x150logo src="skin/favicon/mstile-310x150.png"/>
<square310x310logo src="skin/favicon/mstile-310x310.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2130 5897 c-3 -3 -38 -7 -78 -11 -41 -3 -77 -8 -80 -10 -4 -2 -27
-7 -52 -10 -25 -3 -73 -13 -105 -22 -33 -8 -67 -17 -75 -19 -51 -11 -211 -75
-305 -120 -93 -45 -289 -163 -305 -184 -3 -3 -27 -24 -55 -46 -108 -86 -238
-229 -323 -357 -48 -71 -142 -245 -142 -263 0 -4 -6 -21 -14 -38 -21 -46 -51
-149 -72 -247 -19 -89 -26 -391 -11 -475 3 -22 9 -56 12 -75 46 -278 218 -619
460 -909 97 -117 141 -182 172 -256 19 -45 85 -251 118 -365 7 -25 35 -119 63
-210 144 -469 182 -592 184 -607 3 -14 -10 -16 -77 -18 -93 -2 -140 -20 -185
-71 -28 -33 -57 -110 -46 -122 4 -3 109 -6 234 -7 l227 -1 105 -51 c154 -74
310 -148 440 -209 63 -30 133 -63 155 -74 25 -12 43 -16 49 -10 6 6 10 39 10
74 1 56 -3 70 -29 108 -25 36 -49 52 -142 98 -61 30 -114 57 -116 60 -3 3 120
5 273 5 l278 0 -3 25 c-9 69 -45 119 -110 153 -38 19 -58 20 -345 22 -272 1
-309 3 -343 19 -48 24 -102 86 -115 134 -5 20 -42 143 -82 272 -40 129 -88
287 -107 350 -19 63 -46 153 -60 200 -48 157 -30 273 54 357 65 65 153 95 237
79 45 -9 96 -60 130 -132 20 -41 286 -885 286 -905 0 -2 -37 -5 -83 -5 -130
-2 -196 -47 -227 -155 -7 -23 -5 -31 8 -37 9 -5 112 -8 227 -7 l210 1 110 -52
c61 -28 167 -79 237 -112 70 -34 147 -70 172 -82 25 -11 87 -40 138 -65 54
-26 96 -41 101 -36 19 19 20 100 3 146 -23 59 -63 91 -178 145 -54 25 -98 47
-98 50 0 3 88 6 196 5 l196 0 60 -80 c208 -276 501 -419 838 -410 381 10 721
235 881 582 80 174 109 431 69 614 -102 469 -504 798 -976 797 -446 0 -828
-283 -958 -708 -35 -114 -45 -194 -41 -325 4 -93 10 -141 37 -255 5 -22 -465
-24 -532 -3 -69 23 -114 78 -144 176 -210 687 -376 1234 -376 1241 0 9 88 51
155 74 107 37 218 62 350 79 117 15 593 8 705 -11 14 -2 61 -7 105 -11 44 -3
94 -8 111 -10 160 -20 504 -27 624 -11 240 30 439 120 611 273 102 91 142 113
207 113 75 0 142 -25 252 -94 334 -208 677 -541 882 -856 46 -71 158 -266 158
-275 0 -2 15 -36 34 -77 18 -40 41 -95 50 -121 l18 -48 31 19 c50 29 87 90 94
153 10 95 -33 204 -168 434 -82 140 -272 379 -429 540 -214 220 -385 364 -669
564 -77 54 -138 100 -136 103 2 2 7 42 11 89 26 336 -200 696 -540 858 -131
63 -205 82 -346 88 -128 5 -216 -9 -332 -53 l-68 -26 -43 38 c-23 21 -44 40
-47 44 -22 27 -248 190 -348 252 -133 81 -408 208 -452 208 -9 0 -20 4 -26 9
-9 9 -189 56 -254 66 -19 3 -60 10 -90 16 -54 10 -421 24 -430 16z m2316
-1050 c97 -49 163 -150 165 -253 2 -87 -33 -199 -39 -126 -6 72 -105 152 -187
152 -95 0 -185 -84 -190 -177 -3 -50 14 -104 43 -137 16 -18 16 -19 -13 -12
-16 4 -51 21 -76 37 -119 76 -167 241 -107 369 27 58 48 84 100 123 81 61 211
71 304 24z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

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

View File

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
(function() {
const root = $(`link[type='root']`).attr('href');
const kiwixServe = (function() {
const root = document.querySelector(`link[type='root']`).getAttribute('href');
const incrementalLoadingParams = {
start: 0,
count: viewPortToCount()
@@ -17,6 +17,7 @@
let params = new URLSearchParams(window.location.search || filters || '');
let timer;
let languages = {};
let allowBookClick = true;
function queryUrlBuilder() {
let url = `${root}/catalog/search?`;
@@ -77,8 +78,15 @@
return queryNode != null ? queryNode.innerHTML : "";
}
function generateTagLink(tagValue) {
tagValue = tagValue.toLowerCase();
const humanFriendlyTagValue = humanFriendlyTitle(tagValue);
const tagMessage = `Filter by tag "${humanFriendlyTagValue}"`;
return `<span class='tag__link' aria-label='${tagMessage}' title='${tagMessage}' data-tag=${tagValue}>${humanFriendlyTagValue}</span>`
}
function generateBookHtml(book, sort = false) {
const link = book.querySelector('link[type="text/html"]').getAttribute('href');
let link = book.querySelector('link[type="text/html"]').getAttribute('href');
let iconUrl;
book.querySelectorAll('link[rel="http://opds-spec.org/image/thumbnail"]').forEach(link => {
if (link.getAttribute('type').split(';')[1] == 'width=48' && !iconUrl) {
@@ -91,9 +99,9 @@
const langCode = getInnerHtml(book, 'language');
const language = languages[langCode];
const tags = getInnerHtml(book, 'tags');
let tagHtml = tags.split(';').filter(tag => {return !(tag.split(':')[0].startsWith('_'))})
.map((tag) => {return tag.charAt(0).toUpperCase() + tag.slice(1)})
.join(' | ').replace(/_/g, ' ');
const tagList = tags.split(';').filter(tag => {return !(tag.startsWith('_'))});
const tagFilterLinks = tagList.map((tagValue) => generateTagLink(tagValue));
const tagHtml = tagFilterLinks.join(' | ');
let downloadLink;
let zimSize = 0;
try {
@@ -113,17 +121,24 @@
}
const faviconAttr = iconUrl != undefined ? `style="background-image: url('${iconUrl}')"` : '';
const languageAttr = langCode != '' ? `title="${language}" aria-label="${language}"` : 'style="background-color: transparent"';
divTag.innerHTML = `<a class="book__link" href="${link}" data-hover="Preview">
if (!allowBookClick) {
link = "javascript:void(0)";
}
divTag.innerHTML = `
<div class="book__wrapper">
<a class="book__link" href="${link}" data-hover="Preview">
<div class="book__link__wrapper">
<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>`: ''}` : ''}
</div>
<div class="book__description" title="${description}">${description}</div>
</div>
</a>
<div class="book__languageTag" ${languageAttr}>${getLanguageCodeToDisplay(langCode)}</div>
<div class="book__tags"><div class="book__tags--wrapper">${tagHtml}</div></div>
</div></div></a>`;
</div></div>`;
return divTag;
}
@@ -236,14 +251,16 @@
toggleFooter();
}
const kiwixResultText = document.querySelector('.kiwixHomeBody__results')
if (results) {
let resultText = `${results} books`;
if (results === 1) {
resultText = `${results} book`;
if (kiwixResultText) {
if (results) {
let resultText = `${results} books`;
if (results === 1) {
resultText = `${results} book`;
}
kiwixResultText.innerHTML = resultText;
} else {
kiwixResultText.innerHTML = ``;
}
kiwixResultText.innerHTML = resultText;
} else {
kiwixResultText.innerHTML = ``;
}
loader.style.display = 'none';
return books;
@@ -254,19 +271,33 @@
await fetch(query).then(async (resp) => {
const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml');
let optionStr = '';
data.querySelectorAll('entry').forEach(entry => {
const title = getInnerHtml(entry, 'title');
const value = getInnerHtml(entry, valueEntryNode);
const hfTitle = humanFriendlyTitle(title);
if (valueEntryNode == 'language') {
languages[value] = hfTitle;
}
optionStr += (hfTitle != '') ? `<option value="${value}">${hfTitle}</option>` : '';
});
document.querySelector(nodeQuery).innerHTML += optionStr;
const entryList = data.querySelectorAll('entry');
const nodeQueryElem = document.querySelector(nodeQuery);
if (entryList && nodeQueryElem) {
entryList.forEach(entry => {
const title = getInnerHtml(entry, 'title');
const value = getInnerHtml(entry, valueEntryNode);
const hfTitle = humanFriendlyTitle(title);
if (valueEntryNode == 'language') {
languages[value] = hfTitle;
}
optionStr += (hfTitle != '') ? `<option value="${value}">${hfTitle}</option>` : '';
});
nodeQueryElem.innerHTML += optionStr;
}
});
}
function setNoResultsContent() {
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>?`;
kiwixHomeBody.append(divTag);
kiwixHomeBody.setAttribute('style', 'display: flex; justify-content: center; align-items: center');
loader.setAttribute('style', 'position: absolute; top: 50%');
}
function checkAndInjectEmptyMessage() {
const kiwixHomeBody = document.querySelector('.kiwixHomeBody');
if (!bookOrderMap.size) {
@@ -274,28 +305,7 @@
noResultInjected = true;
iso.remove(document.getElementsByClassName('book__list')[0].getElementsByTagName('div'));
iso.layout();
setTimeout(() => {
const divTag = document.createElement('div');
divTag.setAttribute('class', 'noResults');
divTag.innerHTML = `No result. Would you like to <a href="/?lang=">reset filter</a>?`;
kiwixHomeBody.append(divTag);
kiwixHomeBody.setAttribute('style', 'display: flex; justify-content: center; align-items: center');
divTag.getElementsByTagName('a')[0].onclick = (event) => {
event.preventDefault();
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?lang=`);
setCookie(filterCookieName, 'lang=');
resetAndFilter();
document.querySelectorAll('.filter').forEach(filter => {
filter.value = params.get(filter.name) || '';
if (filter.value) {
filter.style = 'background-color: #858585; color: #fff';
} else {
filter.style = 'background-color: #ffffff; color: black';
}
})
};
loader.setAttribute('style', 'position: absolute; top: 50%');
}, 300);
setTimeout(setNoResultsContent, 300);
}
return true;
} else if (noResultInjected) {
@@ -320,7 +330,7 @@
const booksToFilter = new Set();
const booksToDelete = new Set();
iso.arrange({
filter: function (idx, elem) {
filter: function (elem) {
const id = elem.getAttribute('data-id');
const retVal = bookOrderMap.has(id);
if (retVal) {
@@ -344,6 +354,7 @@
insertModal(downloadButton);
}
});
refreshTagLinks();
}
async function resetAndFilter(filterType = '', filterValue = '') {
@@ -355,22 +366,16 @@
params = new URLSearchParams(window.location.search);
if (filterType) {
params.set(filterType, filterValue);
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?${params.toString()}`);
window.history.pushState({}, null, `?${params.toString()}`);
setCookie(filterCookieName, params.toString());
}
document.querySelectorAll('.filter').forEach(filter => {
if (filter.value) {
filter.style = 'background-color: #858585; color: #fff';
} else {
filter.style = 'background-color: #ffffff; color: black';
}
});
updateFilterColors();
await loadAndDisplayBooks(true);
}
window.addEventListener('popstate', async () => {
await resetAndFilter();
document.querySelectorAll('.filter').forEach(filter => {filter.value = params.get(filter.name) || ''});
updateVisibleParams();
});
async function loadSubset() {
@@ -384,16 +389,78 @@
}
}
window.addEventListener('resize', (event) => {
function updateFilterColors() {
document.querySelectorAll('.filter').forEach(filter => {
if (filter.value) {
filter.style = 'background-color: #858585; color: #fff';
} else {
filter.style = 'background-color: #ffffff; color: black';
}
});
}
function disableBookClick() {
allowBookClick = false;
}
function addTagElement(tagValue, resetFilter) {
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
tagElement.style.display = 'inline-block';
const humanFriendlyTagValue = humanFriendlyTitle(tagValue);
tagElement.innerHTML = `${humanFriendlyTagValue}`;
const tagMessage = `Stop filtering by tag "${humanFriendlyTagValue}"`;
tagElement.setAttribute('aria-label', tagMessage);
tagElement.setAttribute('title', tagMessage);
if (resetFilter)
resetAndFilter('tag', tagValue);
}
function refreshTagLinks() {
const tagLinks = document.getElementsByClassName('tag__link');
[...tagLinks].forEach(elem => {
if (!elem.getAttribute('click-listener')) {
elem.addEventListener('click', () => addTagElement(elem.dataset.tag, true));
elem.setAttribute('click-listener', 'true');
}
});
}
function removeTagElement(resetFilter) {
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
tagElement.style.display = 'none';
if (resetFilter)
resetAndFilter('tag', '');
}
function updateVisibleParams() {
document.querySelectorAll('.filter').forEach(filter => {filter.value = params.get(filter.name) || ''});
updateFilterColors();
const tagKey = params.get('tag');
if (tagKey !== null && tagKey.trim() !== '') {
addTagElement(tagKey, false);
} else {
removeTagElement(false);
}
}
function updateBookCount(event) {
if (timer) {clearTimeout(timer)}
timer = setTimeout(() => {
incrementalLoadingParams.count = incrementalLoadingParams.count && viewPortToCount();
loadSubset();
}, 100, event);
});
}
window.addEventListener('resize', (event) => updateBookCount(event));
window.addEventListener('scroll', loadSubset);
window.addEventListener('keydown', function (event) {
if (event.key === "Escape" ) {
closeModal();
}
});
window.onload = async () => {
iso = new Isotope( '.book__list', {
itemSelector: '.book',
@@ -404,10 +471,9 @@
}
},
sortBy: 'weight',
layoutMode: 'cellsByRow',
cellsByRow: {
columnWidth: '.book',
rowHeight: '.book'
layoutMode: 'masonry',
masonry: {
fitWidth: true
}
});
footer = document.getElementById('kiwixfooter');
@@ -419,15 +485,17 @@
document.querySelectorAll('.filter').forEach(filter => {
filter.addEventListener('change', () => {resetAndFilter(filter.name, filter.value)});
});
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
tagElement.addEventListener('click', () => removeTagElement(true));
if (filters) {
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?${params.toString()}`);
}
params.forEach((value, key) => {
const selectBox = document.getElementsByName(key)[0];
if (selectBox) {
selectBox.value = value
const currentLink = window.location.search;
const newLink = `?${params.toString()}`;
if (currentLink != newLink) {
window.history.pushState({}, null, newLink);
}
});
}
updateVisibleParams();
updateBookCount();
document.getElementById('kiwixSearchForm').onsubmit = (event) => {event.preventDefault()};
if (!window.location.search) {
const browserLang = navigator.language.split('-')[0];
@@ -438,13 +506,12 @@
langFilter.dispatchEvent(new Event('change'));
}
}
document.querySelectorAll('.filter').forEach(filter => {
if (filter.value) {
filter.style = 'background-color: #858585; color: #fff';
} else {
filter.style = 'background-color: #ffffff; color: black';
}
});
setCookie(filterCookieName, params.toString());
}
return {
updateBookCount,
disableBookClick
};
})();

View File

File diff suppressed because it is too large Load Diff

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -9,6 +9,8 @@
transition: 0.3s;
width: 100%;
box-sizing: border-box;
background: #e3e3e3;
border-bottom: 1px solid #aaa;
}
#kiwixtoolbar>a {
@@ -100,7 +102,6 @@ label[for="kiwix_button_show_toggle"],
}
.kiwix #kiwixtoolbar #kiwixsearchform input[type='text'] {
position: absolute;
left: 0;
box-sizing: border-box !important;
width: 100%;
@@ -123,20 +124,19 @@ label[for=kiwixsearchbox] {
vertical-align: middle;
}
body {
padding-top: calc(3em - 5px) !important;
a.suggest, a.suggest:visited, a.suggest:hover, a.suggest:active {
display: block;
text-decoration: none;
color: inherit;
}
/* Try to fix buggy stuff in jquery-ui autocomplete */
#ui-id-1,
.ui-autocomplete {
background: white !important;
border: solid 1px grey !important;
.autoComplete_wrapper > ul {
max-height: none !important;
column-count: 1 !important;
}
li.ui-state-focus {
font-weight: bold;
body {
padding-top: calc(3em - 5px) !important;
}
@media(min-width:420px) {
@@ -155,6 +155,11 @@ li.ui-state-focus {
@media (max-width: 645px) {
.autoComplete_wrapper > ul {
max-height: none !important;
column-count: 1 !important;
}
#kiwix_button_show_toggle~label~.kiwix_button_cont.searching {
display: none !important;
}

View File

@@ -3,95 +3,120 @@ function htmlDecode(input) {
return doc.documentElement.textContent;
}
const jq = jQuery.noConflict(true);
jq(document).ready(() => {
(function ($) {
const root = $( `link[type='root']` ).attr("href");
function setupAutoHidingOfTheToolbar() {
let lastScrollTop = 0;
const delta = 5;
let didScroll = false;
const kiwixToolBar = document.querySelector('#kiwixtoolbar');
const bookName = (window.location.pathname == `${root}/search`)
? (new URLSearchParams(window.location.search)).get('content')
: window.location.pathname.split(`${root}/`)[1].split('/')[0];
const userlang = (new URLSearchParams(window.location.search)).get('userlang') || "en";
$( "#kiwixsearchbox" ).autocomplete({
source: `${root}/suggest?content=${bookName}&userlang=${userlang}`,
dataType: "json",
cache: false,
response: function( event, ui ) {
for(const item of ui.content) {
item.label = htmlDecode(item.label);
item.value = htmlDecode(item.value);
if (item.path) item.path = htmlDecode(item.path);
}
},
select: function(event, ui) {
if (ui.item.kind === 'path') {
window.location.href = `${root}/${bookName}/${encodeURI(ui.item.path)}`;
} else {
$( "#kiwixsearchbox" ).val(ui.item.value);
$( "#kiwixsearchform" ).submit();
}
},
}).data( "ui-autocomplete" )._renderItem = function( ul, item ) {
return $( "<li>" )
.data( "ui-autocomplete-item", item )
.append( item.label )
.appendTo( ul );
};
/* cybook hack */
if (navigator.userAgent.indexOf("bookeen/cybook") != -1) {
$("html").addClass("cybook");
}
if ($(window).width() < 520) {
var didScroll;
var lastScrollTop = 0;
var delta = 5;
// on scroll, let the interval function know the user has scrolled
$(window).scroll(function (event) {
didScroll = true;
});
// run hasScrolled() and reset didScroll status
setInterval(function () {
if (didScroll) {
hasScrolled();
didScroll = false;
}
}, 250);
function hasScrolled() {
var st = $(this).scrollTop();
// Make sure they scroll more than delta
if (Math.abs(lastScrollTop - st) <= delta)
return;
// If they scrolled down and are past the navbar, add class .nav-up.
// This is necessary so you never see what is "behind" the navbar.
if (st > lastScrollTop) {
// Scroll Down
$('#kiwixtoolbar').css({ top: '-100%' });
} else {
// Scroll Up
$('#kiwixtoolbar').css({ top: '0' });
}
lastScrollTop = st;
}
}
$('#kiwixsearchbox').on({
focus: function () {
$('.kiwix_searchform').addClass('full_width');
$('label[for="kiwix_button_show_toggle"], .kiwix_button_cont').addClass('searching');
},
blur: function () {
$('.kiwix_searchform').removeClass('full_width');
$('label[for="kiwix_button_show_toggle"], .kiwix_button_cont').removeClass('searching');
}
});
})(jq);
})
window.addEventListener('scroll', () => {
didScroll = true;
});
setInterval(function() {
if (didScroll) {
hasScrolled();
didScroll = false;
}
}, 250);
function hasScrolled() {
const st = document.documentElement.scrollTop || document.body.scrollTop;
if (Math.abs(lastScrollTop - st) <= delta)
return;
if (st > lastScrollTop) {
kiwixToolBar.style.top = '-100%';
} else {
kiwixToolBar.style.top = '0';
}
lastScrollTop = st;
}
}
document.addEventListener('DOMContentLoaded', function () {
const root = document.querySelector(`link[type='root']`).getAttribute("href");
const bookName = (window.location.pathname == `${root}/search`)
? (new URLSearchParams(window.location.search)).get('content')
: window.location.pathname.split(`${root}/`)[1].split('/')[0];
const autoCompleteJS = new autoComplete(
{
selector: "#kiwixsearchbox",
placeHolder: document.querySelector("#kiwixsearchbox").title,
threshold: 1,
debounce: 300,
data : {
src: async (query) => {
try {
// Fetch Data from external Source
const source = await fetch(`${root}/suggest?content=${encodeURIComponent(bookName)}&term=${encodeURIComponent(query)}`);
const data = await source.json();
return data;
} catch (error) {
return error;
}
},
keys: ['label'],
},
submit: true,
searchEngine: (query, record) => {
// We accept all records
return true;
},
resultsList: {
noResults: true,
/* We must display 10 results (requested) + 1 potential link to do a full text search. */
maxResults: 11,
},
resultItem: {
element: (item, data) => {
let searchLink;
if (data.value.kind == "path") {
searchLink = `${root}/${bookName}/${htmlDecode(data.value.path)}`;
} else {
searchLink = `${root}/search?content=${encodeURIComponent(bookName)}&pattern=${encodeURIComponent(htmlDecode(data.value.value))}`;
}
item.innerHTML = `<a class="suggest" href="${searchLink}">${htmlDecode(data.value.label)}</a>`;
},
highlight: "autoComplete_highlight",
selected: "autoComplete_selected"
}
}
);
document.querySelector('#kiwixsearchform').addEventListener('submit', function(event) {
try {
const selectedElemLink = document.querySelector('.autoComplete_selected > a').href;
if (selectedElemLink) {
event.preventDefault();
window.location = selectedElemLink;
}
} catch (err) {}
});
const kiwixSearchBox = document.querySelector('#kiwixsearchbox');
const kiwixSearchForm = document.querySelector('.kiwix_searchform');
kiwixSearchBox.addEventListener('focus', () => {
kiwixSearchForm.classList.add('full_width');
document.querySelector('label[for="kiwix_button_show_toggle"]').classList.add('searching');
document.querySelector('.kiwix_button_cont').classList.add('searching');
});
kiwixSearchBox.addEventListener('blur', () => {
kiwixSearchForm.classList.remove('full_width');
document.querySelector('label[for="kiwix_button_show_toggle"]').classList.remove('searching');
document.querySelector('.kiwix_button_cont').classList.remove('searching');
});
// cybook hack
if (navigator.userAgent.indexOf("bookeen/cybook") != -1) {
document.querySelector('html').classList.add('cybook');
}
if (document.body.clientWidth < 520) {
setupAutoHidingOfTheToolbar();
}
});

107
static/skin/widget.js Normal file
View File

@@ -0,0 +1,107 @@
function disableSearchFilters(widgetStyles) {
const hideNavRule = `
.kiwixNav {
display: none;
}`;
const hideResultsLabelRule = `
.kiwixHomeBody__results {
display: none;
}`;
const hideTagFilterRule = `
.book__tags {
pointer-events: none;
}`;
insertNewCssRules(widgetStyles, [hideNavRule, hideResultsLabelRule, hideTagFilterRule]);
}
function disableBookClick() {
kiwixServe.disableBookClick();
}
function disableDownload(widgetStyles) {
const hideBookDownloadRule = `
.book__download {
display: none;
}`;
insertNewCssRules(widgetStyles, [hideBookDownloadRule]);
}
function disableDescription(widgetStyles) {
const decreaseHeightRule = `
.book__wrapper {
height:128px;
grid-template-rows: 70px 0 1fr 1fr;
}`;
const hideDescRule = `
.book__description {
display: none;
}`;
insertNewCssRules(widgetStyles, [decreaseHeightRule, hideDescRule]);
}
function hideFooter(widgetStyles) {
const hideFooterRule = `
.kiwixfooter {
display: none !important;
}`;
insertNewCssRules(widgetStyles, [hideFooterRule]);
}
function insertNewCssRules(stylesheet, ruleList) {
if (stylesheet) {
for (rule of ruleList) {
stylesheet.insertRule(rule, 0);
}
}
}
function addCustomCss(cssCode) {
let customCSS = document.createElement('style');
customCSS.innerHTML = cssCode;
document.head.appendChild(customCSS);
}
function addCustomJs(jsCode) {
new Function(`"use strict";${jsCode}`)();
}
function handleMessages(event) {
if ('css' in event.data) {
addCustomCss(event.data.css);
}
if ('js' in event.data) {
addCustomJs(event.data.js);
}
}
function handleWidget() {
const params = new URLSearchParams(window.location.search || filters || '');
const widgetStyleElem = document.createElement('style');
document.head.appendChild(widgetStyleElem);
const widgetStyles = widgetStyleElem.sheet;
const disableFilters = params.has('disablefilter');
const disableClick = params.has('disableclick');
const disableDwld = params.has('disabledownload');
const disableDesc = params.has('disabledesc');
const blankBase = document.createElement('base');
blankBase.target = '_blank';
document.head.appendChild(blankBase); // open all links in new tab
if (disableFilters)
disableSearchFilters(widgetStyles);
if (disableClick)
disableBookClick();
if (disableDwld)
disableDownload(widgetStyles);
if (disableDesc)
disableDescription(widgetStyles);
hideFooter(widgetStyles);
kiwixServe.updateBookCount();
}
window.addEventListener('message', handleMessages);
handleWidget();

View File

@@ -16,9 +16,9 @@
<articleCount>{{article_count}}</articleCount>
<mediaCount>{{media_count}}</mediaCount>
{{#icons}}<link rel="http://opds-spec.org/image/thumbnail"
href="{{root}}/catalog/v2/illustration/{{{content_id}}}/?size={{icon_size}}"
href="{{root}}/catalog/v2/illustration/{{{id}}}/?size={{icon_size}}"
type="{{icon_mimetype}};width={{icon_size}};height={{icon_size}};scale=1"/>
{{/icons}}<link type="text/html" href="{{root}}/{{{content_id}}}" />
{{/icons}}<link type="text/html" href="{{root}}/content/{{{content_id}}}" />
<author>
<name>{{author_name}}</name>
</author>

View File

@@ -1,6 +1,4 @@
<link type="text/css" href="{{root}}/skin/jquery-ui/jquery-ui.min.css?KIWIXCACHEID" rel="Stylesheet" />
<link type="text/css" href="{{root}}/skin/jquery-ui/jquery-ui.theme.min.css?KIWIXCACHEID" rel="Stylesheet" />
<link type="text/css" href="{{root}}/skin/taskbar.css?KIWIXCACHEID" rel="Stylesheet" />
<script type="text/javascript" src="{{root}}/skin/jquery-ui/external/jquery/jquery.js?KIWIXCACHEID" defer></script>
<script type="text/javascript" src="{{root}}/skin/jquery-ui/jquery-ui.min.js?KIWIXCACHEID" defer></script>
<link type="text/css" href="{{root}}/skin/css/autoComplete.css?KIWIXCACHEID" rel="Stylesheet" />
<script type="text/javascript" src="{{root}}/skin/taskbar.js?KIWIXCACHEID" defer></script>
<script type="text/javascript" src="{{root}}/skin/autoComplete.min.js?KIWIXCACHEID"></script>

View File

@@ -4,29 +4,20 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Welcome to Kiwix Server</title>
<script
type="text/javascript"
src="{{root}}/skin/jquery-ui/external/jquery/jquery.js?KIWIXCACHEID"
></script>
<script
type="text/javascript"
src="{{root}}/skin/jquery-ui/jquery-ui.min.js?KIWIXCACHEID"
></script>
<link
type="text/css"
href="{{root}}/skin/jquery-ui/jquery-ui.min.css?KIWIXCACHEID"
rel="Stylesheet"
/>
<link
type="text/css"
href="{{root}}/skin/jquery-ui/jquery-ui.theme.min.css?KIWIXCACHEID"
rel="Stylesheet"
/>
<link
type="text/css"
href="{{root}}/skin/index.css?KIWIXCACHEID"
rel="Stylesheet"
/>
<link rel="apple-touch-icon" sizes="180x180" href="{{root}}/skin/favicon/apple-touch-icon.png?KIWIXCACHEID">
<link rel="icon" type="image/png" sizes="32x32" href="{{root}}/skin/favicon/favicon-32x32.png?KIWIXCACHEID">
<link rel="icon" type="image/png" sizes="16x16" href="{{root}}/skin/favicon/favicon-16x16.png?KIWIXCACHEID">
<link rel="manifest" href="{{root}}/skin/favicon/site.webmanifest">
<link rel="mask-icon" href="{{root}}/skin/favicon/safari-pinned-tab.svg?KIWIXCACHEID" color="#5bbad5">
<link rel="shortcut icon" href="{{root}}/skin/favicon/favicon.ico?KIWIXCACHEID">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="{{root}}/skin/favicon/browserconfig.xml?KIWIXCACHEID">
<meta name="theme-color" content="#ffffff">
<style>
@font-face {
font-family: "poppins";
@@ -58,12 +49,14 @@
</div>
<form id='kiwixSearchForm' class='kiwixNav__SearchForm'>
<input type="text" name="q" placeholder="Search" id="searchFilter" class='kiwixSearch filter'>
<input type="submit" class="searchButton" value="Search"/>
<span class="kiwixButton tagFilterLabel"></span>
<input type="submit" class="kiwixButton kiwixButtonHover" value="Search"/>
</form>
</div>
<div class="kiwixHomeBody">
<h3 class="kiwixHomeBody__results"></h3>
<div class="book__list"></div>
<div class="book__list">
<h3 class="kiwixHomeBody__results"></h3>
</div>
<div id="fadeOut" class="fadeOut"></div>
</div>
<div class="loader" style="position: absolute; top: 50%"><div class="loader-spinner"></div></div>

View File

@@ -5,7 +5,7 @@
<form class="kiwixsearch" method="GET" action="{{root}}/search" id="kiwixsearchform">
{{#hascontent}}<input type="hidden" name="content" value="{{content}}" />{{/hascontent}}
<label for="kiwixsearchbox">&#x1f50d;</label>
<input autocomplete="off" class="ui-autocomplete-input" id="kiwixsearchbox" name="pattern" type="text" title="{{{SEARCHBOX_TOOLTIP}}}" aria-label="{{{SEARCHBOX_TOOLTIP}}}">
<input autocomplete="off" id="kiwixsearchbox" name="pattern" type="text" size="50" title="{{{SEARCHBOX_TOOLTIP}}}" aria-label="{{{SEARCHBOX_TOOLTIP}}}">
</form>
</div>
<input type="checkbox" id="kiwix_button_show_toggle">

View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Welcome to Kiwix Server</title>
<link
type="text/css"
href="{{root}}/skin/index.css?KIWIXCACHEID"
rel="Stylesheet"
/>
<style>
@font-face {
font-family: "poppins";
src: url("{{root}}/skin/fonts/Poppins.ttf?KIWIXCACHEID") format("truetype");
}
@font-face {
font-family: "roboto";
src: url("{{root}}/skin/fonts/Roboto.ttf?KIWIXCACHEID") format("truetype");
}
</style>
<script src="{{root}}/skin/isotope.pkgd.min.js?KIWIXCACHEID" defer></script>
<script src="{{root}}/skin/iso6391To3.js?KIWIXCACHEID"></script>
<script type="text/javascript" src="{{root}}/skin/index.js?KIWIXCACHEID" defer></script>
<script type="text/javascript" src="{{root}}/skin/widget.js?KIWIXCACHEID" defer></script>
</head>
<body>
<div class='kiwixNav'>
<div class="kiwixNav__filters">
<div class="kiwixNav__select">
<select name="lang" id="languageFilter" class='kiwixNav__kiwixFilter filter'>
<option value="" selected>All languages</option>
</select>
</div>
<div class="kiwixNav__select">
<select name="category" id="categoryFilter" class='kiwixNav__kiwixFilter filter'>
<option value="" selected>All categories</option>
</select>
</div>
</div>
<form id='kiwixSearchForm' class='kiwixNav__SearchForm'>
<input type="text" name="q" placeholder="Search" id="searchFilter" class='kiwixSearch filter'>
<span class="kiwixButton tagFilterLabel"></span>
<input type="submit" class="kiwixButton kiwixButtonHover" value="Search"/>
</form>
</div>
<div class="kiwixHomeBody">
<div class="book__list">
<h3 class="kiwixHomeBody__results"></h3>
</div>
<div id="fadeOut" class="fadeOut"></div>
</div>
<div class="loader" style="position: absolute; top: 50%"><div class="loader-spinner"></div></div>
<div id="kiwixfooter" class="kiwixfooter">Powered by&nbsp;<a href="https://kiwix.org">Kiwix</a></div>
</body>
<script>
function closeModal() {
for(modal of document.getElementsByClassName('modal-wrapper')) {
modal.remove();
}
}
</script>
</html>

View File

@@ -0,0 +1,6 @@
/non-existent-item text/plain ./test/helloworld.txt
/ text/html ./test/welcome.html
/skin/index.css application/json ./test/helloworld.txt
/zimfile/A/Ray_Charles ray/charles ./test/welcome.html
/content/zimfile/A/Ray_Charles charles/ray ./test/welcome.html
/search text/html ./test/helloworld.txt

1
test/data/helloworld.txt Normal file
View File

@@ -0,0 +1 @@
Hello world!

View File

@@ -10,7 +10,7 @@
publisher="Kiwix"
date="2020-03-31"
name="wikipedia_en_ray_charles"
tags="unittest;wikipedia;_category:wikipedia;_pictures:no;_videos:no;_details:no;_ftindex:yes"
tags="public_tag_without_a_value;_private_tag_without_a_value;wikipedia;_category:wikipedia;_pictures:no;_videos:no;_details:no;_ftindex:yes"
articleCount="284"
mediaCount="2"
size="556"
@@ -28,7 +28,7 @@
publisher="Kiwix"
date="2020-03-31"
name="wikipedia_ru_ray_charles"
tags="unittest;wikipedia;_pictures:no;_videos:no;_details:no"
tags="public_tag_with_a_value:value_of_a_public_tag;_private_tag_with_a_value:value_of_a_private_tag;wikipedia;_pictures:no;_videos:no;_details:no"
articleCount="284"
mediaCount="2"
size="123"

1
test/data/welcome.html Normal file
View File

@@ -0,0 +1 @@
<html><head></head><body>Welcome</body></html>

View File

@@ -550,9 +550,11 @@ TEST_F(LibraryTest, filterByQuery)
// by default, filtering by query assumes partial query
EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki"),
"An example ZIM archive", // due to the "wikibooks" tag
"Encyclopédie de la Tunisie",
"Granblue Fantasy Wiki",
"Géographie par Wikipédia",
"Mathématiques", // due to the "wikipedia" tag
"Ray Charles",
"Wikiquote"
);
@@ -714,6 +716,7 @@ TEST_F(LibraryTest, filterByMultipleCriteria)
EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki").creator("Wikipedia"),
"Encyclopédie de la Tunisie",
"Géographie par Wikipédia",
"Mathématiques", // due to the "wikipedia" tag
"Ray Charles"
);

783
test/library_server.cpp Normal file
View File

@@ -0,0 +1,783 @@
#define CPPHTTPLIB_ZLIB_SUPPORT 1
#include "./httplib.h"
#include "gtest/gtest.h"
#define SERVER_PORT 8001
#include "server_testing_tools.h"
////////////////////////////////////////////////////////////////////////////////
// Testing of the library-related functionality of the server
////////////////////////////////////////////////////////////////////////////////
class LibraryServerTest : public ::testing::Test
{
protected:
std::unique_ptr<ZimFileServer> zfs1_;
const int PORT = 8002;
protected:
void SetUp() override {
zfs1_.reset(new ZimFileServer(PORT, "./test/library.xml"));
}
void TearDown() override {
zfs1_.reset();
}
};
// Returns a copy of 'text' where every line that fully matches 'pattern'
// preceded by optional whitespace is replaced with the fixed string
// 'replacement' preserving the leading whitespace
std::string replaceLines(const std::string& text,
const std::string& pattern,
const std::string& replacement)
{
std::regex regex("^ *" + pattern + "$");
std::ostringstream oss;
std::istringstream iss(text);
std::string line;
while ( std::getline(iss, line) ) {
if ( std::regex_match(line, regex) ) {
for ( size_t i = 0; i < line.size() && line[i] == ' '; ++i )
oss << ' ';
oss << replacement << "\n";
} else {
oss << line << "\n";
}
}
return oss.str();
}
std::string maskVariableOPDSFeedData(std::string s)
{
s = replaceLines(s, R"(<updated>\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ</updated>)",
"<updated>YYYY-MM-DDThh:mm:ssZ</updated>");
s = replaceLines(s, "<id>[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}</id>",
"<id>12345678-90ab-cdef-1234-567890abcdef</id>");
return s;
}
#define OPDS_FEED_TAG \
"<feed xmlns=\"http://www.w3.org/2005/Atom\"\n" \
" xmlns:dc=\"http://purl.org/dc/terms/\"\n" \
" xmlns:opds=\"http://opds-spec.org/2010/catalog\">\n"
#define CATALOG_LINK_TAGS \
" <link rel=\"self\" href=\"\" type=\"application/atom+xml\" />\n" \
" <link rel=\"search\"" \
" type=\"application/opensearchdescription+xml\"" \
" href=\"/ROOT/catalog/searchdescription.xml\" />\n"
#define CHARLES_RAY_CATALOG_ENTRY \
" <entry>\n" \
" <id>urn:uuid:charlesray</id>\n" \
" <title>Charles, Ray</title>\n" \
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
" <language>fra</language>\n" \
" <name>wikipedia_fr_ray_charles</name>\n" \
" <flavour></flavour>\n" \
" <category>jazz</category>\n" \
" <tags>unittest;wikipedia;_category:jazz;_pictures:no;_videos:no;_details:no;_ftindex:yes</tags>\n" \
" <articleCount>284</articleCount>\n" \
" <mediaCount>2</mediaCount>\n" \
" <link type=\"text/html\" href=\"/ROOT/content/zimfile%26other\" />\n" \
" <author>\n" \
" <name>Wikipedia</name>\n" \
" </author>\n" \
" <publisher>\n" \
" <name>Kiwix</name>\n" \
" </publisher>\n" \
" <dc:issued>2020-03-31T00:00:00Z</dc:issued>\n" \
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile%26other.zim\" length=\"569344\" />\n" \
" </entry>\n"
#define RAY_CHARLES_CATALOG_ENTRY \
" <entry>\n" \
" <id>urn:uuid:raycharles</id>\n" \
" <title>Ray Charles</title>\n" \
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
" <language>eng</language>\n" \
" <name>wikipedia_en_ray_charles</name>\n" \
" <flavour></flavour>\n" \
" <category>wikipedia</category>\n" \
" <tags>public_tag_without_a_value;_private_tag_without_a_value;wikipedia;_category:wikipedia;_pictures:no;_videos:no;_details:no;_ftindex:yes</tags>\n" \
" <articleCount>284</articleCount>\n" \
" <mediaCount>2</mediaCount>\n" \
" <link rel=\"http://opds-spec.org/image/thumbnail\"\n" \
" href=\"/ROOT/catalog/v2/illustration/raycharles/?size=48\"\n" \
" type=\"image/png;width=48;height=48;scale=1\"/>\n" \
" <link type=\"text/html\" href=\"/ROOT/content/zimfile\" />\n" \
" <author>\n" \
" <name>Wikipedia</name>\n" \
" </author>\n" \
" <publisher>\n" \
" <name>Kiwix</name>\n" \
" </publisher>\n" \
" <dc:issued>2020-03-31T00:00:00Z</dc:issued>\n" \
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim\" length=\"569344\" />\n" \
" </entry>\n"
#define UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY \
" <entry>\n" \
" <id>urn:uuid:raycharles_uncategorized</id>\n" \
" <title>Ray (uncategorized) Charles</title>\n" \
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
" <summary>No category is assigned to this library entry.</summary>\n" \
" <language>rus</language>\n" \
" <name>wikipedia_ru_ray_charles</name>\n" \
" <flavour></flavour>\n" \
" <category></category>\n" \
" <tags>public_tag_with_a_value:value_of_a_public_tag;_private_tag_with_a_value:value_of_a_private_tag;wikipedia;_pictures:no;_videos:no;_details:no</tags>\n" \
" <articleCount>284</articleCount>\n" \
" <mediaCount>2</mediaCount>\n" \
" <link type=\"text/html\" href=\"/ROOT/content/zimfile\" />\n" \
" <author>\n" \
" <name>Wikipedia</name>\n" \
" </author>\n" \
" <publisher>\n" \
" <name>Kiwix</name>\n" \
" </publisher>\n" \
" <dc:issued>2020-03-31T00:00:00Z</dc:issued>\n" \
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim\" length=\"125952\" />\n" \
" </entry>\n"
TEST_F(LibraryServerTest, catalog_root_xml)
{
const auto r = zfs1_->GET("/ROOT/catalog/root.xml");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>All zims</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
"\n"
CATALOG_LINK_TAGS
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
TEST_F(LibraryServerTest, catalog_searchdescription_xml)
{
const auto r = zfs1_->GET("/ROOT/catalog/searchdescription.xml");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(r->body,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">\n"
" <ShortName>Zim catalog search</ShortName>\n"
" <Description>Search zim files in the catalog.</Description>\n"
" <Url type=\"application/atom+xml;profile=opds-catalog\"\n"
" xmlns:atom=\"http://www.w3.org/2005/Atom\"\n"
" xmlns:k=\"http://kiwix.org/opensearchextension/1.0\"\n"
" indexOffset=\"0\"\n"
" template=\"/ROOT/catalog/search?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}&notag={k:notag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}\"/>\n"
"</OpenSearchDescription>\n"
);
}
TEST_F(LibraryServerTest, catalog_search_by_phrase)
{
const auto r = zfs1_->GET("/ROOT/catalog/search?q=\"ray%20charles\"");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (q=&quot;ray charles&quot;)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>2</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
CATALOG_LINK_TAGS
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
TEST_F(LibraryServerTest, catalog_search_by_words)
{
const auto r = zfs1_->GET("/ROOT/catalog/search?q=ray%20charles");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (q=ray charles)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>3</itemsPerPage>\n"
CATALOG_LINK_TAGS
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
TEST_F(LibraryServerTest, catalog_prefix_search)
{
{
const auto r = zfs1_->GET("/ROOT/catalog/search?q=description:ray%20description:charles");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (q=description:ray description:charles)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>2</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
CATALOG_LINK_TAGS
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
{
const auto r = zfs1_->GET("/ROOT/catalog/search?q=title:\"ray%20charles\"");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (q=title:&quot;ray charles&quot;)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
CATALOG_LINK_TAGS
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
}
TEST_F(LibraryServerTest, catalog_search_with_word_exclusion)
{
const auto r = zfs1_->GET("/ROOT/catalog/search?q=ray%20-uncategorized");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (q=ray -uncategorized)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>2</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
CATALOG_LINK_TAGS
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
TEST_F(LibraryServerTest, catalog_search_by_tag)
{
const auto r = zfs1_->GET("/ROOT/catalog/search?tag=_category:jazz");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (tag=_category:jazz)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
CATALOG_LINK_TAGS
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
TEST_F(LibraryServerTest, catalog_search_by_category)
{
const auto r = zfs1_->GET("/ROOT/catalog/search?category=jazz");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (category=jazz)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
CATALOG_LINK_TAGS
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
TEST_F(LibraryServerTest, catalog_search_results_pagination)
{
{
const auto r = zfs1_->GET("/ROOT/catalog/search?count=0");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (count=0)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>3</itemsPerPage>\n"
CATALOG_LINK_TAGS
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
{
const auto r = zfs1_->GET("/ROOT/catalog/search?count=1");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (count=1)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
CATALOG_LINK_TAGS
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
{
const auto r = zfs1_->GET("/ROOT/catalog/search?start=1&count=1");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (count=1&amp;start=1)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>1</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
CATALOG_LINK_TAGS
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
{
const auto r = zfs1_->GET("/ROOT/catalog/search?start=100&count=10");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (count=10&amp;start=100)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>100</startIndex>\n"
" <itemsPerPage>0</itemsPerPage>\n"
CATALOG_LINK_TAGS
"</feed>\n"
);
}
}
TEST_F(LibraryServerTest, catalog_v2_root)
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/root.xml");
EXPECT_EQ(r->status, 200);
const char expected_output[] = R"(<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:opds="https://specs.opds.io/opds-1.2">
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<link rel="self"
href="/ROOT/catalog/v2/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start"
href="/ROOT/catalog/v2/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="search"
href="/ROOT/catalog/v2/searchdescription.xml"
type="application/opensearchdescription+xml"/>
<title>OPDS Catalog Root</title>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<entry>
<title>All entries</title>
<link rel="subsection"
href="/ROOT/catalog/v2/entries"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<content type="text">All entries from this catalog.</content>
</entry>
<entry>
<title>All entries (partial)</title>
<link rel="subsection"
href="/ROOT/catalog/v2/partial_entries"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<content type="text">All entries from this catalog in partial format.</content>
</entry>
<entry>
<title>List of categories</title>
<link rel="subsection"
href="/ROOT/catalog/v2/categories"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<content type="text">List of all categories in this catalog.</content>
</entry>
<entry>
<title>List of languages</title>
<link rel="subsection"
href="/ROOT/catalog/v2/languages"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<content type="text">List of all languages in this catalog.</content>
</entry>
</feed>
)";
EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output);
}
TEST_F(LibraryServerTest, catalog_v2_searchdescription_xml)
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/searchdescription.xml");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(r->body,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">\n"
" <ShortName>Zim catalog search</ShortName>\n"
" <Description>Search zim files in the catalog.</Description>\n"
" <Url type=\"application/atom+xml;profile=opds-catalog;kind=acquisition\"\n"
" xmlns:atom=\"http://www.w3.org/2005/Atom\"\n"
" xmlns:k=\"http://kiwix.org/opensearchextension/1.0\"\n"
" indexOffset=\"0\"\n"
" template=\"/ROOT/catalog/v2/entries?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}\"/>\n"
"</OpenSearchDescription>\n"
);
}
TEST_F(LibraryServerTest, catalog_v2_categories)
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/categories");
EXPECT_EQ(r->status, 200);
const char expected_output[] = R"(<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:opds="https://specs.opds.io/opds-1.2">
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<link rel="self"
href="/ROOT/catalog/v2/categories"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start"
href="/ROOT/catalog/v2/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<title>List of categories</title>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<entry>
<title>jazz</title>
<link rel="subsection"
href="/ROOT/catalog/v2/entries?category=jazz"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<content type="text">All entries with category of 'jazz'.</content>
</entry>
<entry>
<title>wikipedia</title>
<link rel="subsection"
href="/ROOT/catalog/v2/entries?category=wikipedia"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<content type="text">All entries with category of 'wikipedia'.</content>
</entry>
</feed>
)";
EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output);
}
TEST_F(LibraryServerTest, catalog_v2_languages)
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/languages");
EXPECT_EQ(r->status, 200);
const char expected_output[] = R"(<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/terms/"
xmlns:opds="https://specs.opds.io/opds-1.2"
xmlns:thr="http://purl.org/syndication/thread/1.0">
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<link rel="self"
href="/ROOT/catalog/v2/languages"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start"
href="/ROOT/catalog/v2/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<title>List of languages</title>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<entry>
<title>English</title>
<dc:language>eng</dc:language>
<thr:count>1</thr:count>
<link rel="subsection"
href="/ROOT/catalog/v2/entries?lang=eng"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
</entry>
<entry>
<title>français</title>
<dc:language>fra</dc:language>
<thr:count>1</thr:count>
<link rel="subsection"
href="/ROOT/catalog/v2/entries?lang=fra"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
</entry>
<entry>
<title>русский</title>
<dc:language>rus</dc:language>
<thr:count>1</thr:count>
<link rel="subsection"
href="/ROOT/catalog/v2/entries?lang=rus"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
</entry>
</feed>
)";
EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output);
}
#define CATALOG_V2_ENTRIES_PREAMBLE0(x) \
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
"<feed xmlns=\"http://www.w3.org/2005/Atom\"\n" \
" xmlns:dc=\"http://purl.org/dc/terms/\"\n" \
" xmlns:opds=\"https://specs.opds.io/opds-1.2\"\n" \
" xmlns:opensearch=\"http://a9.com/-/spec/opensearch/1.1/\">\n" \
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n" \
"\n" \
" <link rel=\"self\"\n" \
" href=\"/ROOT/catalog/v2/" x "\"\n" \
" type=\"application/atom+xml;profile=opds-catalog;kind=acquisition\"/>\n" \
" <link rel=\"start\"\n" \
" href=\"/ROOT/catalog/v2/root.xml\"\n" \
" type=\"application/atom+xml;profile=opds-catalog;kind=navigation\"/>\n" \
" <link rel=\"up\"\n" \
" href=\"/ROOT/catalog/v2/root.xml\"\n" \
" type=\"application/atom+xml;profile=opds-catalog;kind=navigation\"/>\n" \
"\n" \
#define CATALOG_V2_ENTRIES_PREAMBLE(q) \
CATALOG_V2_ENTRIES_PREAMBLE0("entries" q)
#define CATALOG_V2_PARTIAL_ENTRIES_PREAMBLE(q) \
CATALOG_V2_ENTRIES_PREAMBLE0("partial_entries" q)
TEST_F(LibraryServerTest, catalog_v2_entries)
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/entries");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("")
" <title>All Entries</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
"\n"
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_range)
{
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?start=1");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?start=1")
" <title>Filtered Entries (start=1)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>1</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
RAY_CHARLES_CATALOG_ENTRY
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?count=2");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?count=2")
" <title>Filtered Entries (count=2)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?start=1&count=1");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?count=1&start=1")
" <title>Filtered Entries (count=1&amp;start=1)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>1</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
}
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_search_terms)
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?q=\"ray%20charles\"");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?q=%22ray%20charles%22")
" <title>Filtered Entries (q=&quot;ray charles&quot;)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>2</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
TEST_F(LibraryServerTest, catalog_v2_individual_entry_access)
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/entry/raycharles");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
RAY_CHARLES_CATALOG_ENTRY
);
const auto r1 = zfs1_->GET("/ROOT/catalog/v2/entry/non-existent-entry");
EXPECT_EQ(r1->status, 404);
}
TEST_F(LibraryServerTest, catalog_v2_partial_entries)
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/partial_entries");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_PARTIAL_ENTRIES_PREAMBLE("")
" <title>All Entries</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
"\n"
" <entry>\n"
" <id>urn:uuid:charlesray</id>\n"
" <title>Charles, Ray</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <link rel=\"alternate\"\n"
" href=\"/ROOT/catalog/v2/entry/charlesray\"\n"
" type=\"application/atom+xml;type=entry;profile=opds-catalog\"/>\n"
" </entry>\n"
" <entry>\n"
" <id>urn:uuid:raycharles</id>\n"
" <title>Ray Charles</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <link rel=\"alternate\"\n"
" href=\"/ROOT/catalog/v2/entry/raycharles\"\n"
" type=\"application/atom+xml;type=entry;profile=opds-catalog\"/>\n"
" </entry>\n"
" <entry>\n"
" <id>urn:uuid:raycharles_uncategorized</id>\n"
" <title>Ray (uncategorized) Charles</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <link rel=\"alternate\"\n"
" href=\"/ROOT/catalog/v2/entry/raycharles_uncategorized\"\n"
" type=\"application/atom+xml;type=entry;profile=opds-catalog\"/>\n"
" </entry>\n"
"</feed>\n"
);
}
#define EXPECT_SEARCH_RESULTS(SEARCH_TERM, RESULT_COUNT, OPDS_ENTRIES) \
{ \
const auto r = zfs1_->GET("/ROOT/catalog/search?q=" SEARCH_TERM); \
EXPECT_EQ(r->status, 200); \
EXPECT_EQ(maskVariableOPDSFeedData(r->body), \
OPDS_FEED_TAG \
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n" \
" <title>Filtered zims (q=" SEARCH_TERM ")</title>\n" \
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
" <totalResults>" #RESULT_COUNT "</totalResults>\n" \
" <startIndex>0</startIndex>\n" \
" <itemsPerPage>" #RESULT_COUNT "</itemsPerPage>\n" \
CATALOG_LINK_TAGS \
\
OPDS_ENTRIES \
\
"</feed>\n" \
); \
}
TEST_F(LibraryServerTest, catalog_search_includes_public_tags)
{
EXPECT_SEARCH_RESULTS("public_tag_without_a_value",
1,
RAY_CHARLES_CATALOG_ENTRY
);
EXPECT_SEARCH_RESULTS("public_tag_with_a_value",
1,
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
);
// prefix search works on tag names
EXPECT_SEARCH_RESULTS("public_tag",
2,
RAY_CHARLES_CATALOG_ENTRY
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
);
EXPECT_SEARCH_RESULTS("value_of_a_public_tag",
1,
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
);
// prefix search works on tag values
EXPECT_SEARCH_RESULTS("value_of",
1,
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
);
}
#define EXPECT_ZERO_RESULTS(SEARCH_TERM) EXPECT_SEARCH_RESULTS(SEARCH_TERM, 0, )
TEST_F(LibraryServerTest, catalog_search_on_tags_is_not_an_any_substring_match)
{
EXPECT_ZERO_RESULTS("tag_with")
EXPECT_ZERO_RESULTS("alue_of_a_public_tag")
}
TEST_F(LibraryServerTest, catalog_search_excludes_hidden_tags)
{
EXPECT_ZERO_RESULTS("_private_tag_without_a_value");
EXPECT_ZERO_RESULTS("private_tag_without_a_value");
EXPECT_ZERO_RESULTS("value_of_a_private_tag");
#undef EXPECT_ZERO_RESULTS
}
#undef EXPECT_SEARCH_RESULTS

View File

@@ -17,6 +17,7 @@ tests = [
if build_machine.system() != 'windows'
tests += [
'server',
'library_server',
'server_search'
]
endif
@@ -35,7 +36,10 @@ if gtest_dep.found() and not meson.is_cross_build()
'zimfile&other.zim',
'corner_cases.zim',
'poor.zim',
'library.xml'
'library.xml',
'customized_resources.txt',
'helloworld.txt',
'welcome.html',
]
foreach file : data_files
# configure_file(input : 'data/' + file,

View File

File diff suppressed because it is too large Load Diff

View File

@@ -195,7 +195,7 @@ struct SearchResult
const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Genius_+_Soul_=_Jazz",
/*link*/ "/ROOT/content/zimfile/A/Genius_+_Soul_=_Jazz",
/*title*/ "Genius + Soul = Jazz",
/*snippet*/ R"SNIPPET(...Grammy Hall of Fame in 2011. It was re-issued in the UK, first in 1989 on the Castle Communications "Essential Records" label, and by Rhino Records in 1997 on a single CD together with Charles' 1970 My Kind of <b>Jazz</b>. In 2010, Concord Records released a deluxe edition comprising digitally remastered versions of Genius + Soul = <b>Jazz</b>, My Kind of <b>Jazz</b>, <b>Jazz</b> Number II, and My Kind of <b>Jazz</b> Part 3. Professional ratings Review scores Source Rating Allmusic link Warr.org link Encyclopedia of Popular Music...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -203,7 +203,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Jazz_Number_II",
/*link*/ "/ROOT/content/zimfile/A/Jazz_Number_II",
/*title*/ "Jazz Number II",
/*snippet*/ R"SNIPPET(<b>Jazz</b> Number II <b>Jazz</b> Number II is a 1973 album by Ray Charles. It is a collection of <b>jazz</b>/soul instrumentals featuring Charles on piano backed by his Big Band. Professional ratings Review scores Source Rating Allmusic link <b>Jazz</b> Number II Studio album by Ray Charles Released January 1973 Recorded 1971-72 Studio Charles Tangerine/RPM Studios, Los Angeles, CA Genre Soul, <b>jazz</b> Length 39:02 Label Tangerine Producer Ray Charles Ray Charles chronology Through the Eyes of Love (1972) <b>Jazz</b> Number II......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -211,7 +211,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/My_Kind_of_Jazz_Part_3",
/*link*/ "/ROOT/content/zimfile/A/My_Kind_of_Jazz_Part_3",
/*title*/ "My Kind of Jazz Part 3",
/*snippet*/ R"SNIPPET(My Kind of <b>Jazz</b> Part 3 My Kind of <b>Jazz</b> Part 3 is a 1975 album by Ray Charles released by Crossover Records. Concord Records re-issued the contents in digital form in 2009. Professional ratings Review scores Source Rating Allmusic link My Kind of <b>Jazz</b> Part 3 Studio album by Ray Charles Released October 1975 Recorded 1975 in Los Angeles, CA Genre Soul, <b>jazz</b> Length 38:13 Label Crossover Producer Ray Charles Ray Charles chronology Renaissance (1975) My Kind of <b>Jazz</b> Part 3 (1975) Live In Japan (1975)...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -219,7 +219,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/My_Kind_of_Jazz",
/*link*/ "/ROOT/content/zimfile/A/My_Kind_of_Jazz",
/*title*/ "My Kind of Jazz",
/*snippet*/ R"SNIPPET(My Kind of <b>Jazz</b> My Kind of <b>Jazz</b> Studio album by Ray Charles Released April 1970 Recorded January 1-10, 1970 in Los Angeles, CA Genre <b>jazz</b> Length 30:20 Label Tangerine Producer Quincy Jones Ray Charles chronology Doing His Thing (1969) My Kind of <b>Jazz</b> (1970) Love Country Style (1970) Professional ratings Review scores Source Rating Allmusic link My Kind of <b>Jazz</b> is a 1970 album by Ray Charles....)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -227,7 +227,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Hank_Crawford",
/*link*/ "/ROOT/content/zimfile/A/Hank_Crawford",
/*title*/ "Hank Crawford",
/*snippet*/ R"SNIPPET(...bop, <b>jazz</b>-funk, soul <b>jazz</b> alto saxophonist, arranger and songwriter. Crawford was musical director for Ray Charles before embarking on a solo career releasing many well-regarded albums on Atlantic, CTI and Milestone. Hank Crawford Background information Birth name Bennie Ross Crawford, Jr Born (1934-12-21)December 21, 1934 Memphis, Tennessee, U.S. Died January 29, 2009(2009-01-29) (aged 74) Memphis, Tennessee, U.S. Genres R&amp;B, Hard bop, <b>Jazz</b>-funk, Soul <b>jazz</b> Occupation(s) Saxophonist, Songwriter......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -235,7 +235,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Catchin'_Some_Rays:_The_Music_of_Ray_Charles",
/*link*/ "/ROOT/content/zimfile/A/Catchin'_Some_Rays:_The_Music_of_Ray_Charles",
/*title*/ "Catchin&apos; Some Rays: The Music of Ray Charles",
/*snippet*/ R"SNIPPET(...<b>jazz</b> singer Roseanna Vitro, released in August 1997 on the Telarc <b>Jazz</b> label. Catchin' Some Rays: The Music of Ray Charles Studio album by Roseanna Vitro Released August 1997 Recorded March 26, 1997 at Sound on Sound, NYC April 4,1997 at Quad Recording Studios, NYC Genre Vocal <b>jazz</b> Length 61:00 Label Telarc <b>Jazz</b> CD-83419 Producer Paul Wickliffe Roseanna Vitro chronology Passion Dance (1996) Catchin' Some Rays: The Music of Ray Charles (1997) The Time of My Life: Roseanna Vitro Sings the Songs of......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -243,7 +243,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/That's_What_I_Say:_John_Scofield_Plays_the_Music_of_Ray_Charles",
/*link*/ "/ROOT/content/zimfile/A/That's_What_I_Say:_John_Scofield_Plays_the_Music_of_Ray_Charles",
/*title*/ "That&apos;s What I Say: John Scofield Plays the Music of Ray Charles",
/*snippet*/ R"SNIPPET(That's What I Say: John Scofield Plays the Music of Ray Charles Studio album by John Scofield Released June 7, 2005 (2005-06-07) Recorded December 2004 Studio Avatar Studios, New York City Genre <b>Jazz</b> Length 65:21 Label Verve Producer Steve Jordan John Scofield chronology EnRoute: John Scofield Trio LIVE (2004) That's What I Say: John Scofield Plays the Music of Ray Charles (2005) Out Louder (2006) Professional ratings Review scores Source Rating Allmusic All About <b>Jazz</b> All About <b>Jazz</b>...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -251,7 +251,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Tribute_to_Uncle_Ray",
/*link*/ "/ROOT/content/zimfile/A/Tribute_to_Uncle_Ray",
/*title*/ "Tribute to Uncle Ray",
/*snippet*/ R"SNIPPET(...Stevie Wonder" with the successful and popular Ray Charles who was also a blind African American musician. Like his debut, this album failed to generate hit singles as Motown struggled to find a sound to fit Wonder, who was just 12 when this album was released. Tribute to Uncle Ray Studio album by Little Stevie Wonder Released October 1962 Recorded 1962 Studio Studio A, Hitsville USA, Detroit Genre Soul, <b>jazz</b> Label Tamla Producer Henry Cosby, Clarence Paul Stevie Wonder chronology The <b>Jazz</b> Soul of Little Stevie (1962) Tribute to Uncle Ray (1962) Recorded Live: The 12 Year Old Genius (1963) Professional ratings Review scores Source Rating Allmusic...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -259,7 +259,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/The_Best_of_Ray_Charles",
/*link*/ "/ROOT/content/zimfile/A/The_Best_of_Ray_Charles",
/*title*/ "The Best of Ray Charles",
/*snippet*/ R"SNIPPET(The Best of Ray Charles The Best of Ray Charles is a compilation album released in 1970 on the Atlantic <b>Jazz</b> label, featuring previously released instrumental (non-vocal) tracks recorded by Ray Charles between November 1956 and November 1958. The Best of Ray Charles Greatest hits album by Ray Charles Released 1970 Genre R&amp;B, <b>Jazz</b> Length 34:06 Label Atlantic The instrumental, "Rockhouse" would later be covered, as "Ray's Rockhouse" (1985), by The Manhattan Transfer with lyrics by Jon Hendricks....)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -267,7 +267,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/The_Genius_Hits_the_Road",
/*link*/ "/ROOT/content/zimfile/A/The_Genius_Hits_the_Road",
/*title*/ "The Genius Hits the Road",
/*snippet*/ R"SNIPPET(...a hit single, "Georgia on My Mind". The Genius Hits the Road Studio album by Ray Charles Released September 1960 Recorded March 25 and 29, 1960 in New York City Genre R&amp;B, blues, <b>jazz</b> Length 33:37 Label ABC-Paramount 335 Producer Sid Feller Ray Charles chronology Genius + Soul = <b>Jazz</b> (1961) The Genius Hits the Road (1960) Dedicated to You (1961) Singles from The Genius Hits the Road "Georgia on My Mind" Released: September 1960 Professional ratings Review scores Source Rating Allmusic Warr.org...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -275,7 +275,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Ray_Charles_at_Newport",
/*link*/ "/ROOT/content/zimfile/A/Ray_Charles_at_Newport",
/*title*/ "Ray Charles at Newport",
/*snippet*/ R"SNIPPET(...Ray Charles at Newport is a 1958 live album of Ray Charles' July 5, 1958 performance at the Newport <b>Jazz</b> Festival. The detailed liner notes on the album were written by Kenneth Lee Karpe. All tracks from this Newport album, along with all tracks from his 1959 Herndon Stadium performance in Atlanta, were also released on the Atlantic compilation LP, Ray Charles Live. A later CD reissue of that compilation album included a previously unissued song from the 1958 Newport concert, "Swanee River Rock". Professional ratings Review scores Source Rating Allmusic link Discogs link Ray Charles at Newport Live album by Ray Charles Released November 1958 Recorded July 5, 1958 Venue Newport <b>Jazz</b> Festival, Newport, Rhode Island Genre R&amp;B Length 40:28 Label Atlantic Producer Tom Dowd (engineer) Ray Charles chronology The Great Ray Charles (1957) Ray Charles at Newport (1958) Yes Indeed! (1958) Re-issue cover 1987 re-issue/compilation...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -283,7 +283,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Here_We_Go_Again:_Celebrating_the_Genius_of_Ray_Charles",
/*link*/ "/ROOT/content/zimfile/A/Here_We_Go_Again:_Celebrating_the_Genius_of_Ray_Charles",
/*title*/ "Here We Go Again: Celebrating the Genius of Ray Charles",
/*snippet*/ R"SNIPPET(...and <b>jazz</b> trumpeter Wynton Marsalis. It was recorded during concerts at the Rose Theater in New York City, on February 9 and 10, 2009. The album received mixed reviews, in which the instrumentation of Marsalis' orchestra was praised by the critics. Here We Go Again: Celebrating the Genius of Ray Charles Live album by Willie Nelson and Wynton Marsalis Released March 29, 2011 (2011-03-29) Recorded February 9 10 2009 Venue Rose Theater, New York Genre <b>Jazz</b>, country Length 61:49 Label Blue Note......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -291,7 +291,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Confession_Blues",
/*link*/ "/ROOT/content/zimfile/A/Confession_Blues",
/*title*/ "Confession Blues",
/*snippet*/ R"SNIPPET(...<b>jazz</b> Length 2:31 Label Down Beat Records Songwriter(s) R. C. Robinson (Ray Charles) Charles moved to Seattle in 1948, where he formed The McSon Trio with guitarist G. D. "Gossie" McKee and bass player Milton S. Garret. In late 1948, Jack Lauderdale of Down Beat Records heard Charles play at the Seattle <b>jazz</b> club, The Rocking Chair. The next day, Lauderdale took Charles and his trio to a Seattle recording studio where they recorded "Confession Blues" and "I Love You, I Love You". In February......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -299,7 +299,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Genius_Loves_Company",
/*link*/ "/ROOT/content/zimfile/A/Genius_Loves_Company",
/*title*/ "Genius Loves Company",
/*snippet*/ R"SNIPPET(...<b>jazz</b> and pop standards performed by Charles and several guest musicians, such as Natalie Cole, Elton John, James Taylor, Norah Jones, B.B. King, Gladys Knight, Diana Krall, Van Morrison, Willie Nelson and Bonnie Raitt. Genius Loves Company was the last album recorded and completed by Charles before his death in June 2004. Genius Loves Company Studio album by Ray Charles Released August 31, 2004 Recorded June 2003March 2004 Genre Rhythm and blues, soul, country, blues, <b>jazz</b>, pop Length 54:03......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -307,7 +307,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Love_Country_Style",
/*link*/ "/ROOT/content/zimfile/A/Love_Country_Style",
/*title*/ "Love Country Style",
/*snippet*/ R"SNIPPET(Love Country Style Love Country Style is a studio album by Ray Charles released in June 1970 on Charles' Tangerine Records label. Love Country Style Studio album by Ray Charles Released June 1970 Genre R&amp;B Length 35:25 Label ABC/Tangerine Producer Joe Adams Ray Charles chronology My Kind of <b>Jazz</b> (1970) Love Country Style (1970) Volcanic Action of My Soul (1971) Professional ratings Review scores Source Rating Allmusic Christgau's Record Guide B...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -315,7 +315,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Doing_His_Thing",
/*link*/ "/ROOT/content/zimfile/A/Doing_His_Thing",
/*title*/ "Doing His Thing",
/*snippet*/ R"SNIPPET(Doing His Thing Doing His Thing is a 1969 studio album by Ray Charles, released by Tangerine Records. The cover artwork was by Lafayette Chew. Doing His Thing Studio album by Ray Charles Released May 1969 Recorded RPM Studios, Los Angeles, California Genre R&amp;B, soul Length 32:33 Label ABC/Tangerine Producer Joe Adams Ray Charles chronology I'm All Yours Baby (1969) Doing His Thing (1969) My Kind of <b>Jazz</b> (1970)...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -323,7 +323,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/The_Inspiration_I_Feel",
/*link*/ "/ROOT/content/zimfile/A/The_Inspiration_I_Feel",
/*title*/ "The Inspiration I Feel",
/*snippet*/ R"SNIPPET(The Inspiration I Feel The Inspiration I Feel is an album by flautist Herbie Mann featuring tunes associated with Ray Charles recorded in 1968 and released on the Atlantic label. The Inspiration I Feel Studio album by Herbie Mann Released 1968 Recorded May 6 &amp; 7, 1968 New York City Genre <b>Jazz</b> Length 34:28 Label Atlantic SD 1513 Producer Nesuhi Ertegun, Joel Dorn Herbie Mann chronology Windows Opened (1968) The Inspiration I Feel (1968) Memphis Underground (1968)...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -331,7 +331,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Milt_Turner",
/*link*/ "/ROOT/content/zimfile/A/Milt_Turner",
/*title*/ "Milt Turner",
/*snippet*/ R"SNIPPET(...Turner After graduating from Pearl High School, he attended Tennessee State University, where he coincided with Hank Crawford, who he later recommended to join him in Ray Charles' band when he took over from William Peeples in the late 1950s. Milton Turner (1930-1993) was a <b>jazz</b> drummer. In 1962, he was a member of Phineas Newborn's trio with Leroy Vinnegar, on whose solo albums he would later appear, and in the early 1960s, Turner also recorded with Teddy Edwards. He never recorded as a leader....)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -339,7 +339,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Rare_Genius",
/*link*/ "/ROOT/content/zimfile/A/Rare_Genius",
/*title*/ "Rare Genius",
/*snippet*/ R"SNIPPET(...studio recordings and demos made in the 1970s, 1980s and 1990s together with some contemporary instrumental and backing vocal parts. Rare Genius: The Undiscovered Masters Remix album by Ray Charles Released 2010 Genre Soul Length 41:36 Label Concord Producer Ray Charles, John Burk Ray Charles chronology Ray Sings, Basie Swings (2006) Rare Genius: The Undiscovered Masters (2010) Professional ratings Review scores Source Rating Allmusic (link) PopMatters (link) All About <b>Jazz</b> (link) favorable...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -347,7 +347,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Tangerine_Records_(1962)",
/*link*/ "/ROOT/content/zimfile/A/Tangerine_Records_(1962)",
/*title*/ "Tangerine Records (1962)",
/*snippet*/ R"SNIPPET(...in 1962. ABC-Paramount Records promoted and distributed it. Early singles labels were orange and later became black, red and white. Many of the later recordings are now sought after in "Northern Soul" circles. In 1973 Charles left ABC, closed Tangerine and started Crossover Records. Ray Charles Enterprises owns the catalog. Tangerine Records Parent company ABC-Paramount Records Founded 1962 Founder Ray Charles Defunct 1973 Distributor(s) ABC-Paramount Records Genre R&amp;B, soul music, <b>jazz</b> music...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -355,7 +355,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Ray_Sings,_Basie_Swings",
/*link*/ "/ROOT/content/zimfile/A/Ray_Sings,_Basie_Swings",
/*title*/ "Ray Sings, Basie Swings",
/*snippet*/ R"SNIPPET(...from 1973 with newly recorded instrumental tracks by the contemporary Count Basie Orchestra. Professional ratings Review scores Source Rating AllMusic Ray Sings, Basie Swings Compilation album by Ray Charles, Count Basie Orchestra Released October 3, 2006 (2006-10-03) Recorded Mid-1970s, February - May 2006 Studio Los Angeles Genre Soul, <b>jazz</b>, Swing Label Concord/Hear Music Producer Gregg Field Ray Charles chronology Genius &amp; Friends (2005) Ray Sings, Basie Swings (2006) Rare Genius: The Undiscovered Masters (2010)...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -363,7 +363,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/I_Remember_Brother_Ray",
/*link*/ "/ROOT/content/zimfile/A/I_Remember_Brother_Ray",
/*title*/ "I Remember Brother Ray",
/*snippet*/ R"SNIPPET(...is an album by saxophonist David "Fathead" Newman, paying tribute to his bandleader and mentor Ray Charles, which was recorded in 2004 and released on the HighNote label the following year. I Remember Brother Ray Studio album by David "Fathead" Newman Released January 11, 2005 Recorded August 14, 2004 Studio Van Gelder Studio, Englewood Cliffs, NJ Genre <b>Jazz</b> Length 50:39 Label HighNote HCD 7135 Producer David "Fathead" Newman, Houston Person David "Fathead" Newman chronology Song for the New Man (2004) I Remember Brother Ray (2005) Cityscape (2006)...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -371,7 +371,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Light_Out_of_Darkness_(A_Tribute_to_Ray_Charles)",
/*link*/ "/ROOT/content/zimfile/A/Light_Out_of_Darkness_(A_Tribute_to_Ray_Charles)",
/*title*/ "Light Out of Darkness (A Tribute to Ray Charles)",
/*snippet*/ R"SNIPPET(...to Ray Charles) is a 1993 studio album by Shirley Horn, recorded in tribute to Ray Charles. Light Out of Darkness (A Tribute to Ray Charles) Studio album by Shirley Horn Released 1993 Recorded April 30 and May 13, 1993, Clinton Recording Studios, New York City Genre Vocal <b>jazz</b> Length 62:53 Label Verve Producer Shirley Horn, Sheila Mathis, Richard Seidel, Lynn Butterer Shirley Horn chronology Here's to Life (1992) Light Out of Darkness (A Tribute to Ray Charles) (1993) I Love You, Paris (1994)...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -379,7 +379,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Soul_Meeting",
/*link*/ "/ROOT/content/zimfile/A/Soul_Meeting",
/*title*/ "Soul Meeting",
/*snippet*/ R"SNIPPET(...in 1957 and released in 1961 on Atlantic Records. The album was later re-issued together with the other CharlesJackson recording, Soul Brothers, on a 2 CD compilation together with other 'bonus' tracks from the same recording sessions. Professional ratings Review scores Source Rating Down Beat (Original Lp release) AllMusic link Soul Meeting Studio album by Ray Charles, Milt Jackson Released 1961 Recorded April 10, 1958 Genre R&amp;B, <b>jazz</b> Length 37:43 Label Atlantic Producer Tom Dowd Ray Charles chronology The Genius Sings the Blues (1961) Soul Meeting (1961) The Genius After Hours (1961) Alternative cover compilation CD re-issue...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -387,7 +387,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Ray_Charles_in_Concert",
/*link*/ "/ROOT/content/zimfile/A/Ray_Charles_in_Concert",
/*title*/ "Ray Charles in Concert",
/*snippet*/ R"SNIPPET(...between 1958 and 1975. In Concert Compilation album by Ray Charles Released 2003 Recorded Newport <b>Jazz</b> Festival (1958 July 5), Herndon Stadium Atlanta (1959 May 19), Sportpalast Berlin (1962 March 6), Shrine Auditorium Los Angeles (1964 September 20), Tokyo (1975 November 27) and Yokohama (1975 November 30) Genre R&amp;B, soul Length 2 hours Label Rhino Handmade Producer Nesuhi Ertegun (Newport), Zenas Sears (Atlanta), Norman Granz (Berlin), Sid Feller (Los Angeles) and Ray Charles (Tokyo / Yokohama)...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -395,7 +395,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Sticks_and_Stones_(Titus_Turner_song)",
/*link*/ "/ROOT/content/zimfile/A/Sticks_and_Stones_(Titus_Turner_song)",
/*title*/ "Sticks and Stones (Titus Turner song)",
/*snippet*/ R"SNIPPET(...in a 1960 version by Ray Charles, who added the Latin drum part. It was his first R&amp;B hit with ABC-Paramount, followed in 1961 with "Hit The Road Jack". The song was also covered by Jerry Lee Lewis, The Zombies, Wanda Jackson and The Kingsmen, as well as Joe Cocker on Mad Dogs and Englishmen, and Elvis Costello in 1994 on the extended play version of Kojak Variety. In 1997, <b>jazz</b> singer Roseanna Vitro included the tune in her tribute to Charles, Catchin Some Rays: The Music of Ray Charles....)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -403,7 +403,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Do_the_Twist!_with_Ray_Charles",
/*link*/ "/ROOT/content/zimfile/A/Do_the_Twist!_with_Ray_Charles",
/*title*/ "Do the Twist! with Ray Charles",
/*snippet*/ R"SNIPPET(...peaked at #11. Do the Twist! with Ray Charles Greatest hits album by Ray Charles Released 1961 Recorded 1954-1960 Genre R&amp;B, Soul, <b>Jazz</b> Length 32:39 Label Atlantic Ray Charles chronology The Genius Sings the Blues (1961) Do the Twist! with Ray Charles (1961) Soul Meeting (1961) Professional ratings Review scores Source Rating Allmusic (link) The Rolling Stone Record Guide In 1963, the album got a new cover and was renamed The Greatest Ray Charles. Track listing and catalog number (Atlantic 8054) remained the same....)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -411,7 +411,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/The_Great_Ray_Charles",
/*link*/ "/ROOT/content/zimfile/A/The_Great_Ray_Charles",
/*title*/ "The Great Ray Charles",
/*snippet*/ R"SNIPPET(...<b>jazz</b> album. Later CD re-issues often include as a bonus, six of eight tracks from The Genius After Hours. The original cover was by Marvin Israel. Professional ratings Review scores Source Rating Allmusic The Great Ray Charles Studio album by Ray Charles Released August 1957 Recorded April 30 - November 26, 1956 in New York City Genre Bebop Length 37:37 Label Atlantic Producer Ahmet Ertegün, Jerry Wexler Ray Charles chronology Ray Charles (or, Hallelujah I Love Her So) (1957) The Great Ray......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -419,7 +419,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Ray_Charles_Live",
/*link*/ "/ROOT/content/zimfile/A/Ray_Charles_Live",
/*title*/ "Ray Charles Live",
/*snippet*/ R"SNIPPET(...<b>Jazz</b> Festival in 1958 and at Herndon Stadium in Atlanta in 1959, respectively). Later CD re-issues of this compilation include an additional, previously unreleased, track from the 1958 Newport concert, "Swanee River Rock." Live Live album by Ray Charles Released 1973 Recorded July 5, 1958 / May 28, 1959 Genre Soul, R&amp;B Length 71:55 Label Atlantic 503 Producer Nesuhi Ertegün / Zenas Sears Ray Charles chronology From the Pages of My Mind (1986) Live (1973) Just Between Us (1988) Professional......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -427,7 +427,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Blue_Funk_(Ray_Charles_song)",
/*link*/ "/ROOT/content/zimfile/A/Blue_Funk_(Ray_Charles_song)",
/*title*/ "Soul Brothers",
/*snippet*/ R"SNIPPET(...on the original LP releases. Soul Brothers Studio album by Ray Charles, Milt Jackson Released June 1958 Recorded September 12, 1957 (Tracks 1-2) and April 10, 1958 (Tracks 3-7), in New York City Genre R&amp;B, <b>jazz</b> Length 38:42 Label Atlantic, Studio One Producer Nesuhi Ertegun Ray Charles chronology Yes Indeed! (1958) Soul Brothers (1958) What'd I Say (1959) alternate release cover compilation CD / re-issue Professional ratings Review scores Source Rating AllMusic Down Beat (Original Lp release)...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -435,7 +435,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Soul_Brothers",
/*link*/ "/ROOT/content/zimfile/A/Soul_Brothers",
/*title*/ "Soul Brothers",
/*snippet*/ R"SNIPPET(...and the eleventh studio album by Milt Jackson and released by Atlantic Records in 1958. The album was later re-issued in a 2 CD compilation together with the other CharlesJackson album Soul Meeting and included additional tracks from the same recording sessions not present on the original LP releases. Soul Brothers Studio album by Ray Charles, Milt Jackson Released June 1958 Recorded September 12, 1957 (Tracks 1-2) and April 10, 1958 (Tracks 3-7), in New York City Genre R&amp;B, <b>jazz</b> Length 38:42 Label Atlantic, Studio One Producer Nesuhi Ertegun Ray Charles chronology Yes Indeed! (1958) Soul Brothers (1958) What'd I Say (1959) alternate release cover compilation CD / re-issue Professional ratings Review scores Source Rating AllMusic Down Beat (Original Lp release)...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -443,7 +443,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Ray_Charles_and_Betty_Carter",
/*link*/ "/ROOT/content/zimfile/A/Ray_Charles_and_Betty_Carter",
/*title*/ "Ray Charles and Betty Carter",
/*snippet*/ R"SNIPPET(...Betty Carter Studio album by Ray Charles and Betty Carter Released August 1961 Recorded August 23, 1960 - June 14, 1961 Genre <b>Jazz</b> Length 41:38 Label ABC Producer Sid Feller Ray Charles chronology Dedicated to You (1961) Ray Charles and Betty Carter (1961) The Genius Sings the Blues (1961) Betty Carter chronology The Modern Sound of Betty Carter (1960) Ray Charles and Betty Carter (1961) 'Round Midnight (1962) Alternative cover / re-issue 1998 Rhino CD re-issue with Dedicated to You Professional ratings Review scores Source Rating Allmusic...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -451,7 +451,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Ingredients_in_a_Recipe_for_Soul",
/*link*/ "/ROOT/content/zimfile/A/Ingredients_in_a_Recipe_for_Soul",
/*title*/ "Ingredients in a Recipe for Soul",
/*snippet*/ R"SNIPPET(...6, 1960April 28, 1963 Genre R&amp;B, soul, country soul, vocal <b>jazz</b> Label ABC 465 Producer Sid Feller Ray Charles chronology Modern Sounds in Country and Western Music, Vol. 2 (1962) Ingredients in a Recipe for Soul (1963) Sweet &amp; Sour Tears (1964) Alternative cover 1997 Rhino CD re-issue with Have a Smile with Me In 1990, the album was released on compact disc by DCC with four bonus tracks. In 1997, it was packaged together with 1964's Have a Smile with Me on a two-for-one CD reissue on Rhino with historical liner notes. Professional ratings Review scores......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -459,7 +459,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/The_Genius_Sings_the_Blues",
/*link*/ "/ROOT/content/zimfile/A/The_Genius_Sings_the_Blues",
/*title*/ "The Genius Sings the Blues",
/*snippet*/ R"SNIPPET(...<b>jazz</b>, and southern R&amp;B. The photo for the album cover was taken by renowned photographer Lee Friedlander. The Genius Sings the Blues was reissued in 2003 by Rhino Entertainment with liner notes by Billy Taylor. The Genius Sings the Blues Compilation album by Ray Charles Released October 1961 Recorded 19521960 Genre Rhythm and blues, piano blues, soul Length 34:19 Label Atlantic SD-8052 Producer Ahmet Ertegün, Jerry Wexler Ray Charles chronology Ray Charles and Betty Carter (1961) The Genius......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -467,7 +467,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/The_Genius_of_Ray_Charles",
/*link*/ "/ROOT/content/zimfile/A/The_Genius_of_Ray_Charles",
/*title*/ "The Genius of Ray Charles",
/*snippet*/ R"SNIPPET(...the sixth studio album by American recording artist Ray Charles, released in 1959 by Atlantic Records. The album eschewed the soul sound of his 1950s recordings, which fused <b>jazz</b>, gospel, and blues, for swinging pop with big band arrangements. It comprises a first half of big band songs and a second half of string-backed ballads. The Genius of Ray Charles sold fewer than 500,000 copies and charted at number 17 on the Billboard 200. "Let the Good Times Roll" and "Don't Let the Sun Catch You Cryin'" were released as singles in 1959. The Genius of Ray Charles Studio album by Ray Charles Released October 1959 Recorded May 6 and June 23, 1959 at 6 West Recording in New......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -475,7 +475,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Ray_Charles_in_Person",
/*link*/ "/ROOT/content/zimfile/A/Ray_Charles_in_Person",
/*title*/ "Ray Charles in Person",
/*snippet*/ R"SNIPPET(...night in Atlanta, Georgia at Morris Brown College's Herndon Stadium. All tracks from this album together with those from Ray Charles at Newport were also released on the 1987 Atlantic compilation CD, Ray Charles Live. Ray Charles: In Person Live album by Ray Charles Released July 1960 Recorded May 28, 1959 Genre R&amp;B Length 29:19 Label Atlantic Producer Harris Zenas Ray Charles chronology The Genius of Ray Charles (1959) Ray Charles: In Person (1960) Genius + Soul = <b>Jazz</b> (1961) Re-issue cover 1987 re-issue / compilation Professional ratings Review scores Source Rating Allmusic The album was recorded by the concert sponsor, radio station WAOK. The station's lead disk jockey, Zenas "Daddy" Sears, recorded the album from the audience using a single microphone. The album is noted for its technical excellence in balancing band, singer, and audience, and also for its documentation of the jazzy R&amp;B Ray Charles sound prior to his great crossover success....)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -483,7 +483,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Don't_Let_the_Sun_Catch_You_Cryin'",
/*link*/ "/ROOT/content/zimfile/A/Don't_Let_the_Sun_Catch_You_Cryin'",
/*title*/ "Don&apos;t Let the Sun Catch You Cryin&apos;",
/*snippet*/ R"SNIPPET(...R&amp;B Sides" and No. 95 on the Billboard Hot 100. It was also recorded by Jackie DeShannon on her 1965 album This is Jackie De Shannon, Paul McCartney on his 1990 live album Tripping the Live Fantastic, Jex Saarelaht and Kate Ceberano on their album Open the Door - Live at Mietta's (1992) and <b>jazz</b> singer Roseanna Vitro on her 1997 album Catchin Some Rays: The Music of Ray Charles. Karin Krog and Steve Kuhn include it on their 2005 album, Together Again. Steve Alaimo released a version in 1963...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -491,7 +491,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/I_Don't_Need_No_Doctor",
/*link*/ "/ROOT/content/zimfile/A/I_Don't_Need_No_Doctor",
/*title*/ "I Don&apos;t Need No Doctor",
/*snippet*/ R"SNIPPET(...<b>jazz</b> guitar player John Scofield recorded a version for his album That's What I Say: John Scofield Plays the Music of Ray Charles in 2005, featuring the blues guitarist John Mayer on additional guitar and vocals. Mayer covered the song again with his band during his tour in summer 2007. A recorded live version from a Los Angeles show during that tour is available on Mayer's CD/DVD release Where the Light Is. A Ray Charles tribute album also provided the impetus for <b>jazz</b> singer Roseanna Vitro's......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -499,7 +499,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/If_You_Go_Away",
/*link*/ "/ROOT/content/zimfile/A/If_You_Go_Away",
/*title*/ "If You Go Away",
/*snippet*/ R"SNIPPET(...<b>Jazz</b> Length 3:49 Label Epic Records Songwriter(s) Jacques Brel, Rod McKuen Producer(s) Bob Morgan Damita Jo singles chronology "Gotta Travel On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the Billboard Hot 100, and went to #8 in......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -507,7 +507,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Anthology_(Ray_Charles_album)",
/*link*/ "/ROOT/content/zimfile/A/Anthology_(Ray_Charles_album)",
/*title*/ "Anthology (Ray Charles album)",
/*snippet*/ R"SNIPPET(...Charles' '60s and '70s ABC-Paramount material", while Rhino Records, the issuing label, refers to it in the liner notes as "the compact disc edition of Ray Charles' Greatest Hits", alluding to the two Rhino LPs issued the same year. It is one of the first CDs to be released by Rhino. Anthology Greatest hits album by Ray Charles Released 1988 Recorded 1960-1972 Genre R&amp;B soul <b>jazz</b> piano blues Length 67:25 (original), 66:18 (re-release) Label Rhino Producer Ray Charles Steve Hoffman Richard Foos Ray Charles chronology Just Between Us (1988) Anthology (1988) Would You Believe? (1990) Posthumous cover Professional ratings Review scores Source Rating AllMusic Charles, who retained the master rights (currently controlled by his estate since his June 2004 passing) to his ABC-Paramount recordings, supervised a remixing of the 20 songs on this compilation especially for this......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -515,7 +515,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Ray_Charles",
/*link*/ "/ROOT/content/zimfile/A/Ray_Charles",
/*title*/ "Ray Charles",
/*snippet*/ R"SNIPPET(...1960s Background information Birth name Ray Charles Robinson Born (1930-09-23)September 23, 1930 Albany, Georgia, U.S. Died June 10, 2004(2004-06-10) (aged 73) Beverly Hills, California, U.S. Genres R&amp;B soul blues gospel country <b>jazz</b> rock and roll Occupation(s) musician singer songwriter composer Instruments Vocals piano Years active 19472004 Labels Atlantic ABC Tangerine Warner Bros. Swing Time Concord Columbia Flashback Associated acts The Raelettes USA for Africa Billy Joel Gladys Knight Website raycharles.com Charles pioneered the soul music genre during the 1950s by combining blues, rhythm and blues, and gospel styles into the music he recorded for Atlantic. He contributed to the integration of country music......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -523,7 +523,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/The_Pages_of_My_Mind",
/*link*/ "/ROOT/content/zimfile/A/The_Pages_of_My_Mind",
/*title*/ "Ray Charles",
/*snippet*/ R"SNIPPET(...<b>jazz</b> rock and roll Occupation(s) musician singer songwriter composer Instruments Vocals piano Years active 19472004 Labels Atlantic ABC Tangerine Warner Bros. Swing Time Concord Columbia Flashback Associated acts The Raelettes USA for Africa Billy Joel Gladys Knight Website raycharles.com Charles pioneered the soul music genre during the 1950s by combining blues, rhythm and blues, and gospel styles into the music he recorded for Atlantic. He contributed to the integration of country music......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -531,7 +531,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Here_We_Go_Again_(Ray_Charles_song)",
/*link*/ "/ROOT/content/zimfile/A/Here_We_Go_Again_(Ray_Charles_song)",
/*title*/ "Here We Go Again (Ray Charles song)",
/*snippet*/ R"SNIPPET(...was first covered in an instrumental <b>jazz</b> format, and many of the more recent covers have been sung as duets, such as one with Willie Nelson and Norah Jones with Wynton Marsalis accompanying. The song was released on their 2011 tribute album Here We Go Again: Celebrating the Genius of Ray Charles. The song lent its name to Red Steagall's 2007 album as well. Cover versions have appeared on compilation albums by a number of artists, even some who did not release "Here We Go Again" as a single....)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -539,7 +539,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Modern_Sounds_in_Country_and_Western_Music",
/*link*/ "/ROOT/content/zimfile/A/Modern_Sounds_in_Country_and_Western_Music",
/*title*/ "Modern Sounds in Country and Western Music",
/*snippet*/ R"SNIPPET(...<b>jazz</b>. Charles produced the album with Sid Feller, who helped the singer select songs to record, and performed alongside saxophonist Hank Crawford, a string section conducted by Marty Paich, and a big band arranged by Gil Fuller and Gerald Wilson. Modern Sounds in Country and Western Music was an immediate critical and commercial success. The album and its four hit singles brought Charles greater mainstream notice and recognition in the pop market, as well as airplay on both R&amp;B and country radio......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -692,11 +692,11 @@ struct TestData
std::string url = "/ROOT/search?" + query;
if ( start >= 0 ) {
url += "&start=" + to_string(start);
url += "&start=" + std::to_string(start);
}
if ( resultsPerPage != 0 ) {
url += "&pageLength=" + to_string(resultsPerPage);
url += "&pageLength=" + std::to_string(resultsPerPage);
}
return url;
@@ -750,9 +750,9 @@ struct TestData
)";
const size_t lastResultIndex = std::min(totalResultCount, firstResultIndex + results.size() - 1);
header = replace(header, "FIRSTRESULT", to_string(firstResultIndex));
header = replace(header, "LASTRESULT", to_string(lastResultIndex));
header = replace(header, "RESULTCOUNT", to_string(totalResultCount));
header = replace(header, "FIRSTRESULT", std::to_string(firstResultIndex));
header = replace(header, "LASTRESULT", std::to_string(lastResultIndex));
header = replace(header, "RESULTCOUNT", std::to_string(totalResultCount));
header = replace(header, "PATTERN", getPattern());
return header;
}
@@ -824,9 +824,9 @@ struct TestData
const auto realResultsPerPage = resultsPerPage?resultsPerPage:25;
const auto url = makeUrl(query + "&format=xml", firstResultIndex, realResultsPerPage);
header = replace(header, "URL", replace(url, "&", "&amp;"));
header = replace(header, "FIRSTRESULT", to_string(firstResultIndex));
header = replace(header, "ITEMCOUNT", to_string(realResultsPerPage));
header = replace(header, "RESULTCOUNT", to_string(totalResultCount));
header = replace(header, "FIRSTRESULT", std::to_string(firstResultIndex));
header = replace(header, "ITEMCOUNT", std::to_string(realResultsPerPage));
header = replace(header, "RESULTCOUNT", std::to_string(totalResultCount));
header = replace(header, "PATTERN", getPattern());
auto queryLang = getLang();
if (queryLang.empty()) {
@@ -937,7 +937,7 @@ TEST_F(ServerTest, searchResults)
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/We_Gonna_Move_to_the_Outskirts_of_Town",
/*link*/ "/ROOT/content/zimfile/A/We_Gonna_Move_to_the_Outskirts_of_Town",
/*title*/ "We Gonna Move to the Outskirts of Town",
/*snippet*/ R"SNIPPET(...to the Outskirts of Town "We Gonna Move to the Outskirts of Town" is a country blues song recorded September 3, 1936 by Casey Bill Weldon (voice and guitar). The song has been covered by many other musicians, most often under the title "I'm Gonna Move to the Outskirts of Town", and sometimes simply Outskirts of Town. All recordings seem to credit Weldon as songwriter, often as Weldon or as Will Weldon or as William Weldon. Some cover versions give credit also to Andy <b>Razaf</b> and/or to Roy Jacobs....)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -955,7 +955,7 @@ TEST_F(ServerTest, searchResults)
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Eleanor_Rigby",
/*link*/ "/ROOT/content/zimfile/A/Eleanor_Rigby",
/*title*/ "Eleanor Rigby",
/*snippet*/ R"SNIPPET(...-side "<b>Yellow</b> Submarine" (double A-side) Released 5 August 1966 (1966-08-05) Format 7-inch single Recorded 2829 April &amp; 6 June 1966 Studio EMI, London Genre Baroque pop, art rock Length 2:08 Label Parlophone (UK), Capitol (US) Songwriter(s) LennonMcCartney Producer(s) George Martin The Beatles singles chronology "Paperback Writer" (1966) "Eleanor Rigby" / "<b>Yellow</b> Submarine" (1966) "Strawberry Fields Forever" / "Penny Lane" (1967) Music video "Eleanor Rigby" on YouTube The song continued the......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -963,7 +963,7 @@ TEST_F(ServerTest, searchResults)
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/If_You_Go_Away",
/*link*/ "/ROOT/content/zimfile/A/If_You_Go_Away",
/*title*/ "If You Go Away",
/*snippet*/ R"SNIPPET(...standard and has been recorded by many artists, including Greta Keller, for whom some say McKuen wrote the lyrics. "If You Go Away" Single by Damita Jo from the album If You Go Away B-side "<b>Yellow</b> Days" Released 1966 Genre Jazz Length 3:49 Label Epic Records Songwriter(s) Jacques Brel, Rod McKuen Producer(s) Bob Morgan Damita Jo singles chronology "Gotta Travel On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -981,7 +981,7 @@ TEST_F(ServerTest, searchResults)
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Eleanor_Rigby",
/*link*/ "/ROOT/content/zimfile/A/Eleanor_Rigby",
/*title*/ "Eleanor Rigby",
/*snippet*/ R"SNIPPET(...-side "<b>Yellow</b> Submarine" (double A-side) Released 5 August 1966 (1966-08-05) Format 7-inch single Recorded 2829 April &amp; 6 June 1966 Studio EMI, London Genre Baroque pop, art rock Length 2:08 Label Parlophone (UK), Capitol (US) Songwriter(s) LennonMcCartney Producer(s) George Martin The Beatles singles chronology "Paperback Writer" (1966) "Eleanor Rigby" / "<b>Yellow</b> Submarine" (1966) "Strawberry Fields Forever" / "Penny Lane" (1967) Music video "Eleanor Rigby" on YouTube The song continued the......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -989,7 +989,7 @@ TEST_F(ServerTest, searchResults)
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/If_You_Go_Away",
/*link*/ "/ROOT/content/zimfile/A/If_You_Go_Away",
/*title*/ "If You Go Away",
/*snippet*/ R"SNIPPET(...standard and has been recorded by many artists, including Greta Keller, for whom some say McKuen wrote the lyrics. "If You Go Away" Single by Damita Jo from the album If You Go Away B-side "<b>Yellow</b> Days" Released 1966 Genre Jazz Length 3:49 Label Epic Records Songwriter(s) Jacques Brel, Rod McKuen Producer(s) Bob Morgan Damita Jo singles chronology "Gotta Travel On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -1007,7 +1007,7 @@ TEST_F(ServerTest, searchResults)
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/Eleanor_Rigby",
/*link*/ "/ROOT/content/zimfile/A/Eleanor_Rigby",
/*title*/ "Eleanor Rigby",
/*snippet*/ R"SNIPPET(...-side "<b>Yellow</b> Submarine" (double A-side) Released 5 August 1966 (1966-08-05) Format 7-inch single Recorded 2829 April &amp; 6 June 1966 Studio EMI, London Genre Baroque pop, art rock Length 2:08 Label Parlophone (UK), Capitol (US) Songwriter(s) LennonMcCartney Producer(s) George Martin The Beatles singles chronology "Paperback Writer" (1966) "Eleanor Rigby" / "<b>Yellow</b> Submarine" (1966) "Strawberry Fields Forever" / "Penny Lane" (1967) Music video "Eleanor Rigby" on YouTube The song continued the......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -1015,7 +1015,7 @@ TEST_F(ServerTest, searchResults)
),
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/If_You_Go_Away",
/*link*/ "/ROOT/content/zimfile/A/If_You_Go_Away",
/*title*/ "If You Go Away",
/*snippet*/ R"SNIPPET(...standard and has been recorded by many artists, including Greta Keller, for whom some say McKuen wrote the lyrics. "If You Go Away" Single by Damita Jo from the album If You Go Away B-side "<b>Yellow</b> Days" Released 1966 Genre Jazz Length 3:49 Label Epic Records Songwriter(s) Jacques Brel, Rod McKuen Producer(s) Bob Morgan Damita Jo singles chronology "Gotta Travel On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -1322,7 +1322,7 @@ TEST_F(ServerTest, searchResults)
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/If_You_Go_Away",
/*link*/ "/ROOT/content/zimfile/A/If_You_Go_Away",
/*title*/ "If You Go Away",
/*snippet*/ R"SNIPPET(...<b>Travel</b> On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the Billboard Hot 100, and went to #8 in the UK. The complex melody is partly derivative of classical music - the poignant "But if you stay..." passage comes from Franz Liszt's......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -1330,7 +1330,7 @@ TEST_F(ServerTest, searchResults)
),
SEARCH_RESULT(
/*link*/ "/ROOT/example/Wikibooks.html",
/*link*/ "/ROOT/content/example/Wikibooks.html",
/*title*/ "Wikibooks",
/*snippet*/ R"SNIPPET(...<b>Travel</b> guide Wikidata Knowledge database Commons Media repository Meta Coordination MediaWiki MediaWiki software Phabricator MediaWiki bug tracker Wikimedia Labs MediaWiki development The Wikimedia Foundation is a non-profit organization that depends on your voluntarism and donations to operate. If you find Wikibooks or other projects hosted by the Wikimedia Foundation useful, please volunteer or make a donation. Your donations primarily helps to purchase server equipment, launch new projects......)SNIPPET",
/*bookTitle*/ "Wikibooks",
@@ -1351,7 +1351,7 @@ TEST_F(ServerTest, searchResults)
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT(
/*link*/ "/ROOT/zimfile/A/If_You_Go_Away",
/*link*/ "/ROOT/content/zimfile/A/If_You_Go_Away",
/*title*/ "If You Go Away",
/*snippet*/ R"SNIPPET(...<b>Travel</b> On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the Billboard Hot 100, and went to #8 in the UK. The complex melody is partly derivative of classical music - the poignant "But if you stay..." passage comes from Franz Liszt's......)SNIPPET",
/*bookTitle*/ "Ray Charles",

View File

@@ -3,6 +3,8 @@
#include "../include/server.h"
#include "../include/name_mapper.h"
#include "../include/tools.h"
#include <zim/entry.h>
#include <zim/item.h>
// Output generated via mustache templates sometimes contains end-of-line
// whitespace. This complicates representing the expected output of a unit-test

View File

@@ -31,6 +31,32 @@ using namespace kiwix;
namespace
{
// Some unit-tests may fail because of partial/missing ICU data. This test
// is intended to pinpoint to the root cause in such build environments.
TEST(stringTools, ICULanguageInfo)
{
ASSERT_GE(ICULanguageInfo("en").selfName(), "English");
ASSERT_GE(ICULanguageInfo("eng").selfName(), "English");
ASSERT_GE(ICULanguageInfo("fr").selfName(), "français");
ASSERT_GE(ICULanguageInfo("fra").selfName(), "français");
ASSERT_GE(ICULanguageInfo("de").selfName(), "Deutsch");
ASSERT_GE(ICULanguageInfo("deu").selfName(), "Deutsch");
ASSERT_GE(ICULanguageInfo("es").selfName(), "español");
ASSERT_GE(ICULanguageInfo("spa").selfName(), "español");
ASSERT_GE(ICULanguageInfo("it").selfName(), "italiano");
ASSERT_GE(ICULanguageInfo("ita").selfName(), "italiano");
ASSERT_GE(ICULanguageInfo("ru").selfName(), "русский");
ASSERT_GE(ICULanguageInfo("rus").selfName(), "русский");
ASSERT_GE(ICULanguageInfo("hy").selfName(), "հայերեն");
ASSERT_GE(ICULanguageInfo("hye").selfName(), "հայերեն");
ASSERT_GE(ICULanguageInfo("zh").selfName(), "中文");
ASSERT_GE(ICULanguageInfo("zho").selfName(), "中文");
ASSERT_GE(ICULanguageInfo("ar").selfName(), "العربية");
ASSERT_GE(ICULanguageInfo("ara").selfName(), "العربية");
ASSERT_GE(ICULanguageInfo("c++").selfName(), "c++");
}
TEST(stringTools, join)
{
std::vector<std::string> list = { "a", "b", "c" };