chinese search support
This commit is contained in:
parent
94f7578576
commit
fe1927f344
|
@ -44,7 +44,7 @@ tokio = { version = "0.2.18", features = ["macros"], optional = true }
|
||||||
warp = { version = "0.2.2", default-features = false, features = ["websocket"], optional = true }
|
warp = { version = "0.2.2", default-features = false, features = ["websocket"], optional = true }
|
||||||
|
|
||||||
# Search feature
|
# Search feature
|
||||||
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
|
elasticlunr-rs = { version = "2.3", optional = true }
|
||||||
ammonia = { version = "3", optional = true }
|
ammonia = { version = "3", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -550,7 +550,7 @@ impl Renderer for HtmlHandlebars {
|
||||||
{
|
{
|
||||||
let search = html_config.search.unwrap_or_default();
|
let search = html_config.search.unwrap_or_default();
|
||||||
if search.enable {
|
if search.enable {
|
||||||
super::search::create_files(&search, &destination, &book)?;
|
super::search::create_files(&search, &ctx.config.book.language, &destination, &book)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,23 @@ use crate::theme::searcher;
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
|
|
||||||
/// Creates all files required for search.
|
/// Creates all files required for search.
|
||||||
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
|
pub fn create_files(
|
||||||
let mut index = Index::new(&["title", "body", "breadcrumbs"]);
|
search_config: &Search,
|
||||||
|
lang: &Option<String>,
|
||||||
|
destination: &Path,
|
||||||
|
book: &Book,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut index = match lang {
|
||||||
|
Some(lang_str) => match lang_str.to_lowercase().as_str() {
|
||||||
|
"zh" => Index::with_language(
|
||||||
|
elasticlunr::Language::Chinese,
|
||||||
|
&["title", "body", "breadcrumbs"],
|
||||||
|
),
|
||||||
|
_ => Index::new(&["title", "body", "breadcrumbs"]),
|
||||||
|
},
|
||||||
|
None => Index::new(&["title", "body", "breadcrumbs"]),
|
||||||
|
};
|
||||||
|
|
||||||
let mut doc_urls = Vec::with_capacity(book.sections.len());
|
let mut doc_urls = Vec::with_capacity(book.sections.len());
|
||||||
|
|
||||||
for item in book.iter() {
|
for item in book.iter() {
|
||||||
|
@ -36,6 +51,7 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
|
||||||
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
|
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
|
||||||
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
|
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
|
||||||
utils::fs::write_file(destination, "elasticlunr.min.js", searcher::ELASTICLUNR_JS)?;
|
utils::fs::write_file(destination, "elasticlunr.min.js", searcher::ELASTICLUNR_JS)?;
|
||||||
|
utils::fs::write_file(destination, "lunr.zh.js", searcher::LUNR_ZH_JS)?;
|
||||||
debug!("Copying search files ✓");
|
debug!("Copying search files ✓");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -271,6 +271,7 @@
|
||||||
|
|
||||||
{{#if search_js}}
|
{{#if search_js}}
|
||||||
<script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
<script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="{{ path_to_root }}lunr.zh.js" type="text/javascript" charset="utf-8"></script>
|
||||||
<script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script>
|
<script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
<script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script>
|
<script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof define === 'function' && define.amd) {
|
||||||
|
// AMD. Register as an anonymous module.
|
||||||
|
define(factory)
|
||||||
|
} else if (typeof exports === 'object') {
|
||||||
|
/**
|
||||||
|
* Node. Does not work with strict CommonJS, but
|
||||||
|
* only CommonJS-like environments that support module.exports,
|
||||||
|
* like Node.
|
||||||
|
*/
|
||||||
|
module.exports = factory()
|
||||||
|
} else {
|
||||||
|
// Browser globals (root is window)
|
||||||
|
factory()(root.lunr);
|
||||||
|
}
|
||||||
|
}(this, function () {
|
||||||
|
return function (lunr) {
|
||||||
|
if ('undefined' === typeof lunr) {
|
||||||
|
throw new Error('Lunr is not present. Please include / require Lunr before this script.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* register specific locale function */
|
||||||
|
lunr.zh = function () {
|
||||||
|
this.pipeline.reset();
|
||||||
|
this.pipeline.add(
|
||||||
|
lunr.zh.trimmer,
|
||||||
|
lunr.zh.stopWordFilter,
|
||||||
|
lunr.zh.stemmer
|
||||||
|
);
|
||||||
|
|
||||||
|
// for lunr version 2
|
||||||
|
// this is necessary so that every searched word is also stemmed before
|
||||||
|
// in lunr <= 1 this is not needed, as it is done using the normal pipeline
|
||||||
|
if (this.searchPipeline) {
|
||||||
|
this.searchPipeline.reset();
|
||||||
|
this.searchPipeline.add(lunr.zh.stemmer)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
lunr.zh.tokenizer = function (str) {
|
||||||
|
if (!arguments.length || str === null || str === undefined) return [];
|
||||||
|
if (Array.isArray(str)) {
|
||||||
|
var arr = str.filter(function (token) {
|
||||||
|
if (token === null || token === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
arr = arr.map(function (t) {
|
||||||
|
return lunr.utils.toString(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
var out = [];
|
||||||
|
arr.forEach(function (item) {
|
||||||
|
var tokens = item.split(lunr.tokenizer.seperator);
|
||||||
|
out = out.concat(tokens);
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.toString().trim().split(lunr.tokenizer.seperator);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* lunr trimmer function */
|
||||||
|
lunr.zh.trimmer = function (_token) {
|
||||||
|
return _token;
|
||||||
|
}
|
||||||
|
|
||||||
|
lunr.Pipeline.registerFunction(lunr.zh.trimmer, 'trimmer-zh');
|
||||||
|
|
||||||
|
/* lunr stemmer function */
|
||||||
|
lunr.zh.stemmer = (function () {
|
||||||
|
/* and return a function that stems a word for the current locale */
|
||||||
|
return function (token) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
lunr.Pipeline.registerFunction(lunr.zh.stemmer, 'stemmer-zh');
|
||||||
|
|
||||||
|
lunr.zh.stopWordFilter = function (token) {
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
lunr.Pipeline.registerFunction(lunr.zh.stopWordFilter, 'stopWordFilter-zh');
|
||||||
|
};
|
||||||
|
}))
|
|
@ -4,3 +4,4 @@
|
||||||
pub static JS: &[u8] = include_bytes!("searcher.js");
|
pub static JS: &[u8] = include_bytes!("searcher.js");
|
||||||
pub static MARK_JS: &[u8] = include_bytes!("mark.min.js");
|
pub static MARK_JS: &[u8] = include_bytes!("mark.min.js");
|
||||||
pub static ELASTICLUNR_JS: &[u8] = include_bytes!("elasticlunr.min.js");
|
pub static ELASTICLUNR_JS: &[u8] = include_bytes!("elasticlunr.min.js");
|
||||||
|
pub static LUNR_ZH_JS: &[u8] = include_bytes!("lunr.zh.js");
|
||||||
|
|
|
@ -89,7 +89,7 @@ window.search = window.search || {};
|
||||||
path: a.pathname.replace(/^([^/])/,'/$1')
|
path: a.pathname.replace(/^([^/])/,'/$1')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to recreate a url string from its building blocks.
|
// Helper to recreate a url string from its building blocks.
|
||||||
function renderURL(urlobject) {
|
function renderURL(urlobject) {
|
||||||
var url = urlobject.protocol + "://" + urlobject.host;
|
var url = urlobject.protocol + "://" + urlobject.host;
|
||||||
|
@ -124,7 +124,7 @@ window.search = window.search || {};
|
||||||
return s.replace(/[&<>'"]/g, repl);
|
return s.replace(/[&<>'"]/g, repl);
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function formatSearchMetric(count, searchterm) {
|
function formatSearchMetric(count, searchterm) {
|
||||||
if (count == 1) {
|
if (count == 1) {
|
||||||
return count + " search result for '" + searchterm + "':";
|
return count + " search result for '" + searchterm + "':";
|
||||||
|
@ -134,7 +134,7 @@ window.search = window.search || {};
|
||||||
return count + " search results for '" + searchterm + "':";
|
return count + " search results for '" + searchterm + "':";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSearchResult(result, searchterms) {
|
function formatSearchResult(result, searchterms) {
|
||||||
var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
|
var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
|
||||||
teaser_count++;
|
teaser_count++;
|
||||||
|
@ -152,10 +152,10 @@ window.search = window.search || {};
|
||||||
|
|
||||||
return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
|
return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
|
||||||
+ '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>'
|
+ '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>'
|
||||||
+ '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
|
+ '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
|
||||||
+ teaser + '</span>';
|
+ teaser + '</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeTeaser(body, searchterms) {
|
function makeTeaser(body, searchterms) {
|
||||||
// The strategy is as follows:
|
// The strategy is as follows:
|
||||||
// First, assign a value to each word in the document:
|
// First, assign a value to each word in the document:
|
||||||
|
@ -257,6 +257,9 @@ window.search = window.search || {};
|
||||||
search_options = config.search_options;
|
search_options = config.search_options;
|
||||||
searchbar_outer = config.searchbar_outer;
|
searchbar_outer = config.searchbar_outer;
|
||||||
doc_urls = config.doc_urls;
|
doc_urls = config.doc_urls;
|
||||||
|
if (config.index.lang == "Chinese") {
|
||||||
|
elasticlunr.tokenizer = elasticlunr.zh.tokenizer
|
||||||
|
}
|
||||||
searchindex = elasticlunr.Index.load(config.index);
|
searchindex = elasticlunr.Index.load(config.index);
|
||||||
|
|
||||||
// Set up events
|
// Set up events
|
||||||
|
@ -271,7 +274,7 @@ window.search = window.search || {};
|
||||||
// If reloaded, do the search or mark again, depending on the current url parameters
|
// If reloaded, do the search or mark again, depending on the current url parameters
|
||||||
doSearchOrMarkFromUrl();
|
doSearchOrMarkFromUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
function unfocusSearchbar() {
|
function unfocusSearchbar() {
|
||||||
// hacky, but just focusing a div only works once
|
// hacky, but just focusing a div only works once
|
||||||
var tmp = document.createElement('input');
|
var tmp = document.createElement('input');
|
||||||
|
@ -280,7 +283,7 @@ window.search = window.search || {};
|
||||||
tmp.focus();
|
tmp.focus();
|
||||||
tmp.remove();
|
tmp.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// On reload or browser history backwards/forwards events, parse the url and do search or mark
|
// On reload or browser history backwards/forwards events, parse the url and do search or mark
|
||||||
function doSearchOrMarkFromUrl() {
|
function doSearchOrMarkFromUrl() {
|
||||||
// Check current URL for search request
|
// Check current URL for search request
|
||||||
|
@ -313,7 +316,7 @@ window.search = window.search || {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eventhandler for keyevents on `document`
|
// Eventhandler for keyevents on `document`
|
||||||
function globalKeyHandler(e) {
|
function globalKeyHandler(e) {
|
||||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text') { return; }
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text') { return; }
|
||||||
|
@ -338,8 +341,8 @@ window.search = window.search || {};
|
||||||
unfocusSearchbar();
|
unfocusSearchbar();
|
||||||
searchresults.firstElementChild.classList.add("focus");
|
searchresults.firstElementChild.classList.add("focus");
|
||||||
} else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE
|
} else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE
|
||||||
|| e.keyCode === UP_KEYCODE
|
|| e.keyCode === UP_KEYCODE
|
||||||
|| e.keyCode === SELECT_KEYCODE)) {
|
|| e.keyCode === SELECT_KEYCODE)) {
|
||||||
// not `:focus` because browser does annoying scrolling
|
// not `:focus` because browser does annoying scrolling
|
||||||
var focused = searchresults.querySelector("li.focus");
|
var focused = searchresults.querySelector("li.focus");
|
||||||
if (!focused) return;
|
if (!focused) return;
|
||||||
|
@ -363,7 +366,7 @@ window.search = window.search || {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSearch(yes) {
|
function showSearch(yes) {
|
||||||
if (yes) {
|
if (yes) {
|
||||||
search_wrap.classList.remove('hidden');
|
search_wrap.classList.remove('hidden');
|
||||||
|
@ -396,7 +399,7 @@ window.search = window.search || {};
|
||||||
showSearch(false);
|
showSearch(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eventhandler for keyevents while the searchbar is focused
|
// Eventhandler for keyevents while the searchbar is focused
|
||||||
function searchbarKeyUpHandler() {
|
function searchbarKeyUpHandler() {
|
||||||
var searchterm = searchbar.value.trim();
|
var searchterm = searchbar.value.trim();
|
||||||
|
@ -414,7 +417,7 @@ window.search = window.search || {};
|
||||||
// Remove marks
|
// Remove marks
|
||||||
marker.unmark();
|
marker.unmark();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor .
|
// Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor .
|
||||||
// `action` can be one of "push", "replace", "push_if_new_search_else_replace"
|
// `action` can be one of "push", "replace", "push_if_new_search_else_replace"
|
||||||
// and replaces or pushes a new browser history item.
|
// and replaces or pushes a new browser history item.
|
||||||
|
@ -439,7 +442,7 @@ window.search = window.search || {};
|
||||||
history.replaceState({}, document.title, renderURL(url));
|
history.replaceState({}, document.title, renderURL(url));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function doSearch(searchterm) {
|
function doSearch(searchterm) {
|
||||||
|
|
||||||
// Don't search the same twice
|
// Don't search the same twice
|
||||||
|
@ -470,7 +473,7 @@ window.search = window.search || {};
|
||||||
|
|
||||||
fetch(path_to_root + 'searchindex.json')
|
fetch(path_to_root + 'searchindex.json')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(json => init(json))
|
.then(json => init(json))
|
||||||
.catch(error => { // Try to load searchindex.js if fetch failed
|
.catch(error => { // Try to load searchindex.js if fetch failed
|
||||||
var script = document.createElement('script');
|
var script = document.createElement('script');
|
||||||
script.src = path_to_root + 'searchindex.js';
|
script.src = path_to_root + 'searchindex.js';
|
||||||
|
|
Loading…
Reference in New Issue