Merge pull request #425 from squidfunk/feat/search-separator

Added support for lazy rendering of search results
This commit is contained in:
Martin Donath 2017-08-02 14:20:42 +02:00 committed by GitHub
commit 14f85c52de
6 changed files with 88 additions and 49 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. 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. 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?" !!! 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 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.none": "No matching documents",
"search.result.one": "1 matching document", "search.result.one": "1 matching document",
"search.result.other": "# matching documents", "search.result.other": "# matching documents",
"search.tokenizer": "[\s\-]+",
"source.link.title": "Go to repository", "source.link.title": "Go to repository",
"toc.title": "Table of contents" "toc.title": "Table of contents"
}[key] }}{% endmacro %} }[key] }}{% endmacro %}
@ -398,6 +403,8 @@ section on [overriding partials][18] and the general guide on
#### Site search #### Site search
##### Language
Site search is implemented using [lunr.js][21], which includes stemmers for the Site search is implemented using [lunr.js][21], which includes stemmers for the
English language by default, while stemmers for other languages are included English language by default, while stemmers for other languages are included
with [lunr-languages][22], both of which are integrated with this theme. Support with [lunr-languages][22], both of which are integrated with this theme. Support

View File

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

View File

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

View File

@ -23,6 +23,30 @@
import escape from "escape-string-regexp" import escape from "escape-string-regexp"
import lunr from "expose-loader?lunr!lunr" 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 * Class
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
@ -42,6 +66,7 @@ export default class Result {
* @property {Array<string>} lang_ - Search languages * @property {Array<string>} lang_ - Search languages
* @property {Object} message_ - Search result messages * @property {Object} message_ - Search result messages
* @property {Object} index_ - Search index * @property {Object} index_ - Search index
* @property {Array<Function>} stack_ - Search result stack
* @property {string} value_ - Last input value * @property {string} value_ - Last input value
* *
* @param {(string|HTMLElement)} el - Selector or HTML element * @param {(string|HTMLElement)} el - Selector or HTML element
@ -81,26 +106,6 @@ export default class Result {
.map(lang => lang.trim()) .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 * Update search results
* *
@ -147,7 +152,8 @@ export default class Result {
const docs = this.docs_, const docs = this.docs_,
lang = this.lang_ lang = this.lang_
/* Create index */ /* Create stack and index */
this.stack_ = []
this.index_ = lunr(function() { this.index_ = lunr(function() {
/* Remove stemmer, as it cripples search experience */ /* Remove stemmer, as it cripples search experience */
@ -157,7 +163,7 @@ export default class Result {
lunr.stopWordFilter lunr.stopWordFilter
) )
/* Set up stemmers for search languages */ /* Set up alternate search languages */
if (lang.length === 1) { if (lang.length === 1) {
this.use(lunr[lang[0]]) this.use(lunr[lang[0]])
} else if (lang.length > 1) { } else if (lang.length > 1) {
@ -172,6 +178,16 @@ export default class Result {
/* Index documents */ /* Index documents */
docs.forEach(doc => this.add(doc)) 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 */ /* eslint-enable no-invalid-this */
@ -208,7 +224,7 @@ export default class Result {
/* Append trailing wildcard to all terms for prefix querying */ /* Append trailing wildcard to all terms for prefix querying */
.query(query => { .query(query => {
this.value_.split(" ") this.value_.toLowerCase().split(" ")
.filter(Boolean) .filter(Boolean)
.forEach(term => { .forEach(term => {
query.term(term, { wildcard: lunr.Query.wildcard.TRAILING }) query.term(term, { wildcard: lunr.Query.wildcard.TRAILING })
@ -236,12 +252,13 @@ export default class Result {
const highlight = (_, separator, token) => const highlight = (_, separator, token) =>
`${separator}<em>${token}</em>` `${separator}<em>${token}</em>`
/* Render results */ /* Reset stack and render results */
this.stack_ = []
result.forEach((items, ref) => { result.forEach((items, ref) => {
const doc = this.docs_.get(ref) const doc = this.docs_.get(ref)
/* Append search result */ /* Render article */
this.list_.appendChild( const article = (
<li class="md-search-result__item"> <li class="md-search-result__item">
<a href={doc.location} title={doc.title} <a href={doc.location} title={doc.title}
class="md-search-result__link"> class="md-search-result__link">
@ -256,29 +273,44 @@ export default class Result {
</p> : {}} </p> : {}}
</article> </article>
</a> </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> </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 */ /* Bind click handlers for anchors */
const anchors = this.list_.querySelectorAll("[data-md-rel=anchor]") const anchors = this.list_.querySelectorAll("[data-md-rel=anchor]")
Array.prototype.forEach.call(anchors, anchor => { Array.prototype.forEach.call(anchors, anchor => {

View File

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