mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-06-14 11:52:32 +03:00
Funding goal reached: merged back new search UI/UX from Insiders
This commit is contained in:
2
.github/ISSUE_TEMPLATE/translate.md
vendored
2
.github/ISSUE_TEMPLATE/translate.md
vendored
@@ -34,6 +34,8 @@ assignees: ''
|
|||||||
"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.result.more.one": "1 more on this page",
|
||||||
|
"search.result.more.other": "# more on this page",
|
||||||
"skip.link.title": "Skip to content",
|
"skip.link.title": "Skip to content",
|
||||||
"source.link.title": "Go to repository",
|
"source.link.title": "Go to repository",
|
||||||
"source.revision.date": "Last update",
|
"source.revision.date": "Last update",
|
||||||
|
|||||||
@@ -213,26 +213,20 @@ following transformations, which can be customized by [extending the theme][16]:
|
|||||||
*
|
*
|
||||||
* 3. Trim excess whitespace from left and right.
|
* 3. Trim excess whitespace from left and right.
|
||||||
*
|
*
|
||||||
* 4. Append a wildcard to the end of every word to make every word a prefix
|
|
||||||
* query in order to provide a good typeahead experience, by adding an
|
|
||||||
* asterisk (wildcard) in between terms, which can be denoted by whitespace,
|
|
||||||
* any non-control character, or a word boundary.
|
|
||||||
*
|
|
||||||
* @param query - Query value
|
* @param query - Query value
|
||||||
*
|
*
|
||||||
* @return Transformed query value
|
* @return Transformed query value
|
||||||
*/
|
*/
|
||||||
function defaultTransform(query: string): string {
|
export function defaultTransform(query: string): string {
|
||||||
return query
|
return query
|
||||||
.split(/"([^"]+)"/g) /* => 1 */
|
.split(/"([^"]+)"/g) /* => 1 */
|
||||||
.map((terms, i) => i & 1
|
.map((terms, index) => index & 1
|
||||||
? terms.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g, " +")
|
? terms.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g, " +")
|
||||||
: terms
|
: terms
|
||||||
)
|
)
|
||||||
.join("")
|
.join("")
|
||||||
.replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g, "") /* => 2 */
|
.replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g, "") /* => 2 */
|
||||||
.trim() /* => 3 */
|
.trim() /* => 3 */
|
||||||
.replace(/\s+|(?![^\x00-\x7F]|^)$|\b$/g, "* ") /* => 4 */
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
2
material/assets/javascripts/bundle.10eaee41.min.js
vendored
Normal file
2
material/assets/javascripts/bundle.10eaee41.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
material/assets/javascripts/bundle.10eaee41.min.js.map
Normal file
1
material/assets/javascripts/bundle.10eaee41.min.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
material/assets/javascripts/vendor.141042ad.min.js.map
Normal file
1
material/assets/javascripts/vendor.141042ad.min.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"assets/javascripts/bundle.js": "assets/javascripts/bundle.66a8d459.min.js",
|
"assets/javascripts/bundle.js": "assets/javascripts/bundle.10eaee41.min.js",
|
||||||
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.66a8d459.min.js.map",
|
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.10eaee41.min.js.map",
|
||||||
"assets/javascripts/vendor.js": "assets/javascripts/vendor.581c8fc6.min.js",
|
"assets/javascripts/vendor.js": "assets/javascripts/vendor.141042ad.min.js",
|
||||||
"assets/javascripts/vendor.js.map": "assets/javascripts/vendor.581c8fc6.min.js.map",
|
"assets/javascripts/vendor.js.map": "assets/javascripts/vendor.141042ad.min.js.map",
|
||||||
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.5eca75d3.min.js",
|
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.cbc634e2.min.js",
|
||||||
"assets/javascripts/worker/search.js.map": "assets/javascripts/worker/search.5eca75d3.min.js.map",
|
"assets/javascripts/worker/search.js.map": "assets/javascripts/worker/search.cbc634e2.min.js.map",
|
||||||
"assets/stylesheets/main.css": "assets/stylesheets/main.45ead06e.min.css",
|
"assets/stylesheets/main.css": "assets/stylesheets/main.b6d72156.min.css",
|
||||||
"assets/stylesheets/main.css.map": "assets/stylesheets/main.45ead06e.min.css.map",
|
"assets/stylesheets/main.css.map": "assets/stylesheets/main.b6d72156.min.css.map",
|
||||||
"assets/stylesheets/overrides.css": "assets/stylesheets/overrides.9514a156.min.css",
|
"assets/stylesheets/overrides.css": "assets/stylesheets/overrides.9514a156.min.css",
|
||||||
"assets/stylesheets/overrides.css.map": "assets/stylesheets/overrides.9514a156.min.css.map",
|
"assets/stylesheets/overrides.css.map": "assets/stylesheets/overrides.9514a156.min.css.map",
|
||||||
"assets/stylesheets/palette.css": "assets/stylesheets/palette.6b892c47.min.css",
|
"assets/stylesheets/palette.css": "assets/stylesheets/palette.6b892c47.min.css",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3
material/assets/stylesheets/main.b6d72156.min.css
vendored
Normal file
3
material/assets/stylesheets/main.b6d72156.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
material/assets/stylesheets/main.b6d72156.min.css.map
Normal file
1
material/assets/stylesheets/main.b6d72156.min.css.map
Normal file
File diff suppressed because one or more lines are too long
@@ -2,11 +2,6 @@
|
|||||||
This file was automatically generated - do not edit
|
This file was automatically generated - do not edit
|
||||||
-#}
|
-#}
|
||||||
{% import "partials/language.html" as lang with context %}
|
{% import "partials/language.html" as lang with context %}
|
||||||
{% set palette = config.theme.palette %}
|
|
||||||
{% if not palette is mapping %}
|
|
||||||
{% set palette = palette | first %}
|
|
||||||
{% endif %}
|
|
||||||
{% set font = config.theme.font %}
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="{{ lang.t('language') }}" class="no-js">
|
<html lang="{{ lang.t('language') }}" class="no-js">
|
||||||
<head>
|
<head>
|
||||||
@@ -39,21 +34,23 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block styles %}
|
{% block styles %}
|
||||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.45ead06e.min.css' | url }}">
|
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.b6d72156.min.css' | url }}">
|
||||||
{% if palette.scheme or palette.primary or palette.accent %}
|
{% if config.theme.palette %}
|
||||||
|
{% set palette = config.theme.palette %}
|
||||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.6b892c47.min.css' | url }}">
|
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.6b892c47.min.css' | url }}">
|
||||||
{% endif %}
|
{% if palette.primary %}
|
||||||
{% if palette.primary %}
|
{% import "partials/palette.html" as map %}
|
||||||
{% import "partials/palette.html" as map %}
|
{% set primary = map.primary(
|
||||||
{% set primary = map.primary(
|
palette.primary | replace(" ", "-") | lower
|
||||||
palette.primary | replace(" ", "-") | lower
|
) %}
|
||||||
) %}
|
<meta name="theme-color" content="{{ primary }}">
|
||||||
<meta name="theme-color" content="{{ primary }}">
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block libs %}{% endblock %}
|
{% block libs %}{% endblock %}
|
||||||
{% block fonts %}
|
{% block fonts %}
|
||||||
{% if font != false %}
|
{% if config.theme.font != false %}
|
||||||
|
{% set font = config.theme.font %}
|
||||||
<link href="https://fonts.gstatic.com" rel="preconnect" crossorigin>
|
<link href="https://fonts.gstatic.com" rel="preconnect" crossorigin>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family={{
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family={{
|
||||||
font.text | replace(' ', '+') + ':300,400,400i,700%7C' +
|
font.text | replace(' ', '+') + ':300,400,400i,700%7C' +
|
||||||
@@ -76,17 +73,21 @@
|
|||||||
{% block extrahead %}{% endblock %}
|
{% block extrahead %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
{% set direction = config.theme.direction or lang.t('direction') %}
|
{% set direction = config.theme.direction or lang.t('direction') %}
|
||||||
{% if palette.scheme or palette.primary or palette.accent %}
|
{% if config.theme.palette %}
|
||||||
{% set scheme = palette.scheme | lower %}
|
{% set palette = config.theme.palette %}
|
||||||
|
{% if not palette is mapping %}
|
||||||
|
{% set palette = palette | first %}
|
||||||
|
{% endif %}
|
||||||
|
{% set scheme = palette.scheme | replace(" ", "-") | lower %}
|
||||||
{% set primary = palette.primary | replace(" ", "-") | lower %}
|
{% set primary = palette.primary | replace(" ", "-") | lower %}
|
||||||
{% set accent = palette.accent | replace(" ", "-") | lower %}
|
{% set accent = palette.accent | replace(" ", "-") | lower %}
|
||||||
<body dir="{{ direction }}" data-md-color-scheme="{{ scheme }}" data-md-color-primary="{{ primary }}" data-md-color-accent="{{ accent }}">
|
<body dir="{{ direction }}" data-md-color-scheme="{{ scheme }}" data-md-color-primary="{{ primary }}" data-md-color-accent="{{ accent }}">
|
||||||
|
{% if "preference" == scheme %}
|
||||||
|
<script>matchMedia("(prefers-color-scheme: dark)").matches&&document.body.setAttribute("data-md-color-scheme","slate")</script>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<body dir="{{ direction }}">
|
<body dir="{{ direction }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if "preference" == palette.scheme %}
|
|
||||||
<script>matchMedia("(prefers-color-scheme: dark)").matches&&document.body.setAttribute("data-md-color-scheme","slate")</script>
|
|
||||||
{% endif %}
|
|
||||||
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
|
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
|
||||||
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
|
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
|
||||||
<label class="md-overlay" for="__drawer"></label>
|
<label class="md-overlay" for="__drawer"></label>
|
||||||
@@ -171,8 +172,8 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{{ 'assets/javascripts/vendor.581c8fc6.min.js' | url }}"></script>
|
<script src="{{ 'assets/javascripts/vendor.141042ad.min.js' | url }}"></script>
|
||||||
<script src="{{ 'assets/javascripts/bundle.66a8d459.min.js' | url }}"></script>
|
<script src="{{ 'assets/javascripts/bundle.10eaee41.min.js' | url }}"></script>
|
||||||
{%- set translations = {} -%}
|
{%- set translations = {} -%}
|
||||||
{%- for key in [
|
{%- for key in [
|
||||||
"clipboard.copy",
|
"clipboard.copy",
|
||||||
@@ -183,7 +184,10 @@
|
|||||||
"search.result.placeholder",
|
"search.result.placeholder",
|
||||||
"search.result.none",
|
"search.result.none",
|
||||||
"search.result.one",
|
"search.result.one",
|
||||||
"search.result.other"
|
"search.result.other",
|
||||||
|
"search.result.more.one",
|
||||||
|
"search.result.more.other",
|
||||||
|
"search.result.term.missing"
|
||||||
] -%}
|
] -%}
|
||||||
{%- set _ = translations.update({ key: lang.t(key) }) -%}
|
{%- set _ = translations.update({ key: lang.t(key) }) -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
@@ -196,7 +200,7 @@
|
|||||||
base: "{{ base_url }}",
|
base: "{{ base_url }}",
|
||||||
features: {{ config.theme.features or [] | tojson }},
|
features: {{ config.theme.features or [] | tojson }},
|
||||||
search: Object.assign({
|
search: Object.assign({
|
||||||
worker: "{{ 'assets/javascripts/worker/search.5eca75d3.min.js' | url }}"
|
worker: "{{ 'assets/javascripts/worker/search.cbc634e2.min.js' | url }}"
|
||||||
}, typeof search !== "undefined" && search)
|
}, typeof search !== "undefined" && search)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
"search.result.none": "Keine Suchergebnisse",
|
"search.result.none": "Keine Suchergebnisse",
|
||||||
"search.result.one": "1 Suchergebnis",
|
"search.result.one": "1 Suchergebnis",
|
||||||
"search.result.other": "# Suchergebnisse",
|
"search.result.other": "# Suchergebnisse",
|
||||||
|
"search.result.more.one": "1 weiteres Suchergebnis auf dieser Seite",
|
||||||
|
"search.result.more.other": "# weitere Suchergebnisse auf dieser Seite",
|
||||||
|
"search.result.term.missing": "Es fehlt",
|
||||||
"skip.link.title": "Zum Inhalt",
|
"skip.link.title": "Zum Inhalt",
|
||||||
"source.link.title": "Quellcode",
|
"source.link.title": "Quellcode",
|
||||||
"source.revision.date": "Letztes Update",
|
"source.revision.date": "Letztes Update",
|
||||||
|
|||||||
@@ -24,6 +24,9 @@
|
|||||||
"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.result.more.one": "1 more on this page",
|
||||||
|
"search.result.more.other": "# more on this page",
|
||||||
|
"search.result.term.missing": "Missing",
|
||||||
"skip.link.title": "Skip to content",
|
"skip.link.title": "Skip to content",
|
||||||
"source.link.title": "Go to repository",
|
"source.link.title": "Go to repository",
|
||||||
"source.revision.date": "Last update",
|
"source.revision.date": "Last update",
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
"search.result.none": "Inga sökresultat",
|
"search.result.none": "Inga sökresultat",
|
||||||
"search.result.one": "1 sökresultat",
|
"search.result.one": "1 sökresultat",
|
||||||
"search.result.other": "# sökresultat",
|
"search.result.other": "# sökresultat",
|
||||||
|
"search.result.more.one": "1 till på denna sidan",
|
||||||
|
"search.result.more.other": "# till på denna sidan",
|
||||||
|
"search.result.term.missing": "Saknas",
|
||||||
"skip.link.title": "Gå till innehållet",
|
"skip.link.title": "Gå till innehållet",
|
||||||
"source.link.title": "Gå till datakatalog",
|
"source.link.title": "Gå till datakatalog",
|
||||||
"source.revision.date": "Senaste uppdateringen",
|
"source.revision.date": "Senaste uppdateringen",
|
||||||
|
|||||||
@@ -96,29 +96,34 @@ export function applySearchResult(
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
/* Apply search result list */
|
/* Apply search result list */
|
||||||
switchMap(result => fetch$
|
switchMap(result => {
|
||||||
.pipe(
|
const thresholds = [...result.map(([best]) => best.score), 0]
|
||||||
|
return fetch$
|
||||||
|
.pipe(
|
||||||
|
|
||||||
/* Defer repaint to next animation frame */
|
/* Defer repaint to next animation frame */
|
||||||
observeOn(animationFrameScheduler),
|
observeOn(animationFrameScheduler),
|
||||||
scan(index => {
|
scan(index => {
|
||||||
const container = el.parentElement!
|
const container = el.parentElement!
|
||||||
while (index < result.length) {
|
while (index < result.length) {
|
||||||
addToSearchResultList(list, renderSearchResult(result[index++]))
|
addToSearchResultList(list, renderSearchResult(
|
||||||
if (container.scrollHeight - container.offsetHeight > 16)
|
result[index++], thresholds[index]
|
||||||
break
|
))
|
||||||
}
|
if (container.scrollHeight - container.offsetHeight > 16)
|
||||||
return index
|
break
|
||||||
}, 0),
|
}
|
||||||
|
return index
|
||||||
|
}, 0),
|
||||||
|
|
||||||
/* Re-map to search result */
|
/* Re-map to search result */
|
||||||
mapTo(result),
|
mapTo(result),
|
||||||
|
|
||||||
/* Reset on complete or error */
|
/* Reset on complete or error */
|
||||||
finalize(() => {
|
finalize(() => {
|
||||||
resetSearchResultList(list)
|
resetSearchResultList(list)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function setSearchResultMeta(
|
|||||||
|
|
||||||
/* Multiple result */
|
/* Multiple result */
|
||||||
default:
|
default:
|
||||||
el.textContent = translate("search.result.other", value.toString())
|
el.textContent = translate("search.result.other", value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
import "focus-visible"
|
import "focus-visible"
|
||||||
|
|
||||||
import { sortBy, prop, values } from "ramda"
|
import { sortBy, prop, values, identity } from "ramda"
|
||||||
import {
|
import {
|
||||||
merge,
|
merge,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
@@ -85,7 +85,7 @@ import {
|
|||||||
setupKeyboard,
|
setupKeyboard,
|
||||||
setupInstantLoading,
|
setupInstantLoading,
|
||||||
setupSearchWorker,
|
setupSearchWorker,
|
||||||
SearchIndex
|
SearchIndex, SearchIndexPipeline
|
||||||
} from "integrations"
|
} from "integrations"
|
||||||
import {
|
import {
|
||||||
patchCodeBlocks,
|
patchCodeBlocks,
|
||||||
@@ -95,7 +95,7 @@ import {
|
|||||||
patchSource,
|
patchSource,
|
||||||
patchScripts
|
patchScripts
|
||||||
} from "patches"
|
} from "patches"
|
||||||
import { isConfig } from "utilities"
|
import { isConfig, translate } from "utilities"
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- */
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
@@ -135,6 +135,38 @@ export function resetScrollLock(
|
|||||||
window.scrollTo(0, value)
|
window.scrollTo(0, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------------------
|
||||||
|
* Helper functions
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up search index
|
||||||
|
*
|
||||||
|
* @param data - Search index
|
||||||
|
*
|
||||||
|
* @return Search index
|
||||||
|
*/
|
||||||
|
function setupSearchIndex( // Hack: move this outside here, temporarily...
|
||||||
|
{ config, docs, index }: SearchIndex
|
||||||
|
): SearchIndex {
|
||||||
|
|
||||||
|
/* Override default language with value from translation */
|
||||||
|
if (config.lang.length === 1 && config.lang[0] === "en")
|
||||||
|
config.lang = [translate("search.config.lang")]
|
||||||
|
|
||||||
|
/* Override default separator with value from translation */
|
||||||
|
if (config.separator === "[\\s\\-]+")
|
||||||
|
config.separator = translate("search.config.separator")
|
||||||
|
|
||||||
|
/* Set pipeline from translation */
|
||||||
|
const pipeline = translate("search.config.pipeline")
|
||||||
|
.split(/\s*,\s*/)
|
||||||
|
.filter(identity) as SearchIndexPipeline
|
||||||
|
|
||||||
|
/* Return search index after defaulting */
|
||||||
|
return { config, docs, index, pipeline }
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Functions
|
* Functions
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
@@ -248,20 +280,26 @@ export function initialize(config: unknown) {
|
|||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
/* Fetch index if it wasn't passed explicitly */
|
/* Fetch index if it wasn't passed explicitly */
|
||||||
const index$ = typeof index !== "undefined"
|
const index$ = (
|
||||||
? from(index)
|
typeof index !== "undefined"
|
||||||
: base$
|
? from(index)
|
||||||
.pipe(
|
: base$
|
||||||
switchMap(base => ajax({
|
.pipe(
|
||||||
url: `${base}/search/search_index.json`,
|
switchMap(base => ajax({
|
||||||
responseType: "json",
|
url: `${base}/search/search_index.json`,
|
||||||
withCredentials: true
|
responseType: "json",
|
||||||
})
|
withCredentials: true
|
||||||
.pipe<SearchIndex>(
|
})
|
||||||
pluck("response")
|
.pipe<SearchIndex>(
|
||||||
|
pluck("response")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.pipe(
|
||||||
|
map(setupSearchIndex),
|
||||||
|
shareReplay(1)
|
||||||
|
)
|
||||||
|
|
||||||
return of(setupSearchWorker(config.search.worker, {
|
return of(setupSearchWorker(config.search.worker, {
|
||||||
base$, index$
|
base$, index$
|
||||||
|
|||||||
@@ -135,7 +135,10 @@ export function setupKeyboard(): Observable<Keyboard> {
|
|||||||
if (typeof active === "undefined") {
|
if (typeof active === "undefined") {
|
||||||
setElementFocus(query)
|
setElementFocus(query)
|
||||||
} else {
|
} else {
|
||||||
const els = [query, ...getElements("[href]", result)]
|
const els = [query, ...getElements(
|
||||||
|
":not(details) > [href], summary, details[open] [href]",
|
||||||
|
result
|
||||||
|
)]
|
||||||
const i = Math.max(0, (
|
const i = Math.max(0, (
|
||||||
Math.max(0, els.indexOf(active)) + els.length + (
|
Math.max(0, els.indexOf(active)) + els.length + (
|
||||||
key.type === "ArrowUp" ? -1 : +1
|
key.type === "ArrowUp" ? -1 : +1
|
||||||
|
|||||||
@@ -21,15 +21,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ArticleDocument,
|
SearchDocument,
|
||||||
SearchDocumentMap,
|
SearchDocumentMap,
|
||||||
SectionDocument,
|
|
||||||
setupSearchDocumentMap
|
setupSearchDocumentMap
|
||||||
} from "../document"
|
} from "../document"
|
||||||
import {
|
import {
|
||||||
SearchHighlightFactoryFn,
|
SearchHighlightFactoryFn,
|
||||||
setupSearchHighlighter
|
setupSearchHighlighter
|
||||||
} from "../highlighter"
|
} from "../highlighter"
|
||||||
|
import {
|
||||||
|
SearchQueryTerms,
|
||||||
|
getSearchQueryTerms,
|
||||||
|
parseSearchQuery
|
||||||
|
} from "../query"
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Types
|
* Types
|
||||||
@@ -84,13 +88,22 @@ export interface SearchIndex {
|
|||||||
|
|
||||||
/* ------------------------------------------------------------------------- */
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search metadata
|
||||||
|
*/
|
||||||
|
export interface SearchMetadata {
|
||||||
|
score: number /* Score (relevance) */
|
||||||
|
terms: SearchQueryTerms /* Search query terms */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search result
|
* Search result
|
||||||
*/
|
*/
|
||||||
export interface SearchResult {
|
export type SearchResult = Array<
|
||||||
article: ArticleDocument /* Article document */
|
SearchDocument & SearchMetadata
|
||||||
sections: SectionDocument[] /* Section documents */
|
> // tslint:disable-line
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Functions
|
* Functions
|
||||||
@@ -116,7 +129,7 @@ function difference(a: string[], b: string[]): string[] {
|
|||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search
|
* Search index
|
||||||
*
|
*
|
||||||
* Note that `lunr` is injected via Webpack, as it will otherwise also be
|
* Note that `lunr` is injected via Webpack, as it will otherwise also be
|
||||||
* bundled in the application bundle.
|
* bundled in the application bundle.
|
||||||
@@ -139,7 +152,7 @@ export class Search {
|
|||||||
protected highlight: SearchHighlightFactoryFn
|
protected highlight: SearchHighlightFactoryFn
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `lunr` search index
|
* The underlying `lunr` search index
|
||||||
*/
|
*/
|
||||||
protected index: lunr.Index
|
protected index: lunr.Index
|
||||||
|
|
||||||
@@ -159,7 +172,7 @@ export class Search {
|
|||||||
if (typeof index === "undefined") {
|
if (typeof index === "undefined") {
|
||||||
this.index = lunr(function() {
|
this.index = lunr(function() {
|
||||||
|
|
||||||
/* Set up alternate search languages */
|
/* Set up multi-language support */
|
||||||
if (config.lang.length === 1 && config.lang[0] !== "en") {
|
if (config.lang.length === 1 && config.lang[0] !== "en") {
|
||||||
this.use((lunr as any)[config.lang[0]])
|
this.use((lunr as any)[config.lang[0]])
|
||||||
} else if (config.lang.length > 1) {
|
} else if (config.lang.length > 1) {
|
||||||
@@ -171,7 +184,7 @@ export class Search {
|
|||||||
"trimmer", "stopWordFilter", "stemmer"
|
"trimmer", "stopWordFilter", "stemmer"
|
||||||
], pipeline!)
|
], pipeline!)
|
||||||
|
|
||||||
/* Remove functions from the pipeline for every language */
|
/* Remove functions from the pipeline for registered languages */
|
||||||
for (const lang of config.lang.map(language => (
|
for (const lang of config.lang.map(language => (
|
||||||
language === "en" ? lunr : (lunr as any)[language]
|
language === "en" ? lunr : (lunr as any)[language]
|
||||||
))) {
|
))) {
|
||||||
@@ -191,7 +204,7 @@ export class Search {
|
|||||||
this.add(doc)
|
this.add(doc)
|
||||||
})
|
})
|
||||||
|
|
||||||
/* Prebuilt or serialized index */
|
/* Handle prebuilt or serialized index */
|
||||||
} else {
|
} else {
|
||||||
this.index = lunr.Index.load(
|
this.index = lunr.Index.load(
|
||||||
typeof index === "string"
|
typeof index === "string"
|
||||||
@@ -213,45 +226,71 @@ export class Search {
|
|||||||
* page. For this reason, section results are grouped within their respective
|
* page. For this reason, section results are grouped within their respective
|
||||||
* articles which are the top-level results that are returned.
|
* articles which are the top-level results that are returned.
|
||||||
*
|
*
|
||||||
* @param value - Query value
|
* @param query - Query value
|
||||||
*
|
*
|
||||||
* @return Search results
|
* @return Search results
|
||||||
*/
|
*/
|
||||||
public query(value: string): SearchResult[] {
|
public search(query: string): SearchResult[] {
|
||||||
if (value) {
|
if (query) {
|
||||||
try {
|
try {
|
||||||
|
const highlight = this.highlight(query)
|
||||||
|
|
||||||
/* Group sections by containing article */
|
/* Parse query to extract clauses for analysis */
|
||||||
const groups = this.index.search(value)
|
const clauses = parseSearchQuery(query)
|
||||||
.reduce((results, result) => {
|
.filter(clause => (
|
||||||
const document = this.documents.get(result.ref)
|
clause.presence !== lunr.Query.presence.PROHIBITED
|
||||||
|
))
|
||||||
|
|
||||||
|
/* Perform search and post-process results */
|
||||||
|
const groups = this.index.search(`${query}*`)
|
||||||
|
|
||||||
|
/* Apply post-query boosts based on title and search query terms */
|
||||||
|
.reduce<SearchResult>((results, { ref, score, matchData }) => {
|
||||||
|
const document = this.documents.get(ref)
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
if ("parent" in document) {
|
const { location, title, text, parent } = document
|
||||||
const ref = document.parent.location
|
|
||||||
results.set(ref, [...results.get(ref) || [], result])
|
/* Compute and analyze search query terms */
|
||||||
} else {
|
const terms = getSearchQueryTerms(
|
||||||
const ref = document.location
|
clauses,
|
||||||
results.set(ref, results.get(ref) || [])
|
Object.keys(matchData.metadata)
|
||||||
}
|
)
|
||||||
|
|
||||||
|
/* Highlight title and text and apply post-query boosts */
|
||||||
|
const boost = +!parent + +Object.values(terms).every(t => t)
|
||||||
|
results.push({
|
||||||
|
location,
|
||||||
|
title: highlight(title),
|
||||||
|
text: highlight(text),
|
||||||
|
score: score * (1 + boost),
|
||||||
|
terms
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return results
|
return results
|
||||||
}, new Map<string, lunr.Index.Result[]>())
|
}, [])
|
||||||
|
|
||||||
/* Create highlighter for query */
|
/* Sort search results again after applying boosts */
|
||||||
const fn = this.highlight(value)
|
.sort((a, b) => b.score - a.score)
|
||||||
|
|
||||||
/* Map groups to search documents */
|
/* Group search results by page */
|
||||||
return [...groups].map(([ref, sections]) => ({
|
.reduce((results, result) => {
|
||||||
article: fn(this.documents.get(ref) as ArticleDocument),
|
const document = this.documents.get(result.location)
|
||||||
sections: sections.map(section => {
|
if (typeof document !== "undefined") {
|
||||||
return fn(this.documents.get(section.ref) as SectionDocument)
|
const ref = "parent" in document
|
||||||
})
|
? document.parent!.location
|
||||||
}))
|
: document.location
|
||||||
|
results.set(ref, [...results.get(ref) || [], result])
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}, new Map<string, SearchResult>())
|
||||||
|
|
||||||
|
/* Expand grouped search results */
|
||||||
|
return [...groups.values()]
|
||||||
|
|
||||||
/* Log errors to console (for now) */
|
/* Log errors to console (for now) */
|
||||||
} catch (err) {
|
} catch {
|
||||||
// tslint:disable-next-line no-console
|
// tslint:disable-next-line no-console
|
||||||
console.warn(`Invalid query: ${value} – see https://bit.ly/2s3ChXG`)
|
console.warn(`Invalid query: ${query} – see https://bit.ly/2s3ChXG`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,28 +29,14 @@ import { SearchIndexDocument } from "../_"
|
|||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A top-level article
|
* Search document
|
||||||
*/
|
*/
|
||||||
export interface ArticleDocument extends SearchIndexDocument {
|
export interface SearchDocument extends SearchIndexDocument {
|
||||||
linked: boolean /* Whether the section was linked */
|
parent?: SearchIndexDocument /* Parent article */
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A section of an article
|
|
||||||
*/
|
|
||||||
export interface SectionDocument extends SearchIndexDocument {
|
|
||||||
parent: ArticleDocument /* Parent article */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- */
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
|
||||||
* Search document
|
|
||||||
*/
|
|
||||||
export type SearchDocument =
|
|
||||||
| ArticleDocument
|
|
||||||
| SectionDocument
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search document mapping
|
* Search document mapping
|
||||||
*/
|
*/
|
||||||
@@ -71,6 +57,7 @@ export function setupSearchDocumentMap(
|
|||||||
docs: SearchIndexDocument[]
|
docs: SearchIndexDocument[]
|
||||||
): SearchDocumentMap {
|
): SearchDocumentMap {
|
||||||
const documents = new Map<string, SearchDocument>()
|
const documents = new Map<string, SearchDocument>()
|
||||||
|
const parents = new Set<SearchDocument>()
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
const [path, hash] = doc.location.split("#")
|
const [path, hash] = doc.location.split("#")
|
||||||
|
|
||||||
@@ -85,13 +72,15 @@ export function setupSearchDocumentMap(
|
|||||||
|
|
||||||
/* Handle section */
|
/* Handle section */
|
||||||
if (hash) {
|
if (hash) {
|
||||||
const parent = documents.get(path) as ArticleDocument
|
const parent = documents.get(path)!
|
||||||
|
|
||||||
/* Ignore first section, override article */
|
/* Ignore first section, override article */
|
||||||
if (!parent.linked) {
|
if (!parents.has(parent)) {
|
||||||
parent.title = doc.title
|
parent.title = doc.title
|
||||||
parent.text = text
|
parent.text = text
|
||||||
parent.linked = true
|
|
||||||
|
/* Remember that we processed the article */
|
||||||
|
parents.add(parent)
|
||||||
|
|
||||||
/* Add subsequent section */
|
/* Add subsequent section */
|
||||||
} else {
|
} else {
|
||||||
@@ -108,8 +97,7 @@ export function setupSearchDocumentMap(
|
|||||||
documents.set(location, {
|
documents.set(location, {
|
||||||
location,
|
location,
|
||||||
title,
|
title,
|
||||||
text,
|
text
|
||||||
linked: false
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { SearchIndexConfig } from "../_"
|
import { SearchIndexConfig } from "../_"
|
||||||
import { SearchDocument } from "../document"
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Types
|
* Types
|
||||||
@@ -30,24 +29,20 @@ import { SearchDocument } from "../document"
|
|||||||
/**
|
/**
|
||||||
* Search highlight function
|
* Search highlight function
|
||||||
*
|
*
|
||||||
* @template T - Search document type
|
* @param value - Value
|
||||||
*
|
*
|
||||||
* @param document - Search document
|
* @return Highlighted value
|
||||||
*
|
|
||||||
* @return Highlighted document
|
|
||||||
*/
|
*/
|
||||||
export type SearchHighlightFn = <
|
export type SearchHighlightFn = (value: string) => string
|
||||||
T extends SearchDocument
|
|
||||||
>(document: Readonly<T>) => T
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search highlight factory function
|
* Search highlight factory function
|
||||||
*
|
*
|
||||||
* @param value - Query value
|
* @param query - Query value
|
||||||
*
|
*
|
||||||
* @return Search highlight function
|
* @return Search highlight function
|
||||||
*/
|
*/
|
||||||
export type SearchHighlightFactoryFn = (value: string) => SearchHighlightFn
|
export type SearchHighlightFactoryFn = (query: string) => SearchHighlightFn
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Functions
|
* Functions
|
||||||
@@ -65,27 +60,25 @@ export function setupSearchHighlighter(
|
|||||||
): SearchHighlightFactoryFn {
|
): SearchHighlightFactoryFn {
|
||||||
const separator = new RegExp(config.separator, "img")
|
const separator = new RegExp(config.separator, "img")
|
||||||
const highlight = (_: unknown, data: string, term: string) => {
|
const highlight = (_: unknown, data: string, term: string) => {
|
||||||
return `${data}<em>${term}</em>`
|
return `${data}<mark data-md-highlight>${term}</mark>`
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Return factory function */
|
/* Return factory function */
|
||||||
return (value: string) => {
|
return (query: string) => {
|
||||||
value = value
|
query = query
|
||||||
.replace(/[\s*+\-:~^]+/g, " ")
|
.replace(/[\s*+\-:~^]+/g, " ")
|
||||||
.trim()
|
.trim()
|
||||||
|
|
||||||
/* Create search term match expression */
|
/* Create search term match expression */
|
||||||
const match = new RegExp(`(^|${config.separator})(${
|
const match = new RegExp(`(^|${config.separator})(${
|
||||||
value
|
query
|
||||||
.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&")
|
.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&")
|
||||||
.replace(separator, "|")
|
.replace(separator, "|")
|
||||||
})`, "img")
|
})`, "img")
|
||||||
|
|
||||||
/* Highlight document */
|
/* Highlight string value */
|
||||||
return document => ({
|
return value => value
|
||||||
...document,
|
.replace(match, highlight)
|
||||||
title: document.title.replace(match, highlight),
|
.replace(/<\/mark>(\s+)<mark[^>]*>/img, "\$1")
|
||||||
text: document.text.replace(match, highlight)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,5 +23,5 @@
|
|||||||
export * from "./_"
|
export * from "./_"
|
||||||
export * from "./document"
|
export * from "./document"
|
||||||
export * from "./highlighter"
|
export * from "./highlighter"
|
||||||
export * from "./transform"
|
export * from "./query"
|
||||||
export * from "./worker"
|
export * from "./worker"
|
||||||
|
|||||||
92
src/assets/javascripts/integrations/search/query/_/index.ts
Normal file
92
src/assets/javascripts/integrations/search/query/_/index.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to
|
||||||
|
* deal in the Software without restriction, including without limitation the
|
||||||
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
* sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
* IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------------------
|
||||||
|
* Types
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search query clause
|
||||||
|
*/
|
||||||
|
export interface SearchQueryClause {
|
||||||
|
presence: lunr.Query.presence /* Clause presence */
|
||||||
|
term: string /* Clause term */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search query terms
|
||||||
|
*/
|
||||||
|
export type SearchQueryTerms = Record<string, boolean>
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------------------
|
||||||
|
* Functions
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a search query for analysis
|
||||||
|
*
|
||||||
|
* @param value - Query value
|
||||||
|
*
|
||||||
|
* @return Search query clauses
|
||||||
|
*/
|
||||||
|
export function parseSearchQuery(
|
||||||
|
value: string
|
||||||
|
): SearchQueryClause[] {
|
||||||
|
const query = new (lunr as any).Query(["title", "text"])
|
||||||
|
const parser = new (lunr as any).QueryParser(value, query)
|
||||||
|
|
||||||
|
/* Parse and return query clauses */
|
||||||
|
parser.parse()
|
||||||
|
return query.clauses
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze the search query clauses in regard to the search terms found
|
||||||
|
*
|
||||||
|
* @param query - Search query clauses
|
||||||
|
* @param terms - Search terms
|
||||||
|
*
|
||||||
|
* @return Search query terms
|
||||||
|
*/
|
||||||
|
export function getSearchQueryTerms(
|
||||||
|
query: SearchQueryClause[], terms: string[]
|
||||||
|
): SearchQueryTerms {
|
||||||
|
const clauses = new Set<SearchQueryClause>(query)
|
||||||
|
|
||||||
|
/* Match query clauses against terms */
|
||||||
|
const result: SearchQueryTerms = {}
|
||||||
|
for (let t = 0; t < terms.length; t++)
|
||||||
|
for (const clause of clauses)
|
||||||
|
if (terms[t].startsWith(clause.term)) {
|
||||||
|
result[clause.term] = true
|
||||||
|
clauses.delete(clause)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Annotate unmatched query clauses */
|
||||||
|
for (const clause of clauses)
|
||||||
|
result[clause.term] = false
|
||||||
|
|
||||||
|
/* Return query terms */
|
||||||
|
return result
|
||||||
|
}
|
||||||
24
src/assets/javascripts/integrations/search/query/index.ts
Normal file
24
src/assets/javascripts/integrations/search/query/index.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to
|
||||||
|
* deal in the Software without restriction, including without limitation the
|
||||||
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
* sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
* IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./_"
|
||||||
|
export * from "./transform"
|
||||||
@@ -54,24 +54,18 @@ export type SearchTransformFn = (value: string) => string
|
|||||||
*
|
*
|
||||||
* 3. Trim excess whitespace from left and right.
|
* 3. Trim excess whitespace from left and right.
|
||||||
*
|
*
|
||||||
* 4. Append a wildcard to the end of every word to make every word a prefix
|
* @param query - Query value
|
||||||
* query in order to provide a good typeahead experience, by adding an
|
|
||||||
* asterisk (wildcard) in between terms, which can be denoted by whitespace,
|
|
||||||
* any non-control character, or a word boundary.
|
|
||||||
*
|
|
||||||
* @param value - Query value
|
|
||||||
*
|
*
|
||||||
* @return Transformed query value
|
* @return Transformed query value
|
||||||
*/
|
*/
|
||||||
export function defaultTransform(value: string): string {
|
export function defaultTransform(query: string): string {
|
||||||
return value
|
return query
|
||||||
.split(/"([^"]+)"/g) /* => 1 */
|
.split(/"([^"]+)"/g) /* => 1 */
|
||||||
.map((terms, i) => i & 1
|
.map((terms, index) => index & 1
|
||||||
? terms.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g, " +")
|
? terms.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g, " +")
|
||||||
: terms
|
: terms
|
||||||
)
|
)
|
||||||
.join("")
|
.join("")
|
||||||
.replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g, "") /* => 2 */
|
.replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g, "") /* => 2 */
|
||||||
.trim() /* => 3 */
|
.trim() /* => 3 */
|
||||||
.replace(/\s+|(?![^\x00-\x7F]|^)$|\b$/g, "* ") /* => 4 */
|
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
* IN THE SOFTWARE.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { identity } from "ramda"
|
|
||||||
import { Observable, Subject, asyncScheduler } from "rxjs"
|
import { Observable, Subject, asyncScheduler } from "rxjs"
|
||||||
import {
|
import {
|
||||||
map,
|
map,
|
||||||
@@ -30,9 +29,8 @@ import {
|
|||||||
} from "rxjs/operators"
|
} from "rxjs/operators"
|
||||||
|
|
||||||
import { WorkerHandler, watchWorker } from "browser"
|
import { WorkerHandler, watchWorker } from "browser"
|
||||||
import { translate } from "utilities"
|
|
||||||
|
|
||||||
import { SearchIndex, SearchIndexPipeline } from "../../_"
|
import { SearchIndex } from "../../_"
|
||||||
import {
|
import {
|
||||||
SearchMessage,
|
SearchMessage,
|
||||||
SearchMessageType,
|
SearchMessageType,
|
||||||
@@ -52,38 +50,6 @@ interface SetupOptions {
|
|||||||
base$: Observable<string> /* Location base observable */
|
base$: Observable<string> /* Location base observable */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
|
||||||
* Helper functions
|
|
||||||
* ------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up search index
|
|
||||||
*
|
|
||||||
* @param data - Search index
|
|
||||||
*
|
|
||||||
* @return Search index
|
|
||||||
*/
|
|
||||||
function setupSearchIndex(
|
|
||||||
{ config, docs, index }: SearchIndex
|
|
||||||
): SearchIndex {
|
|
||||||
|
|
||||||
/* Override default language with value from translation */
|
|
||||||
if (config.lang.length === 1 && config.lang[0] === "en")
|
|
||||||
config.lang = [translate("search.config.lang")]
|
|
||||||
|
|
||||||
/* Override default separator with value from translation */
|
|
||||||
if (config.separator === "[\\s\\-]+")
|
|
||||||
config.separator = translate("search.config.separator")
|
|
||||||
|
|
||||||
/* Set pipeline from translation */
|
|
||||||
const pipeline = translate("search.config.pipeline")
|
|
||||||
.split(/\s*,\s*/)
|
|
||||||
.filter(identity) as SearchIndexPipeline
|
|
||||||
|
|
||||||
/* Return search index after defaulting */
|
|
||||||
return { config, docs, index, pipeline }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Functions
|
* Functions
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
@@ -112,11 +78,9 @@ export function setupSearchWorker(
|
|||||||
withLatestFrom(base$),
|
withLatestFrom(base$),
|
||||||
map(([message, base]) => {
|
map(([message, base]) => {
|
||||||
if (isSearchResultMessage(message)) {
|
if (isSearchResultMessage(message)) {
|
||||||
for (const { article, sections } of message.data) {
|
for (const result of message.data)
|
||||||
article.location = `${base}/${article.location}`
|
for (const document of result)
|
||||||
for (const section of sections)
|
document.location = `${base}/${document.location}`
|
||||||
section.location = `${base}/${section.location}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return message
|
return message
|
||||||
}),
|
}),
|
||||||
@@ -126,9 +90,9 @@ export function setupSearchWorker(
|
|||||||
/* Set up search index */
|
/* Set up search index */
|
||||||
index$
|
index$
|
||||||
.pipe(
|
.pipe(
|
||||||
map<SearchIndex, SearchSetupMessage>(index => ({
|
map<SearchIndex, SearchSetupMessage>(data => ({
|
||||||
type: SearchMessageType.SETUP,
|
type: SearchMessageType.SETUP,
|
||||||
data: setupSearchIndex(index)
|
data
|
||||||
})),
|
})),
|
||||||
observeOn(asyncScheduler)
|
observeOn(asyncScheduler)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,31 +22,74 @@
|
|||||||
|
|
||||||
import "lunr"
|
import "lunr"
|
||||||
|
|
||||||
import { Search, SearchIndexConfig } from "../../_"
|
import {
|
||||||
import { SearchMessage, SearchMessageType } from "../message"
|
Search,
|
||||||
|
SearchIndex,
|
||||||
|
SearchIndexConfig
|
||||||
|
} from "../../_"
|
||||||
|
import {
|
||||||
|
SearchMessage,
|
||||||
|
SearchMessageType
|
||||||
|
} from "../message"
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------------------
|
||||||
|
* Types
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add support for usage with `iframe-worker` polyfill
|
||||||
|
*
|
||||||
|
* While `importScripts` is synchronous when executed inside of a webworker,
|
||||||
|
* it's not possible to provide a synchronous polyfilled implementation. The
|
||||||
|
* cool thing is that awaiting a non-Promise is a noop, so extending the type
|
||||||
|
* definition to return a `Promise` shouldn't break anything.
|
||||||
|
*
|
||||||
|
* @see https://bit.ly/2PjDnXi - GitHub comment
|
||||||
|
*/
|
||||||
|
declare global {
|
||||||
|
function importScripts(...urls: string[]): Promise<void> | void
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Data
|
* Data
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search
|
* Search index
|
||||||
*/
|
*/
|
||||||
let search: Search
|
let index: Search
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Helper functions
|
* Helper functions
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up multi-language support through `lunr-languages`
|
* Fetch search index from given URL
|
||||||
|
*
|
||||||
|
* @param url - Search index URL
|
||||||
|
*
|
||||||
|
* @return Promise resolving with search index
|
||||||
|
*/
|
||||||
|
async function fetchSearchIndex(url: string): Promise<SearchIndex> {
|
||||||
|
return fetch(url, {
|
||||||
|
credentials: "same-origin"
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch (= import) multi-language support through `lunr-languages`
|
||||||
*
|
*
|
||||||
* This function will automatically import the stemmers necessary to process
|
* This function will automatically import the stemmers necessary to process
|
||||||
* the languages which were given through the search index configuration.
|
* the languages which were given through the search index configuration.
|
||||||
*
|
*
|
||||||
* @param config - Search index configuration
|
* @param config - Search index configuration
|
||||||
|
*
|
||||||
|
* @return Promise resolving with no result
|
||||||
*/
|
*/
|
||||||
function setupLunrLanguages(config: SearchIndexConfig): void {
|
async function setupSearchLanguages(
|
||||||
|
config: SearchIndexConfig
|
||||||
|
): Promise<void> {
|
||||||
const base = "../lunr"
|
const base = "../lunr"
|
||||||
|
|
||||||
/* Add scripts for languages */
|
/* Add scripts for languages */
|
||||||
@@ -62,7 +105,7 @@ function setupLunrLanguages(config: SearchIndexConfig): void {
|
|||||||
|
|
||||||
/* Load scripts synchronously */
|
/* Load scripts synchronously */
|
||||||
if (scripts.length)
|
if (scripts.length)
|
||||||
importScripts(
|
await importScripts(
|
||||||
`${base}/min/lunr.stemmer.support.min.js`,
|
`${base}/min/lunr.stemmer.support.min.js`,
|
||||||
...scripts
|
...scripts
|
||||||
)
|
)
|
||||||
@@ -79,13 +122,20 @@ function setupLunrLanguages(config: SearchIndexConfig): void {
|
|||||||
*
|
*
|
||||||
* @return Target message
|
* @return Target message
|
||||||
*/
|
*/
|
||||||
export function handler(message: SearchMessage): SearchMessage {
|
export async function handler(
|
||||||
|
message: SearchMessage
|
||||||
|
): Promise<SearchMessage> {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
|
|
||||||
/* Search setup message */
|
/* Search setup message */
|
||||||
case SearchMessageType.SETUP:
|
case SearchMessageType.SETUP:
|
||||||
setupLunrLanguages(message.data.config)
|
const data = typeof message.data === "string"
|
||||||
search = new Search(message.data)
|
? await fetchSearchIndex(message.data)
|
||||||
|
: message.data
|
||||||
|
|
||||||
|
/* Set up search index with multi-language support */
|
||||||
|
await setupSearchLanguages(data.config)
|
||||||
|
index = new Search(data)
|
||||||
return {
|
return {
|
||||||
type: SearchMessageType.READY
|
type: SearchMessageType.READY
|
||||||
}
|
}
|
||||||
@@ -94,7 +144,7 @@ export function handler(message: SearchMessage): SearchMessage {
|
|||||||
case SearchMessageType.QUERY:
|
case SearchMessageType.QUERY:
|
||||||
return {
|
return {
|
||||||
type: SearchMessageType.RESULT,
|
type: SearchMessageType.RESULT,
|
||||||
data: search ? search.query(message.data) : []
|
data: index ? index.search(message.data) : []
|
||||||
}
|
}
|
||||||
|
|
||||||
/* All other messages */
|
/* All other messages */
|
||||||
@@ -107,6 +157,6 @@ export function handler(message: SearchMessage): SearchMessage {
|
|||||||
* Worker
|
* Worker
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
addEventListener("message", ev => {
|
addEventListener("message", async ev => {
|
||||||
postMessage(handler(ev.data))
|
postMessage(await handler(ev.data))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const enum SearchMessageType {
|
|||||||
*/
|
*/
|
||||||
export interface SearchSetupMessage {
|
export interface SearchSetupMessage {
|
||||||
type: SearchMessageType.SETUP /* Message type */
|
type: SearchMessageType.SETUP /* Message type */
|
||||||
data: SearchIndex /* Message data */
|
data: SearchIndex | string /* Message data */
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,8 +20,12 @@
|
|||||||
* IN THE SOFTWARE.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SearchResult } from "integrations/search"
|
import {
|
||||||
import { h, truncate } from "utilities"
|
SearchDocument,
|
||||||
|
SearchMetadata,
|
||||||
|
SearchResult
|
||||||
|
} from "integrations/search"
|
||||||
|
import { h, translate, truncate } from "utilities"
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Data
|
* Data
|
||||||
@@ -33,10 +37,12 @@ import { h, truncate } from "utilities"
|
|||||||
const css = {
|
const css = {
|
||||||
item: "md-search-result__item",
|
item: "md-search-result__item",
|
||||||
link: "md-search-result__link",
|
link: "md-search-result__link",
|
||||||
|
more: "md-search-result__more",
|
||||||
article: "md-search-result__article md-search-result__article--document",
|
article: "md-search-result__article md-search-result__article--document",
|
||||||
section: "md-search-result__article",
|
section: "md-search-result__article",
|
||||||
title: "md-search-result__title",
|
title: "md-search-result__title",
|
||||||
teaser: "md-search-result__teaser"
|
teaser: "md-search-result__teaser",
|
||||||
|
terms: "md-search-result__terms"
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- */
|
/* ------------------------------------------------------------------------- */
|
||||||
@@ -53,6 +59,99 @@ const path =
|
|||||||
"18.88,20.32L22,23.39L23.39,22L20.31,18.9M16.5,19A2.5,2.5 0 0,1 " +
|
"18.88,20.32L22,23.39L23.39,22L20.31,18.9M16.5,19A2.5,2.5 0 0,1 " +
|
||||||
"14,16.5A2.5,2.5 0 0,1 16.5,14A2.5,2.5 0 0,1 19,16.5A2.5,2.5 0 0,1 16.5,19Z"
|
"14,16.5A2.5,2.5 0 0,1 16.5,14A2.5,2.5 0 0,1 19,16.5A2.5,2.5 0 0,1 16.5,19Z"
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------------------
|
||||||
|
* Helper function
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render an article document
|
||||||
|
*
|
||||||
|
* @param document - Article document
|
||||||
|
* @param teaser - Whether to render the teaser
|
||||||
|
*
|
||||||
|
* @return Element
|
||||||
|
*/
|
||||||
|
function renderArticleDocument(
|
||||||
|
{ location, title, text, terms, score }: SearchDocument & SearchMetadata,
|
||||||
|
teaser: boolean
|
||||||
|
) {
|
||||||
|
|
||||||
|
const highlight: string[] = []
|
||||||
|
for (const [value, found] of Object.entries(terms))
|
||||||
|
if (found)
|
||||||
|
highlight.push(value)
|
||||||
|
|
||||||
|
const url = new URL(location)
|
||||||
|
url.searchParams.append("h", highlight.join(" "))
|
||||||
|
|
||||||
|
const miss = Object.keys(terms)
|
||||||
|
// tslint:disable-next-line: array-type
|
||||||
|
.reduce<Array<Element | string>>((list, key) => [
|
||||||
|
...list, ...!terms[key] ? [<del>{key}</del>, " "] : []
|
||||||
|
], [])
|
||||||
|
return (
|
||||||
|
<a href={url.toString().replace(/%20/g, "+")} class={css.link} tabIndex={-1}>
|
||||||
|
<article class={css.article} data-md-score={score.toFixed(2)}>
|
||||||
|
<div class="md-search-result__icon md-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path d={path}></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 class={css.title}>{title}</h1>
|
||||||
|
{teaser && text.length > 0 &&
|
||||||
|
<p class={css.teaser}>{truncate(text, 320)}</p>
|
||||||
|
}
|
||||||
|
{teaser && miss.length > 0 &&
|
||||||
|
<p class={css.terms}>
|
||||||
|
{translate("search.result.term.missing")}: {...miss.slice(0, -1)}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</article>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a search document
|
||||||
|
*
|
||||||
|
* @param section - Search document
|
||||||
|
*
|
||||||
|
* @return Element
|
||||||
|
*/
|
||||||
|
function renderSection(
|
||||||
|
{ location, title, text, terms, score }: SearchDocument & SearchMetadata
|
||||||
|
) {
|
||||||
|
|
||||||
|
const highlight: string[] = []
|
||||||
|
for (const [value, found] of Object.entries(terms))
|
||||||
|
if (found)
|
||||||
|
highlight.push(value)
|
||||||
|
|
||||||
|
const url = new URL(location)
|
||||||
|
url.searchParams.append("h", highlight.join(" "))
|
||||||
|
|
||||||
|
const miss = Object.keys(terms)
|
||||||
|
// tslint:disable-next-line: array-type
|
||||||
|
.reduce<Array<Element | string>>((list, key) => [
|
||||||
|
...list, ...!terms[key] ? [<del>{key}</del>, " "] : []
|
||||||
|
], [])
|
||||||
|
return (
|
||||||
|
<a href={url.toString().replace(/%20/g, "+")} class={css.link} tabIndex={-1}>
|
||||||
|
<article class={css.section} data-md-score={score.toFixed(2)}>
|
||||||
|
<h1 class={css.title}>{title}</h1>
|
||||||
|
{text.length > 0 &&
|
||||||
|
<p class={css.teaser}>{truncate(text, 320)}</p>
|
||||||
|
}
|
||||||
|
{miss.length > 0 &&
|
||||||
|
<p class={css.terms}>
|
||||||
|
{translate("search.result.term.missing")}: {...miss.slice(0, -1)}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</article>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Functions
|
* Functions
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
@@ -65,31 +164,39 @@ const path =
|
|||||||
* @return Element
|
* @return Element
|
||||||
*/
|
*/
|
||||||
export function renderSearchResult(
|
export function renderSearchResult(
|
||||||
{ article, sections }: SearchResult
|
result: SearchResult, threshold: number = Infinity
|
||||||
) {
|
) {
|
||||||
|
const docs = [...result]
|
||||||
|
|
||||||
/* Render icon */
|
/* Find and extract parent article */
|
||||||
const icon = (
|
const parent = docs.findIndex(doc => !doc.location.includes("#"))
|
||||||
<div class="md-search-result__icon md-icon">
|
const [article] = docs.splice(parent, 1)
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path d={path}></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
/* Render article and sections */
|
/* Determine last index above threshold */
|
||||||
const children = [article, ...sections].map(document => {
|
let index = docs.findIndex(doc => doc.score < threshold)
|
||||||
const { location, title, text } = document
|
if (index === -1)
|
||||||
return (
|
index = docs.length
|
||||||
<a href={location} class={css.link} tabIndex={-1}>
|
|
||||||
<article class={"parent" in document ? css.section : css.article}>
|
/* Partition sections */
|
||||||
{!("parent" in document) && icon}
|
const best = docs.slice(0, index)
|
||||||
<h1 class={css.title}>{title}</h1>
|
const more = docs.slice(index)
|
||||||
{text.length > 0 && <p class={css.teaser}>{truncate(text, 320)}</p>}
|
|
||||||
</article>
|
/* Render children */
|
||||||
</a>
|
const children = [
|
||||||
)
|
renderArticleDocument(article, !parent && index === 0),
|
||||||
})
|
...best.map(renderSection),
|
||||||
|
...more.length ? [
|
||||||
|
<details class={css.more}>
|
||||||
|
<summary>
|
||||||
|
{more.length > 0 && more.length === 1
|
||||||
|
? translate("search.result.more.one")
|
||||||
|
: translate("search.result.more.other", more.length)
|
||||||
|
}
|
||||||
|
</summary>
|
||||||
|
{...more.map(renderSection)}
|
||||||
|
</details>
|
||||||
|
] : []
|
||||||
|
]
|
||||||
|
|
||||||
/* Render search result */
|
/* Render search result */
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ type TranslateKey =
|
|||||||
| "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.result.more.one" /* 1 more on this page */
|
||||||
|
| "search.result.more.other" /* # more on this page */
|
||||||
|
| "search.result.term.missing" /* Missing */
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Data
|
* Data
|
||||||
@@ -61,7 +64,9 @@ let lang: Record<string, string>
|
|||||||
*
|
*
|
||||||
* @return Translation
|
* @return Translation
|
||||||
*/
|
*/
|
||||||
export function translate(key: TranslateKey, value?: string): string {
|
export function translate(
|
||||||
|
key: TranslateKey, value?: string | number
|
||||||
|
): string {
|
||||||
if (typeof lang === "undefined") {
|
if (typeof lang === "undefined") {
|
||||||
const el = getElementOrThrow("#__lang")
|
const el = getElementOrThrow("#__lang")
|
||||||
lang = JSON.parse(el.textContent!)
|
lang = JSON.parse(el.textContent!)
|
||||||
@@ -70,7 +75,7 @@ export function translate(key: TranslateKey, value?: string): string {
|
|||||||
throw new ReferenceError(`Invalid translation: ${key}`)
|
throw new ReferenceError(`Invalid translation: ${key}`)
|
||||||
}
|
}
|
||||||
return typeof value !== "undefined"
|
return typeof value !== "undefined"
|
||||||
? lang[key].replace("#", value)
|
? lang[key].replace("#", value.toString())
|
||||||
: lang[key]
|
: lang[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,10 +136,10 @@ export function round(value: number): string {
|
|||||||
* @return Hash as 32bit integer
|
* @return Hash as 32bit integer
|
||||||
*/
|
*/
|
||||||
export function hash(value: string): number {
|
export function hash(value: string): number {
|
||||||
let h = 0
|
let h = 0
|
||||||
for (let i = 0, len = value.length; i < len; i++) {
|
for (let i = 0, len = value.length; i < len; i++) {
|
||||||
h = ((h << 5) - h) + value.charCodeAt(i)
|
h = ((h << 5) - h) + value.charCodeAt(i)
|
||||||
h |= 0 // Convert to 32bit integer
|
h |= 0 // Convert to 32bit integer
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,6 +66,8 @@
|
|||||||
// Hide radio buttons
|
// Hide radio buttons
|
||||||
> input {
|
> input {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
// Active tab label
|
// Active tab label
|
||||||
|
|||||||
@@ -560,12 +560,57 @@ $md-toggle__search--checked:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a little spacing on the teaser of the last link
|
// Add a little spacing on the last element of the last link
|
||||||
&:last-child .md-search-result__teaser {
|
&:last-child p:last-child {
|
||||||
margin-bottom: px2rem(12px);
|
margin-bottom: px2rem(12px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Search result container
|
||||||
|
&__more summary {
|
||||||
|
padding: px2em(12px) px2rem(16px);
|
||||||
|
color: var(--md-typeset-a-color);
|
||||||
|
font-size: px2rem(12.8px);
|
||||||
|
outline: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color 250ms,
|
||||||
|
background-color 250ms;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
|
||||||
|
// Focused or hovered button
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
color: var(--md-accent-fg-color);
|
||||||
|
background-color: var(--md-accent-fg-color--transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// [tablet landscape +]: Increase left indent
|
||||||
|
@include break-from-device(tablet landscape) {
|
||||||
|
padding-left: px2rem(44px);
|
||||||
|
|
||||||
|
// Adjust for right-to-left languages
|
||||||
|
[dir="rtl"] & {
|
||||||
|
padding-right: px2rem(44px);
|
||||||
|
padding-left: px2rem(16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove default details marker
|
||||||
|
&::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All following elements
|
||||||
|
& ~ * {
|
||||||
|
|
||||||
|
// Make less relevant terms more transparent
|
||||||
|
> * {
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Article - document or section
|
// Article - document or section
|
||||||
&__article {
|
&__article {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -626,39 +671,50 @@ $md-toggle__search--checked:
|
|||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: px2rem(12.8px);
|
font-size: px2rem(12.8px);
|
||||||
line-height: 1.4;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Teaser
|
// Teaser
|
||||||
&__teaser {
|
&__teaser {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
max-height: px2rem(33px);
|
max-height: px2rem(40px);
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--md-default-fg-color--light);
|
color: var(--md-default-fg-color--light);
|
||||||
font-size: px2rem(12.8px);
|
font-size: px2rem(12.8px);
|
||||||
line-height: 1.4;
|
line-height: 1.6;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
|
|
||||||
// [mobile -]: Increase number of lines
|
// [mobile -]: Increase number of lines
|
||||||
@include break-to-device(mobile) {
|
@include break-to-device(mobile) {
|
||||||
max-height: px2rem(50px);
|
max-height: px2rem(60px);
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [tablet landscape]: Increase number of lines
|
// [tablet landscape]: Increase number of lines
|
||||||
@include break-at-device(tablet landscape) {
|
@include break-at-device(tablet landscape) {
|
||||||
max-height: px2rem(50px);
|
max-height: px2rem(60px);
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Search term highlighting
|
||||||
|
mark {
|
||||||
|
background-color: transparent;
|
||||||
|
border-bottom: px2rem(1px) solid var(--md-accent-fg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terms
|
||||||
|
&__terms {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
font-size: px2rem(12.8px);
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search term highlighting
|
// Search term highlighting
|
||||||
em {
|
mark {
|
||||||
font-weight: 700;
|
color: var(--md-accent-fg-color);
|
||||||
font-style: normal;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,6 @@
|
|||||||
|
|
||||||
{% import "partials/language.html" as lang with context %}
|
{% import "partials/language.html" as lang with context %}
|
||||||
|
|
||||||
<!-- Theme options -->
|
|
||||||
{% set palette = config.theme.palette %}
|
|
||||||
{% if not palette is mapping %}
|
|
||||||
{% set palette = palette | first %}
|
|
||||||
{% endif %}
|
|
||||||
{% set font = config.theme.font %}
|
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="{{ lang.t('language') }}" class="no-js">
|
<html lang="{{ lang.t('language') }}" class="no-js">
|
||||||
<head>
|
<head>
|
||||||
@@ -83,20 +76,21 @@
|
|||||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.css' | url }}" />
|
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.css' | url }}" />
|
||||||
|
|
||||||
<!-- Extra color palette -->
|
<!-- Extra color palette -->
|
||||||
{% if palette.scheme or palette.primary or palette.accent %}
|
{% if config.theme.palette %}
|
||||||
|
{% set palette = config.theme.palette %}
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="{{ 'assets/stylesheets/palette.css' | url }}"
|
href="{{ 'assets/stylesheets/palette.css' | url }}"
|
||||||
/>
|
/>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Theme-color meta tag for Android -->
|
<!-- Theme-color meta tag for Android -->
|
||||||
{% if palette.primary %}
|
{% if palette.primary %}
|
||||||
{% import "partials/palette.html" as map %}
|
{% import "partials/palette.html" as map %}
|
||||||
{% set primary = map.primary(
|
{% set primary = map.primary(
|
||||||
palette.primary | replace(" ", "-") | lower
|
palette.primary | replace(" ", "-") | lower
|
||||||
) %}
|
) %}
|
||||||
<meta name="theme-color" content="{{ primary }}" />
|
<meta name="theme-color" content="{{ primary }}" />
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -107,7 +101,8 @@
|
|||||||
{% block fonts %}
|
{% block fonts %}
|
||||||
|
|
||||||
<!-- Load fonts from Google -->
|
<!-- Load fonts from Google -->
|
||||||
{% if font != false %}
|
{% if config.theme.font != false %}
|
||||||
|
{% set font = config.theme.font %}
|
||||||
<link href="https://fonts.gstatic.com" rel="preconnect" crossorigin />
|
<link href="https://fonts.gstatic.com" rel="preconnect" crossorigin />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
@@ -156,8 +151,12 @@
|
|||||||
|
|
||||||
<!-- Text direction and color palette, if defined -->
|
<!-- Text direction and color palette, if defined -->
|
||||||
{% set direction = config.theme.direction or lang.t('direction') %}
|
{% set direction = config.theme.direction or lang.t('direction') %}
|
||||||
{% if palette.scheme or palette.primary or palette.accent %}
|
{% if config.theme.palette %}
|
||||||
{% set scheme = palette.scheme | lower %}
|
{% set palette = config.theme.palette %}
|
||||||
|
{% if not palette is mapping %}
|
||||||
|
{% set palette = palette | first %}
|
||||||
|
{% endif %}
|
||||||
|
{% set scheme = palette.scheme | replace(" ", "-") | lower %}
|
||||||
{% set primary = palette.primary | replace(" ", "-") | lower %}
|
{% set primary = palette.primary | replace(" ", "-") | lower %}
|
||||||
{% set accent = palette.accent | replace(" ", "-") | lower %}
|
{% set accent = palette.accent | replace(" ", "-") | lower %}
|
||||||
<body
|
<body
|
||||||
@@ -166,18 +165,19 @@
|
|||||||
data-md-color-primary="{{ primary }}"
|
data-md-color-primary="{{ primary }}"
|
||||||
data-md-color-accent="{{ accent }}"
|
data-md-color-accent="{{ accent }}"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<!-- Experimental: set color scheme based on preference -->
|
||||||
|
{% if "preference" == scheme %}
|
||||||
|
<script>
|
||||||
|
if (matchMedia("(prefers-color-scheme: dark)").matches)
|
||||||
|
document.body.setAttribute("data-md-color-scheme", "slate")
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<body dir="{{ direction }}">
|
<body dir="{{ direction }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Experimental: set color scheme based on preference -->
|
|
||||||
{% if "preference" == palette.scheme %}
|
|
||||||
<script>
|
|
||||||
if (matchMedia("(prefers-color-scheme: dark)").matches)
|
|
||||||
document.body.setAttribute("data-md-color-scheme", "slate")
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
State toggles - we need to set autocomplete="off" in order to reset the
|
State toggles - we need to set autocomplete="off" in order to reset the
|
||||||
drawer on back button invocation in some browsers
|
drawer on back button invocation in some browsers
|
||||||
@@ -346,7 +346,10 @@
|
|||||||
"search.result.placeholder",
|
"search.result.placeholder",
|
||||||
"search.result.none",
|
"search.result.none",
|
||||||
"search.result.one",
|
"search.result.one",
|
||||||
"search.result.other"
|
"search.result.other",
|
||||||
|
"search.result.more.one",
|
||||||
|
"search.result.more.other",
|
||||||
|
"search.result.term.missing"
|
||||||
] -%}
|
] -%}
|
||||||
{%- set _ = translations.update({ key: lang.t(key) }) -%}
|
{%- set _ = translations.update({ key: lang.t(key) }) -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@
|
|||||||
"search.result.none": "Keine Suchergebnisse",
|
"search.result.none": "Keine Suchergebnisse",
|
||||||
"search.result.one": "1 Suchergebnis",
|
"search.result.one": "1 Suchergebnis",
|
||||||
"search.result.other": "# Suchergebnisse",
|
"search.result.other": "# Suchergebnisse",
|
||||||
|
"search.result.more.one": "1 weiteres Suchergebnis auf dieser Seite",
|
||||||
|
"search.result.more.other": "# weitere Suchergebnisse auf dieser Seite",
|
||||||
|
"search.result.term.missing": "Es fehlt",
|
||||||
"skip.link.title": "Zum Inhalt",
|
"skip.link.title": "Zum Inhalt",
|
||||||
"source.link.title": "Quellcode",
|
"source.link.title": "Quellcode",
|
||||||
"source.revision.date": "Letztes Update",
|
"source.revision.date": "Letztes Update",
|
||||||
|
|||||||
@@ -44,6 +44,9 @@
|
|||||||
"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.result.more.one": "1 more on this page",
|
||||||
|
"search.result.more.other": "# more on this page",
|
||||||
|
"search.result.term.missing": "Missing",
|
||||||
"skip.link.title": "Skip to content",
|
"skip.link.title": "Skip to content",
|
||||||
"source.link.title": "Go to repository",
|
"source.link.title": "Go to repository",
|
||||||
"source.revision.date": "Last update",
|
"source.revision.date": "Last update",
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
"search.result.none": "Inga sökresultat",
|
"search.result.none": "Inga sökresultat",
|
||||||
"search.result.one": "1 sökresultat",
|
"search.result.one": "1 sökresultat",
|
||||||
"search.result.other": "# sökresultat",
|
"search.result.other": "# sökresultat",
|
||||||
|
"search.result.more.one": "1 till på denna sidan",
|
||||||
|
"search.result.more.other": "# till på denna sidan",
|
||||||
|
"search.result.term.missing": "Saknas",
|
||||||
"skip.link.title": "Gå till innehållet",
|
"skip.link.title": "Gå till innehållet",
|
||||||
"source.link.title": "Gå till datakatalog",
|
"source.link.title": "Gå till datakatalog",
|
||||||
"source.revision.date": "Senaste uppdateringen",
|
"source.revision.date": "Senaste uppdateringen",
|
||||||
|
|||||||
Reference in New Issue
Block a user