Added support for lazy rendering of search results

This commit is contained in:
squidfunk 2017-08-02 14:09:13 +02:00
parent ef1c5a4043
commit bbb3455c52
6 changed files with 88 additions and 48 deletions

View File

@ -267,6 +267,10 @@ Furthermore, if `repo_url` points to a GitHub, BitBucket or GitLab repository,
the respective service logo will be shown next to the name of the repository.
Additionally, for GitHub, the number of stars and forks is shown.
If the repository is hosted in a private environment, the service logo can be
set explicitly by setting `extra.repo_icon` to `github`, `gitlab` or
`bitbucket`.
!!! warning "Why is there an edit button at the top of every article?"
If the `repo_url` is set to a GitHub or BitBucket repository, and the
@ -382,6 +386,7 @@ macro `t`:
"search.result.none": "No matching documents",
"search.result.one": "1 matching document",
"search.result.other": "# matching documents",
"search.tokenizer": "[\s\-]+",
"source.link.title": "Go to repository",
"toc.title": "Table of contents"
}[key] }}{% endmacro %}
@ -398,6 +403,8 @@ section on [overriding partials][18] and the general guide on
#### Site search
##### Language
Site search is implemented using [lunr.js][21], which includes stemmers for the
English language by default, while stemmers for other languages are included
with [lunr-languages][22], both of which are integrated with this theme. Support

File diff suppressed because one or more lines are too long

View File

@ -150,7 +150,7 @@
{% endblock %}
</div>
{% block scripts %}
<script src="{{ base_url }}/assets/javascripts/application-7a3ab08b28.js"></script>
<script src="{{ base_url }}/assets/javascripts/application-3b8048ec29.js"></script>
{% set languages = lang.t("search.languages").split(",") %}
{% if languages | length and languages[0] != "" %}
{% set path = base_url + "/assets/javascripts/lunr" %}

View File

@ -11,7 +11,7 @@
"search.result.none": "No matching documents",
"search.result.one": "1 matching document",
"search.result.other": "# matching documents",
"search.tokenizer": "",
"search.tokenizer": "[\s\-]+",
"source.link.title": "Go to repository",
"toc.title": "Table of contents"
}[key] }}{% endmacro %}

View File

@ -23,6 +23,30 @@
import escape from "escape-string-regexp"
import lunr from "expose-loader?lunr!lunr"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Truncate a string after the given number of character
*
* This is not a reasonable approach, since the summaries kind of suck. It
* would be better to create something more intelligent, highlighting the
* search occurrences and making a better summary out of it.
*
* @param {string} string - String to be truncated
* @param {number} n - Number of characters
* @return {string} Truncated string
*/
const truncate = (string, n) => {
let i = n
if (string.length > i) {
while (string[i] !== " " && --i > 0);
return `${string.substring(0, i)}...`
}
return string
}
/* ----------------------------------------------------------------------------
* Class
* ------------------------------------------------------------------------- */
@ -42,6 +66,7 @@ export default class Result {
* @property {Array<string>} lang_ - Search languages
* @property {Object} message_ - Search result messages
* @property {Object} index_ - Search index
* @property {Array<Function>} stack_ - Search result stack
* @property {string} value_ - Last input value
*
* @param {(string|HTMLElement)} el - Selector or HTML element
@ -81,26 +106,6 @@ export default class Result {
.map(lang => lang.trim())
}
/**
* Truncate a string after the given number of character
*
* This is not a reasonable approach, since the summaries kind of suck. It
* would be better to create something more intelligent, highlighting the
* search occurrences and making a better summary out of it.
*
* @param {string} string - String to be truncated
* @param {number} n - Number of characters
* @return {string} Truncated string
*/
truncate_(string, n) {
let i = n
if (string.length > i) {
while (string[i] !== " " && --i > 0);
return `${string.substring(0, i)}...`
}
return string
}
/**
* Update search results
*
@ -147,7 +152,8 @@ export default class Result {
const docs = this.docs_,
lang = this.lang_
/* Create index */
/* Create stack and index */
this.stack_ = []
this.index_ = lunr(function() {
/* Remove stemmer, as it cripples search experience */
@ -161,7 +167,7 @@ export default class Result {
if (lang.length === 1) {
this.use(lunr[lang[0]])
} else if (lang.length > 1) {
this.use(lunr.multiLanguage(...lang))
this.use(lunr.multiLanguage(...lang)) // TODO: remove
}
/* Index fields */
@ -172,6 +178,16 @@ export default class Result {
/* Index documents */
docs.forEach(doc => this.add(doc))
})
/* Register event handler for lazy rendering */
const container = this.el_.parentNode
if (!(container instanceof HTMLElement))
throw new ReferenceError
container.addEventListener("scroll", () => {
while (this.stack_.length && container.scrollTop +
container.offsetHeight >= container.scrollHeight - 16)
this.stack_.splice(0, 10).forEach(render => render())
})
}
/* eslint-enable no-invalid-this */
@ -208,7 +224,7 @@ export default class Result {
/* Append trailing wildcard to all terms for prefix querying */
.query(query => {
this.value_.split(" ")
this.value_.toLowerCase().split(" ")
.filter(Boolean)
.forEach(term => {
query.term(term, { wildcard: lunr.Query.wildcard.TRAILING })
@ -236,12 +252,13 @@ export default class Result {
const highlight = (_, separator, token) =>
`${separator}<em>${token}</em>`
/* Render results */
/* Reset stack and render results */
this.stack_ = []
result.forEach((items, ref) => {
const doc = this.docs_.get(ref)
/* Append search result */
this.list_.appendChild(
/* Render article */
const article = (
<li class="md-search-result__item">
<a href={doc.location} title={doc.title}
class="md-search-result__link">
@ -256,29 +273,44 @@ export default class Result {
</p> : {}}
</article>
</a>
{items.map(item => {
const section = this.docs_.get(item.ref)
return (
<a href={section.location} title={section.title}
class="md-search-result__link" data-md-rel="anchor">
<article class="md-search-result__article">
<h1 class="md-search-result__title">
{{ __html: section.title.replace(match, highlight) }}
</h1>
{section.text.length ?
<p class="md-search-result__teaser">
{{ __html: this.truncate_(
section.text.replace(match, highlight), 400)
}}
</p> : {}}
</article>
</a>
)
})}
</li>
)
/* Render sections for article */
const sections = items.map(item => {
return () => {
const section = this.docs_.get(item.ref)
article.appendChild(
<a href={section.location} title={section.title}
class="md-search-result__link" data-md-rel="anchor">
<article class="md-search-result__article">
<h1 class="md-search-result__title">
{{ __html: section.title.replace(match, highlight) }}
</h1>
{section.text.length ?
<p class="md-search-result__teaser">
{{ __html: truncate(
section.text.replace(match, highlight), 400)
}}
</p> : {}}
</article>
</a>
)
}
})
/* Push articles and section renderers onto stack */
this.stack_.push(() => this.list_.appendChild(article), ...sections)
})
/* Gradually add results as long as the height of the container grows */
const container = this.el_.parentNode
if (!(container instanceof HTMLElement))
throw new ReferenceError
while (this.stack_.length &&
container.offsetHeight >= container.scrollHeight - 16)
(this.stack_.shift())()
/* Bind click handlers for anchors */
const anchors = this.list_.querySelectorAll("[data-md-rel=anchor]")
Array.prototype.forEach.call(anchors, anchor => {

View File

@ -34,7 +34,7 @@
"search.result.none": "No matching documents",
"search.result.one": "1 matching document",
"search.result.other": "# matching documents",
"search.tokenizer": "",
"search.tokenizer": "[\s\-]+",
"source.link.title": "Go to repository",
"toc.title": "Table of contents"
}[key] }}{% endmacro %}