mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-11-28 01:10:58 +01:00
Merge branch 'refactor/rxjs-typescript' into feature/landing-page
This commit is contained in:
commit
77e60a9bb8
@ -387,7 +387,7 @@ from the template.
|
||||
```
|
||||
|
||||
[12]: http://www.materialui.co/colors
|
||||
[13]: customization.md/#additional-stylesheets
|
||||
[13]: customization.md#additional-stylesheets
|
||||
|
||||
#### Primary color
|
||||
|
||||
|
2
material/assets/javascripts/bundle.aa7a7592.min.js
vendored
Normal file
2
material/assets/javascripts/bundle.aa7a7592.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
material/assets/javascripts/bundle.aa7a7592.min.js.map
Normal file
1
material/assets/javascripts/bundle.aa7a7592.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
31
material/assets/javascripts/vendor.3340e0de.min.js
vendored
Normal file
31
material/assets/javascripts/vendor.3340e0de.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
material/assets/javascripts/vendor.3340e0de.min.js.map
Normal file
1
material/assets/javascripts/vendor.3340e0de.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
59
material/assets/javascripts/worker/search.3bc815f0.min.js
vendored
Normal file
59
material/assets/javascripts/worker/search.3bc815f0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,10 +1,10 @@
|
||||
{
|
||||
"assets/javascripts/bundle.js": "assets/javascripts/bundle.cad4abbb.min.js",
|
||||
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.cad4abbb.min.js.map",
|
||||
"assets/javascripts/vendor.js": "assets/javascripts/vendor.0c35f0aa.min.js",
|
||||
"assets/javascripts/vendor.js.map": "assets/javascripts/vendor.0c35f0aa.min.js.map",
|
||||
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.2613054f.min.js",
|
||||
"assets/javascripts/worker/search.js.map": "assets/javascripts/worker/search.2613054f.min.js.map",
|
||||
"assets/javascripts/bundle.js": "assets/javascripts/bundle.aa7a7592.min.js",
|
||||
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.aa7a7592.min.js.map",
|
||||
"assets/javascripts/vendor.js": "assets/javascripts/vendor.3340e0de.min.js",
|
||||
"assets/javascripts/vendor.js.map": "assets/javascripts/vendor.3340e0de.min.js.map",
|
||||
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.3bc815f0.min.js",
|
||||
"assets/javascripts/worker/search.js.map": "assets/javascripts/worker/search.3bc815f0.min.js.map",
|
||||
"assets/stylesheets/main.css": "assets/stylesheets/main.b32d3181.min.css",
|
||||
"assets/stylesheets/palette.css": "assets/stylesheets/palette.4444686e.min.css"
|
||||
}
|
@ -173,12 +173,9 @@
|
||||
{% include "partials/footer.html" %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% block config %}
|
||||
<script>var __config={}</script>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="{{ 'assets/javascripts/vendor.0c35f0aa.min.js' | url }}"></script>
|
||||
<script src="{{ 'assets/javascripts/bundle.cad4abbb.min.js' | url }}"></script>
|
||||
<script src="{{ 'assets/javascripts/vendor.3340e0de.min.js' | url }}"></script>
|
||||
<script src="{{ 'assets/javascripts/bundle.aa7a7592.min.js' | url }}"></script>
|
||||
{%- set translations = {} -%}
|
||||
{%- for key in [
|
||||
"clipboard.copy",
|
||||
@ -194,18 +191,17 @@
|
||||
{%- set _ = translations.update({ key: lang.t(key) }) -%}
|
||||
{%- endfor -%}
|
||||
<script id="__lang" type="application/json">
|
||||
{{ translations | tojson }}
|
||||
{{- translations | tojson -}}
|
||||
</script>
|
||||
{% block config %}{% endblock %}
|
||||
<script>
|
||||
__material = initialize(Object.assign({
|
||||
url: {
|
||||
base: "{{ base_url }}",
|
||||
worker: {
|
||||
search: "{{ 'assets/javascripts/worker/search.2613054f.min.js' | url }}"
|
||||
}
|
||||
},
|
||||
features: {{ config.theme.features | tojson }}
|
||||
}, typeof __config !== "undefined" ? __config : {}))
|
||||
app = initialize({
|
||||
base: "{{ base_url }}",
|
||||
features: {{ config.theme.features | tojson }},
|
||||
search: Object.assign({
|
||||
worker: "{{ 'assets/javascripts/worker/search.3bc815f0.min.js' | url }}"
|
||||
}, typeof search !== "undefined" && search)
|
||||
})
|
||||
</script>
|
||||
{% for path in config["extra_javascript"] %}
|
||||
<script src="{{ path | url }}"></script>
|
||||
|
@ -51,7 +51,7 @@ interface WatchOptions {
|
||||
/**
|
||||
* Watch document switch
|
||||
*
|
||||
* This function returns an observables that fetches a document if the provided
|
||||
* This function returns an observable that fetches a document if the provided
|
||||
* location observable emits a new value (i.e. URL). If the emitted URL points
|
||||
* to the same page, the request is effectively ignored (i.e. when only the
|
||||
* fragment identifier changes).
|
||||
|
@ -87,3 +87,17 @@ export function getElements<T extends HTMLElement>(
|
||||
): T[] {
|
||||
return Array.from(node.querySelectorAll<T>(selector))
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Replace an element with another element
|
||||
*
|
||||
* @param source - Source element
|
||||
* @param target - Target element
|
||||
*/
|
||||
export function replaceElement(
|
||||
source: HTMLElement, target: Node
|
||||
): void {
|
||||
source.replaceWith(target)
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
*/
|
||||
|
||||
import { Observable, fromEvent, merge } from "rxjs"
|
||||
import { mapTo, shareReplay, startWith } from "rxjs/operators"
|
||||
import { map, shareReplay, startWith } from "rxjs/operators"
|
||||
|
||||
import { getActiveElement } from "../_"
|
||||
|
||||
@ -56,15 +56,12 @@ el: HTMLElement, value: boolean = true
|
||||
export function watchElementFocus(
|
||||
el: HTMLElement
|
||||
): Observable<boolean> {
|
||||
const focus$ = fromEvent(el, "focus")
|
||||
const blur$ = fromEvent(el, "blur")
|
||||
|
||||
/* Map events to boolean state */
|
||||
return merge(
|
||||
focus$.pipe(mapTo(true)),
|
||||
blur$.pipe(mapTo(false))
|
||||
fromEvent<FocusEvent>(el, "focus"),
|
||||
fromEvent<FocusEvent>(el, "blur")
|
||||
)
|
||||
.pipe(
|
||||
map(({ type }) => type === "focus"),
|
||||
startWith(el === getActiveElement()),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
@ -66,8 +66,8 @@ export function watchElementOffset(
|
||||
el: HTMLElement
|
||||
): Observable<ElementOffset> {
|
||||
return merge(
|
||||
fromEvent<UIEvent>(el, "scroll"),
|
||||
fromEvent<UIEvent>(window, "resize")
|
||||
fromEvent(el, "scroll"),
|
||||
fromEvent(window, "resize")
|
||||
)
|
||||
.pipe(
|
||||
map(() => getElementOffset(el)),
|
||||
|
@ -20,7 +20,7 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import { BehaviorSubject, Subject } from "rxjs"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
@ -52,14 +52,14 @@ export function setLocation(url: URL): void {
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Check whether a URL is an internal link or a file (except `.html`)
|
||||
* Check whether a URL is a local link or a file (except `.html`)
|
||||
*
|
||||
* @param url - URL or HTML anchor element
|
||||
* @param ref - Reference URL
|
||||
*
|
||||
* @return Test result
|
||||
*/
|
||||
export function isLocationInternal(
|
||||
export function isLocalLocation(
|
||||
url: URL | HTMLAnchorElement,
|
||||
ref: URL | Location = location
|
||||
): boolean {
|
||||
@ -75,7 +75,7 @@ export function isLocationInternal(
|
||||
*
|
||||
* @return Test result
|
||||
*/
|
||||
export function isLocationAnchor(
|
||||
export function isAnchorLocation(
|
||||
url: URL | HTMLAnchorElement,
|
||||
ref: URL | Location = location
|
||||
): boolean {
|
||||
@ -90,6 +90,6 @@ export function isLocationAnchor(
|
||||
*
|
||||
* @return Location subject
|
||||
*/
|
||||
export function watchLocation(): BehaviorSubject<URL> {
|
||||
export function watchLocation(): Subject<URL> {
|
||||
return new BehaviorSubject<URL>(getLocation())
|
||||
}
|
||||
|
56
src/assets/javascripts/browser/location/base/index.ts
Normal file
56
src/assets/javascripts/browser/location/base/index.ts
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { Observable } from "rxjs"
|
||||
import { map, shareReplay, take } from "rxjs/operators"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Watch options
|
||||
*/
|
||||
interface WatchOptions {
|
||||
location$: Observable<URL> /* Location observable */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Watch location base
|
||||
*
|
||||
* @return Location base observable
|
||||
*/
|
||||
export function watchLocationBase(
|
||||
base: string, { location$ }: WatchOptions
|
||||
): Observable<string> {
|
||||
return location$
|
||||
.pipe(
|
||||
take(1),
|
||||
map(({ href }) => new URL(base, href)
|
||||
.toString()
|
||||
.replace(/\/$/, "")
|
||||
),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
@ -40,9 +40,9 @@ export function getLocationHash(): string {
|
||||
* Set location hash
|
||||
*
|
||||
* Setting a new fragment identifier via `location.hash` will have no effect
|
||||
* if the value doesn't change. However, when a new fragment identifier is set,
|
||||
* we want the browser to target the respective element at all times, which is
|
||||
* why we use this dirty little trick.
|
||||
* if the value doesn't change. When a new fragment identifier is set, we want
|
||||
* the browser to target the respective element at all times, which is why we
|
||||
* use this dirty little trick.
|
||||
*
|
||||
* @param hash - Location hash
|
||||
*/
|
||||
|
@ -21,4 +21,5 @@
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./base"
|
||||
export * from "./hash"
|
||||
|
@ -74,8 +74,8 @@ export function setViewportOffset(
|
||||
*/
|
||||
export function watchViewportOffset(): Observable<ViewportOffset> {
|
||||
return merge(
|
||||
fromEvent<UIEvent>(window, "scroll", { passive: true }),
|
||||
fromEvent<UIEvent>(window, "resize", { passive: true })
|
||||
fromEvent(window, "scroll", { passive: true }),
|
||||
fromEvent(window, "resize", { passive: true })
|
||||
)
|
||||
.pipe(
|
||||
map(getViewportOffset),
|
||||
|
@ -59,7 +59,7 @@ export function getViewportSize(): ViewportSize {
|
||||
* @return Viewport size observable
|
||||
*/
|
||||
export function watchViewportSize(): Observable<ViewportSize> {
|
||||
return fromEvent<UIEvent>(window, "resize")
|
||||
return fromEvent(window, "resize")
|
||||
.pipe(
|
||||
map(getViewportSize),
|
||||
startWith(getViewportSize())
|
||||
|
@ -38,7 +38,7 @@ import {
|
||||
*/
|
||||
export interface WorkerMessage {
|
||||
type: unknown /* Message type */
|
||||
data: unknown /* Message data */
|
||||
data?: unknown /* Message data */
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,7 +29,7 @@ import {
|
||||
switchMap
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { getElement } from "browser"
|
||||
import { getElement, replaceElement } from "browser"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
@ -85,7 +85,7 @@ let components$: Observable<ComponentMap>
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup bindings to components with given names
|
||||
* Set up bindings to components with given names
|
||||
*
|
||||
* This function will maintain bindings to the elements identified by the given
|
||||
* names in-between document switches and update the elements in-place.
|
||||
@ -118,7 +118,7 @@ export function setupComponents(
|
||||
case "container":
|
||||
case "skip":
|
||||
if (name in prev && typeof prev[name] !== "undefined") {
|
||||
prev[name]!.replaceWith(next[name]!)
|
||||
replaceElement(prev[name]!, next[name]!)
|
||||
prev[name] = next[name]
|
||||
}
|
||||
break
|
||||
@ -149,7 +149,7 @@ export function setupComponents(
|
||||
*
|
||||
* @param name - Component name
|
||||
*
|
||||
* @return Element observable
|
||||
* @return Component observable
|
||||
*/
|
||||
export function useComponent<T extends HTMLInputElement>(
|
||||
name: "search-query"
|
||||
|
@ -99,7 +99,7 @@ export function watchMain(
|
||||
}))
|
||||
)
|
||||
),
|
||||
distinctUntilKeyChanged("top"),
|
||||
distinctUntilKeyChanged("bottom"),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
|
@ -31,8 +31,9 @@ import { WorkerHandler, setToggle } from "browser"
|
||||
import {
|
||||
SearchMessage,
|
||||
SearchMessageType,
|
||||
SearchQueryMessage
|
||||
} from "workers"
|
||||
SearchQueryMessage,
|
||||
SearchTransformFn
|
||||
} from "integrations"
|
||||
|
||||
import { watchSearchQuery } from "../react"
|
||||
|
||||
@ -48,6 +49,13 @@ export interface SearchQuery {
|
||||
focus: boolean /* Query focus */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search query transform
|
||||
*/
|
||||
export type SearchQueryTransform = (value: string) => string
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
@ -56,7 +64,7 @@ export interface SearchQuery {
|
||||
* Mount options
|
||||
*/
|
||||
interface MountOptions {
|
||||
transform?(value: string): string /* Transformation function */
|
||||
transform?: SearchTransformFn /* Transformation function */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { watchElementFocus } from "browser"
|
||||
import { SearchTransformFn, defaultTransform } from "integrations"
|
||||
|
||||
import { SearchQuery } from "../_"
|
||||
|
||||
@ -40,28 +41,7 @@ import { SearchQuery } from "../_"
|
||||
* Watch options
|
||||
*/
|
||||
interface WatchOptions {
|
||||
transform?(value: string): string /* Transformation function */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Default transformation function
|
||||
*
|
||||
* Rogue control characters are filtered before handing the query to the
|
||||
* search index, as `lunr` will throw otherwise.
|
||||
*
|
||||
* @param value - Query value
|
||||
*
|
||||
* @return Transformed query value
|
||||
*/
|
||||
function defaultTransform(value: string): string {
|
||||
return value
|
||||
.replace(/(?:^|\s+)[*+-:^~]+(?=\s+|$)/g, "")
|
||||
.trim()
|
||||
.replace(/\s+|\b$/g, "* ")
|
||||
transform?: SearchTransformFn /* Transformation function */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
|
@ -31,11 +31,11 @@ import {
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { WorkerHandler, watchElementOffset } from "browser"
|
||||
import { SearchResult } from "integrations/search"
|
||||
import {
|
||||
SearchMessage,
|
||||
SearchResult,
|
||||
isSearchResultMessage
|
||||
} from "workers"
|
||||
} from "integrations"
|
||||
|
||||
import { SearchQuery } from "../../query"
|
||||
import { applySearchResult } from "../react"
|
||||
|
@ -73,7 +73,7 @@ export function resetSearchResultMeta(
|
||||
* @param child - Search result element
|
||||
*/
|
||||
export function addToSearchResultList(
|
||||
el: HTMLElement, child: HTMLElement
|
||||
el: HTMLElement, child: Element
|
||||
): void {
|
||||
el.appendChild(child)
|
||||
}
|
||||
|
@ -33,8 +33,10 @@ import {
|
||||
animationFrameScheduler,
|
||||
fromEvent,
|
||||
of,
|
||||
NEVER
|
||||
NEVER,
|
||||
from
|
||||
} from "rxjs"
|
||||
import { ajax } from "rxjs/ajax"
|
||||
import {
|
||||
delay,
|
||||
switchMap,
|
||||
@ -44,7 +46,8 @@ import {
|
||||
observeOn,
|
||||
take,
|
||||
shareReplay,
|
||||
share
|
||||
share,
|
||||
pluck
|
||||
} from "rxjs/operators"
|
||||
|
||||
import {
|
||||
@ -56,12 +59,11 @@ import {
|
||||
watchLocation,
|
||||
watchLocationHash,
|
||||
watchViewport,
|
||||
isLocationInternal,
|
||||
isLocationAnchor,
|
||||
setLocationHash
|
||||
} from "./browser"
|
||||
import { setupSearchWorker } from "./workers"
|
||||
|
||||
isLocalLocation,
|
||||
isAnchorLocation,
|
||||
setLocationHash,
|
||||
watchLocationBase
|
||||
} from "browser"
|
||||
import {
|
||||
mountHeader,
|
||||
mountHero,
|
||||
@ -76,10 +78,14 @@ import {
|
||||
mountSearchReset,
|
||||
mountSearchResult
|
||||
} from "components"
|
||||
import { setupClipboard } from "./integrations/clipboard"
|
||||
import { setupDialog } from "integrations/dialog"
|
||||
import { setupKeyboard } from "./integrations/keyboard"
|
||||
import { setupInstantLoading } from "integrations/instant"
|
||||
import {
|
||||
setupClipboard,
|
||||
setupDialog,
|
||||
setupKeyboard,
|
||||
setupInstantLoading,
|
||||
setupSearchWorker,
|
||||
SearchIndex
|
||||
} from "integrations"
|
||||
import {
|
||||
patchTables,
|
||||
patchDetails,
|
||||
@ -91,6 +97,7 @@ import { isConfig } from "utilities"
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/* Denote that JavaScript is available */
|
||||
document.documentElement.classList.remove("no-js")
|
||||
document.documentElement.classList.add("js")
|
||||
|
||||
@ -139,19 +146,22 @@ export function initialize(config: unknown) {
|
||||
if (!isConfig(config))
|
||||
throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`)
|
||||
|
||||
/* Setup user interface observables */
|
||||
/* Set up user interface observables */
|
||||
const location$ = watchLocation()
|
||||
const base$ = watchLocationBase(config.base, { location$ })
|
||||
const hash$ = watchLocationHash()
|
||||
const viewport$ = watchViewport()
|
||||
const tablet$ = watchMedia("(min-width: 960px)")
|
||||
const screen$ = watchMedia("(min-width: 1220px)")
|
||||
|
||||
/* Setup document observable */
|
||||
/* Set up document observable */
|
||||
const document$ = config.features.includes("instant")
|
||||
? watchDocument({ location$ })
|
||||
: watchDocument()
|
||||
|
||||
/* Setup component bindings */
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
/* Set up component bindings */
|
||||
setupComponents([
|
||||
"container", /* Container */
|
||||
"header", /* Header */
|
||||
@ -168,17 +178,19 @@ export function initialize(config: unknown) {
|
||||
"toc" /* Table of contents */
|
||||
], { document$ })
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
const keyboard$ = setupKeyboard()
|
||||
|
||||
// External index
|
||||
const index = config.search && config.search.index
|
||||
? config.search.index
|
||||
: undefined
|
||||
patchDetails({ document$, hash$ })
|
||||
patchScripts({ document$ })
|
||||
patchSource({ document$ })
|
||||
patchTables({ document$ })
|
||||
|
||||
// TODO: pass URL config as first parameter, options as second
|
||||
const worker = setupSearchWorker(config.url.worker.search, {
|
||||
base: config.url.base, index, location$
|
||||
})
|
||||
/* Force 1px scroll offset to trigger overflow scrolling */
|
||||
patchScrollfix({ document$ })
|
||||
|
||||
/* Set up clipboard and dialog */
|
||||
const dialog$ = setupDialog()
|
||||
const clipboard$ = setupClipboard({ document$, dialog$ })
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
@ -197,37 +209,6 @@ export function initialize(config: unknown) {
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
/* Mount search query */
|
||||
const query$ = useComponent("search-query")
|
||||
.pipe(
|
||||
mountSearchQuery(worker),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
/* Mount search reset */
|
||||
const reset$ = useComponent("search-reset")
|
||||
.pipe(
|
||||
mountSearchReset(),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
/* Mount search result */
|
||||
const result$ = useComponent("search-result")
|
||||
.pipe(
|
||||
mountSearchResult(worker, { query$ }),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
const search$ = useComponent("search")
|
||||
.pipe(
|
||||
mountSearch({ query$, reset$, result$ }),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
const navigation$ = useComponent("navigation")
|
||||
.pipe(
|
||||
mountNavigation({ header$, main$, viewport$, screen$ }),
|
||||
@ -254,19 +235,62 @@ export function initialize(config: unknown) {
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
const keyboard$ = setupKeyboard()
|
||||
// External index
|
||||
const index = config.search && config.search.index
|
||||
? config.search.index
|
||||
: undefined
|
||||
|
||||
patchDetails({ document$, hash$ })
|
||||
patchScripts({ document$ })
|
||||
patchSource({ document$ })
|
||||
patchTables({ document$ })
|
||||
/* Fetch index if it wasn't passed explicitly */
|
||||
const index$ = typeof index !== "undefined"
|
||||
? from(index)
|
||||
: base$
|
||||
.pipe(
|
||||
switchMap(base => ajax({
|
||||
url: `${base}/search/search_index.json`,
|
||||
responseType: "json",
|
||||
withCredentials: true
|
||||
})
|
||||
.pipe<SearchIndex>(
|
||||
pluck("response")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
/* Force 1px scroll offset to trigger overflow scrolling */
|
||||
patchScrollfix({ document$ })
|
||||
|
||||
/* Setup clipboard and dialog */
|
||||
const dialog$ = setupDialog()
|
||||
const clipboard$ = setupClipboard({ document$, dialog$ })
|
||||
const worker = setupSearchWorker(config.search.worker, {
|
||||
base$, index$
|
||||
})
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
/* Mount search query */
|
||||
const query$ = useComponent("search-query")
|
||||
.pipe(
|
||||
mountSearchQuery(worker, { transform: config.search.transform }),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
/* Mount search reset */
|
||||
const reset$ = useComponent("search-reset")
|
||||
.pipe(
|
||||
mountSearchReset(),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
/* Mount search result */
|
||||
const result$ = useComponent("search-result")
|
||||
.pipe(
|
||||
mountSearchResult(worker, { query$ }),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
const search$ = useComponent("search")
|
||||
.pipe(
|
||||
mountSearch({ query$, reset$, result$ }),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
@ -310,8 +334,8 @@ export function initialize(config: unknown) {
|
||||
switchMap(ev => {
|
||||
if (ev.target instanceof HTMLElement) {
|
||||
const el = ev.target.closest("a") // TODO: abstract as link click?
|
||||
if (el && isLocationInternal(el)) {
|
||||
if (!isLocationAnchor(el) && config.features.includes("instant"))
|
||||
if (el && isLocalLocation(el)) {
|
||||
if (!isAnchorLocation(el) && config.features.includes("instant"))
|
||||
ev.preventDefault()
|
||||
return of(el)
|
||||
}
|
||||
@ -326,8 +350,6 @@ export function initialize(config: unknown) {
|
||||
setToggle("drawer", false)
|
||||
})
|
||||
|
||||
// somehow call this setupNavigation ?
|
||||
|
||||
// instant loading
|
||||
if (config.features.includes("instant")) {
|
||||
|
||||
|
@ -45,7 +45,7 @@ interface SetupOptions {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup clipboard
|
||||
* Set up clipboard
|
||||
*
|
||||
* This function implements the Clipboard.js integration and injects a button
|
||||
* into all code blocks when the document changes.
|
||||
@ -70,7 +70,7 @@ export function setupClipboard(
|
||||
})
|
||||
})
|
||||
|
||||
/* Initialize and setup clipboard */
|
||||
/* Initialize clipboard */
|
||||
const clipboard$ = fromEventPattern<ClipboardJS.Event>(next => {
|
||||
new ClipboardJS(".md-clipboard").on("success", next)
|
||||
})
|
||||
|
@ -45,7 +45,7 @@ interface SetupOptions {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup dialog
|
||||
* Set up dialog
|
||||
*
|
||||
* @param options - Options
|
||||
*
|
||||
|
@ -20,4 +20,8 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "./clipboard"
|
||||
export * from "./dialog"
|
||||
export * from "./instant"
|
||||
export * from "./keyboard"
|
||||
export * from "./search"
|
@ -38,7 +38,7 @@ import {
|
||||
Viewport,
|
||||
ViewportOffset,
|
||||
getElement,
|
||||
isLocationAnchor,
|
||||
isAnchorLocation,
|
||||
setLocationHash,
|
||||
setViewportOffset
|
||||
} from "browser"
|
||||
@ -72,7 +72,7 @@ interface SetupOptions {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup instant loading
|
||||
* Set up instant loading
|
||||
*
|
||||
* @param options - Options
|
||||
*
|
||||
@ -91,7 +91,7 @@ export function setupInstantLoading(
|
||||
const push$ = state$
|
||||
.pipe(
|
||||
distinctUntilChanged((prev, next) => prev.url.href === next.url.href),
|
||||
filter(({ url }) => !isLocationAnchor(url)),
|
||||
filter(({ url }) => !isAnchorLocation(url)),
|
||||
share()
|
||||
)
|
||||
|
||||
@ -129,7 +129,7 @@ export function setupInstantLoading(
|
||||
bufferCount(2, 1),
|
||||
filter(([prev, next]) => {
|
||||
return prev.url.pathname === next.url.pathname
|
||||
&& !isLocationAnchor(next.url)
|
||||
&& !isAnchorLocation(next.url)
|
||||
}),
|
||||
map(([, state]) => state)
|
||||
)
|
||||
|
@ -67,9 +67,9 @@ export interface Keyboard extends Key {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup keyboard
|
||||
* Set up keyboard
|
||||
*
|
||||
* This function will setup the keyboard handlers and ensure that keys are
|
||||
* This function will set up the keyboard handlers and ensure that keys are
|
||||
* correctly propagated. Currently there are two modes:
|
||||
*
|
||||
* - `global`: This mode is active when the search is closed. It is intended
|
||||
@ -92,10 +92,18 @@ export function setupKeyboard(): Observable<Keyboard> {
|
||||
mode: getToggle("search") ? "search" : "global",
|
||||
...key
|
||||
})),
|
||||
filter(({ mode }) => {
|
||||
if (mode === "global") {
|
||||
const active = getActiveElement()
|
||||
if (typeof active !== "undefined")
|
||||
return !isSusceptibleToKeyboard(active)
|
||||
}
|
||||
return true
|
||||
}),
|
||||
share()
|
||||
)
|
||||
|
||||
/* Setup search keyboard handlers */
|
||||
/* Set up search keyboard handlers */
|
||||
keyboard$
|
||||
.pipe(
|
||||
filter(({ mode }) => mode === "search"),
|
||||
@ -147,17 +155,10 @@ export function setupKeyboard(): Observable<Keyboard> {
|
||||
}
|
||||
})
|
||||
|
||||
/* Setup global keyboard handlers */
|
||||
/* Set up global keyboard handlers */
|
||||
keyboard$
|
||||
.pipe(
|
||||
filter(({ mode }) => {
|
||||
if (mode === "global") {
|
||||
const active = getActiveElement()
|
||||
if (typeof active !== "undefined")
|
||||
return !isSusceptibleToKeyboard(active)
|
||||
}
|
||||
return false
|
||||
}),
|
||||
filter(({ mode }) => mode === "global"),
|
||||
withLatestFrom(useComponent("search-query"))
|
||||
)
|
||||
.subscribe(([key, query]) => {
|
||||
|
@ -29,7 +29,7 @@ import {
|
||||
import {
|
||||
SearchHighlightFactoryFn,
|
||||
setupSearchHighlighter
|
||||
} from "../highlight"
|
||||
} from "../highlighter"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
@ -70,16 +70,16 @@ export type SearchIndexPipeline = SearchIndexPipelineFn[]
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search index options
|
||||
* Search index
|
||||
*
|
||||
* This interfaces describes the format of the `search_index.json` file which
|
||||
* is automatically built by the MkDocs search plugin.
|
||||
*/
|
||||
export interface SearchIndexOptions {
|
||||
export interface SearchIndex {
|
||||
config: SearchIndexConfig /* Search index configuration */
|
||||
docs: SearchIndexDocument[] /* Search index documents */
|
||||
pipeline?: SearchIndexPipeline /* Search index pipeline */
|
||||
index?: object | string /* Prebuilt or serialized index */
|
||||
pipeline?: SearchIndexPipeline /* Search index pipeline */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
@ -97,12 +97,12 @@ export interface SearchResult {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search index
|
||||
* Search
|
||||
*
|
||||
* Note that `lunr` is injected via Webpack, as it will otherwise also be
|
||||
* bundled in the application bundle.
|
||||
*/
|
||||
export class SearchIndex {
|
||||
export class Search {
|
||||
|
||||
/**
|
||||
* Search document mapping
|
||||
@ -125,11 +125,11 @@ export class SearchIndex {
|
||||
protected index: lunr.Index
|
||||
|
||||
/**
|
||||
* Create a search index
|
||||
* Create the search integration
|
||||
*
|
||||
* @param options - Options
|
||||
* @param data - Search index
|
||||
*/
|
||||
public constructor({ config, docs, pipeline, index }: SearchIndexOptions) {
|
||||
public constructor({ config, docs, pipeline, index }: SearchIndex) {
|
||||
this.documents = setupSearchDocumentMap(docs)
|
||||
this.highlight = setupSearchHighlighter(config)
|
||||
|
||||
@ -150,7 +150,7 @@ export class SearchIndex {
|
||||
this.use((lunr as any).multiLanguage(...config.lang))
|
||||
}
|
||||
|
||||
/* Setup fields and reference */
|
||||
/* Set up fields and reference */
|
||||
this.field("title", { boost: 1000 })
|
||||
this.field("text")
|
||||
this.ref("location")
|
||||
@ -182,16 +182,16 @@ export class SearchIndex {
|
||||
* page. For this reason, section results are grouped within their respective
|
||||
* articles which are the top-level results that are returned.
|
||||
*
|
||||
* @param query - Query string
|
||||
* @param value - Query value
|
||||
*
|
||||
* @return Search results
|
||||
*/
|
||||
public search(query: string): SearchResult[] {
|
||||
if (query) {
|
||||
public query(value: string): SearchResult[] {
|
||||
if (value) {
|
||||
try {
|
||||
|
||||
/* Group sections by containing article */
|
||||
const groups = this.index.search(query)
|
||||
const groups = this.index.search(value)
|
||||
.reduce((results, result) => {
|
||||
const document = this.documents.get(result.ref)
|
||||
if (typeof document !== "undefined") {
|
||||
@ -207,7 +207,7 @@ export class SearchIndex {
|
||||
}, new Map<string, lunr.Index.Result[]>())
|
||||
|
||||
/* Create highlighter for query */
|
||||
const fn = this.highlight(query)
|
||||
const fn = this.highlight(value)
|
||||
|
||||
/* Map groups to search documents */
|
||||
return [...groups].map(([ref, sections]) => ({
|
||||
@ -220,20 +220,11 @@ export class SearchIndex {
|
||||
/* Log errors to console (for now) */
|
||||
} catch (err) {
|
||||
// tslint:disable-next-line no-console
|
||||
console.warn(`Invalid query: ${query} – see https://bit.ly/2s3ChXG`)
|
||||
console.warn(`Invalid query: ${value} – see https://bit.ly/2s3ChXG`)
|
||||
}
|
||||
}
|
||||
|
||||
/* Return nothing in case of error or empty query */
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize search index
|
||||
*
|
||||
* @return String representation
|
||||
*/
|
||||
public toString(): string {
|
||||
return JSON.stringify(this.index)
|
||||
}
|
||||
}
|
||||
|
@ -36,18 +36,18 @@ import { SearchDocument } from "../document"
|
||||
*
|
||||
* @return Highlighted document
|
||||
*/
|
||||
export type SearchHighlightFn =
|
||||
<T extends SearchDocument>(document: Readonly<T>) => T
|
||||
export type SearchHighlightFn = <
|
||||
T extends SearchDocument
|
||||
>(document: Readonly<T>) => T
|
||||
|
||||
/**
|
||||
* Search highlight factory function
|
||||
*
|
||||
* @param query - Query string
|
||||
* @param value - Query value
|
||||
*
|
||||
* @return Search highlight function
|
||||
*/
|
||||
export type SearchHighlightFactoryFn =
|
||||
(query: string) => SearchHighlightFn
|
||||
export type SearchHighlightFactoryFn = (value: string) => SearchHighlightFn
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
@ -69,15 +69,15 @@ export function setupSearchHighlighter(
|
||||
}
|
||||
|
||||
/* Return factory function */
|
||||
return (query: string) => {
|
||||
query = query
|
||||
return (value: string) => {
|
||||
value = value
|
||||
.replace(/[\s*+-:~^]+/g, " ")
|
||||
.trim()
|
||||
|
||||
/* Create search term match expression */
|
||||
const match = new RegExp(`(^|${config.separator})(${
|
||||
query
|
||||
.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&") // TODO: taken from escape-string-regexp
|
||||
value
|
||||
.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&")
|
||||
.replace(separator, "|")
|
||||
})`, "img")
|
||||
|
@ -21,8 +21,7 @@
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export {
|
||||
ArticleDocument,
|
||||
SearchDocument,
|
||||
SectionDocument
|
||||
} from "./document"
|
||||
export * from "./document"
|
||||
export * from "./highlighter"
|
||||
export * from "./transform"
|
||||
export * from "./worker"
|
||||
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 transformation function
|
||||
*
|
||||
* @param value - Query value
|
||||
*
|
||||
* @return Transformed query value
|
||||
*/
|
||||
export type SearchTransformFn = (value: string) => string
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Default transformation function
|
||||
*
|
||||
* Rogue control characters are filtered before handing the query to the
|
||||
* search index, as `lunr` will throw otherwise.
|
||||
*
|
||||
* @param value - Query value
|
||||
*
|
||||
* @return Transformed query value
|
||||
*/
|
||||
export function defaultTransform(value: string): string {
|
||||
return value
|
||||
.replace(/(?:^|\s+)[*+-:^~]+(?=\s+|$)/g, "")
|
||||
.trim()
|
||||
.replace(/\s+|\b$/g, "* ")
|
||||
}
|
@ -20,22 +20,19 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Observable, Subject, asyncScheduler, from } from "rxjs"
|
||||
import { ajax } from "rxjs/ajax"
|
||||
import { identity } from "ramda"
|
||||
import { Observable, Subject, asyncScheduler } from "rxjs"
|
||||
import {
|
||||
map,
|
||||
observeOn,
|
||||
pluck,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
take,
|
||||
withLatestFrom
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { WorkerHandler, watchWorker } from "browser"
|
||||
import { SearchIndexConfig, SearchIndexOptions } from "integrations/search"
|
||||
|
||||
import { translate } from "utilities"
|
||||
|
||||
import { SearchIndex, SearchIndexPipeline } from "../../_"
|
||||
import {
|
||||
SearchMessage,
|
||||
SearchMessageType,
|
||||
@ -51,9 +48,40 @@ import {
|
||||
* Setup options
|
||||
*/
|
||||
interface SetupOptions {
|
||||
base: string /* Base url */
|
||||
index?: Promise<SearchIndexOptions> /* Promise resolving with index */
|
||||
location$: Observable<URL> /* Location observable */
|
||||
index$: Observable<SearchIndex> /* Search index 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 }
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@ -61,44 +89,33 @@ interface SetupOptions {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup search web worker
|
||||
* Set up search web worker
|
||||
*
|
||||
* This function will create a web worker to setup and query the search index
|
||||
* which is done using `lunr`. The index can be passed explicitly in order to
|
||||
* enable hacks like _localsearch_ via search index embedding as JSON. If no
|
||||
* index is given, this function will load it from the default location.
|
||||
* This function will create a web worker to set up and query the search index
|
||||
* which is done using `lunr`. The index must be passed as an observable to
|
||||
* enable hacks like _localsearch_ via search index embedding as JSON.
|
||||
*
|
||||
* @param url - Worker url
|
||||
* @param url - Worker URL
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Worker handler
|
||||
*/
|
||||
export function setupSearchWorker(
|
||||
url: string, { base, index, location$ }: SetupOptions
|
||||
url: string, { index$, base$ }: SetupOptions
|
||||
): WorkerHandler<SearchMessage> {
|
||||
const worker = new Worker(url)
|
||||
|
||||
/* Ensure stable base URL */
|
||||
const origin$ = location$
|
||||
.pipe(
|
||||
take(1),
|
||||
map(({ href }) => new URL(base, href)
|
||||
.toString()
|
||||
.replace(/\/$/, "")
|
||||
)
|
||||
)
|
||||
|
||||
/* Create communication channels and resolve relative links */
|
||||
const tx$ = new Subject<SearchMessage>()
|
||||
const rx$ = watchWorker(worker, { tx$ })
|
||||
.pipe(
|
||||
withLatestFrom(origin$),
|
||||
map(([message, origin]) => {
|
||||
withLatestFrom(base$),
|
||||
map(([message, base]) => {
|
||||
if (isSearchResultMessage(message)) {
|
||||
for (const { article, sections } of message.data) {
|
||||
article.location = `${origin}/${article.location}`
|
||||
article.location = `${base}/${article.location}`
|
||||
for (const section of sections)
|
||||
section.location = `${origin}/${section.location}`
|
||||
section.location = `${base}/${section.location}`
|
||||
}
|
||||
}
|
||||
return message
|
||||
@ -106,57 +123,14 @@ export function setupSearchWorker(
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
/* Fetch index if it wasn't passed explicitly */
|
||||
const index$ = typeof index !== "undefined"
|
||||
? from(index)
|
||||
: origin$
|
||||
.pipe(
|
||||
switchMap(origin => ajax({
|
||||
url: `${origin}/search/search_index.json`,
|
||||
responseType: "json",
|
||||
withCredentials: true
|
||||
})
|
||||
.pipe<SearchIndexOptions>(
|
||||
pluck("response")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
function isConfigDefaultLang(config: SearchIndexConfig) {
|
||||
return config.lang.length === 1 && config.lang[0] === "en"
|
||||
}
|
||||
|
||||
function isConfigDefaultSeparator(config: SearchIndexConfig) {
|
||||
return config.separator === "[\s\-]+"
|
||||
}
|
||||
|
||||
/* Set up search index */
|
||||
index$
|
||||
.pipe(
|
||||
map(({ config, ...rest }) => ({
|
||||
config: {
|
||||
lang: isConfigDefaultLang(config)
|
||||
? [translate("search.config.lang")]
|
||||
: config.lang,
|
||||
separator: isConfigDefaultSeparator(config)
|
||||
? translate("search.config.separator")
|
||||
: config.separator
|
||||
},
|
||||
pipeline: translate("search.config.pipeline")
|
||||
.split(/\s*,\s*/)
|
||||
.filter(Boolean) as any, // Hack
|
||||
...rest
|
||||
}))
|
||||
)
|
||||
// .subscribe(console.log)
|
||||
|
||||
// /* Send index to worker */
|
||||
// index$
|
||||
.pipe(
|
||||
map((data): SearchSetupMessage => ({
|
||||
map<SearchIndex, SearchSetupMessage>(index => ({
|
||||
type: SearchMessageType.SETUP,
|
||||
data
|
||||
data: setupSearchIndex(index)
|
||||
})),
|
||||
observeOn(asyncScheduler) // make sure it runs on the next tick
|
||||
observeOn(asyncScheduler)
|
||||
)
|
||||
.subscribe(tx$.next.bind(tx$))
|
||||
|
@ -22,8 +22,7 @@
|
||||
|
||||
import "expose-loader?lunr!lunr"
|
||||
|
||||
import { SearchIndex, SearchIndexConfig } from "integrations/search"
|
||||
|
||||
import { Search, SearchIndexConfig } from "../../_"
|
||||
import { SearchMessage, SearchMessageType } from "../message"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@ -31,16 +30,16 @@ import { SearchMessage, SearchMessageType } from "../message"
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search index
|
||||
* Search
|
||||
*/
|
||||
let index: SearchIndex
|
||||
let search: Search
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup multi-language support through `lunr-languages`
|
||||
* Set up multi-language support through `lunr-languages`
|
||||
*
|
||||
* This function will automatically import the stemmers necessary to process
|
||||
* the languages which were given through the search index configuration.
|
||||
@ -83,20 +82,19 @@ function setupLunrLanguages(config: SearchIndexConfig): void {
|
||||
export function handler(message: SearchMessage): SearchMessage {
|
||||
switch (message.type) {
|
||||
|
||||
/* Setup search index */
|
||||
/* Search setup message */
|
||||
case SearchMessageType.SETUP:
|
||||
setupLunrLanguages(message.data.config)
|
||||
index = new SearchIndex(message.data)
|
||||
search = new Search(message.data)
|
||||
return {
|
||||
type: SearchMessageType.DUMP,
|
||||
data: index.toString()
|
||||
type: SearchMessageType.READY
|
||||
}
|
||||
|
||||
/* Query search index */
|
||||
/* Search query message */
|
||||
case SearchMessageType.QUERY:
|
||||
return {
|
||||
type: SearchMessageType.RESULT,
|
||||
data: index ? index.search(message.data) : []
|
||||
data: search ? search.query(message.data) : []
|
||||
}
|
||||
|
||||
/* All other messages */
|
@ -20,7 +20,7 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { SearchIndexOptions, SearchResult } from "integrations/search"
|
||||
import { SearchIndex, SearchResult } from "../../_"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
@ -31,7 +31,7 @@ import { SearchIndexOptions, SearchResult } from "integrations/search"
|
||||
*/
|
||||
export const enum SearchMessageType {
|
||||
SETUP, /* Search index setup */
|
||||
DUMP, /* Search index dump */
|
||||
READY, /* Search index ready */
|
||||
QUERY, /* Search query */
|
||||
RESULT /* Search results */
|
||||
}
|
||||
@ -43,15 +43,14 @@ export const enum SearchMessageType {
|
||||
*/
|
||||
export interface SearchSetupMessage {
|
||||
type: SearchMessageType.SETUP /* Message type */
|
||||
data: SearchIndexOptions /* Message data */
|
||||
data: SearchIndex /* Message data */
|
||||
}
|
||||
|
||||
/**
|
||||
* A message containing the a dump of the search index
|
||||
* A message indicating the search index is ready
|
||||
*/
|
||||
export interface SearchDumpMessage {
|
||||
type: SearchMessageType.DUMP /* Message type */
|
||||
data: string /* Message data */
|
||||
export interface SearchReadyMessage {
|
||||
type: SearchMessageType.READY /* Message type */
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,7 +76,7 @@ export interface SearchResultMessage {
|
||||
*/
|
||||
export type SearchMessage =
|
||||
| SearchSetupMessage
|
||||
| SearchDumpMessage
|
||||
| SearchReadyMessage
|
||||
| SearchQueryMessage
|
||||
| SearchResultMessage
|
||||
|
||||
@ -99,16 +98,16 @@ export function isSearchSetupMessage(
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for search dump messages
|
||||
* Type guard for search ready messages
|
||||
*
|
||||
* @param message - Search worker message
|
||||
*
|
||||
* @return Test result
|
||||
*/
|
||||
export function isSearchDumpMessage(
|
||||
export function isSearchReadyMessage(
|
||||
message: SearchMessage
|
||||
): message is SearchDumpMessage {
|
||||
return message.type === SearchMessageType.DUMP
|
||||
): message is SearchReadyMessage {
|
||||
return message.type === SearchMessageType.READY
|
||||
}
|
||||
|
||||
/**
|
@ -20,7 +20,7 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { SearchIndexConfig, SearchIndexOptions } from "integrations/search"
|
||||
import { SearchIndex, SearchTransformFn } from "integrations"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
@ -35,31 +35,17 @@ export type Feature =
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* URL configuration
|
||||
*/
|
||||
export interface UrlConfig {
|
||||
base: string /* Base URL */
|
||||
worker: {
|
||||
search: string /* Search worker URL */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search configuration
|
||||
*/
|
||||
export interface SearchConfig {
|
||||
index?: Promise<SearchIndexOptions>
|
||||
query?: (value: string) => string
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration
|
||||
*/
|
||||
export interface Config {
|
||||
url: UrlConfig
|
||||
base: string /* Base URL */
|
||||
features: Feature[] /* Feature flags */
|
||||
search?: SearchConfig
|
||||
search: {
|
||||
worker: string /* Worker URL */
|
||||
index?: Promise<SearchIndex> /* Promise resolving with index */
|
||||
transform?: SearchTransformFn /* Transformation function */
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@ -78,8 +64,7 @@ export interface Config {
|
||||
*/
|
||||
export function isConfig(config: any): config is Config {
|
||||
return typeof config === "object"
|
||||
&& typeof config.url === "object"
|
||||
&& typeof config.url.base === "string"
|
||||
&& typeof config.url.worker === "object"
|
||||
&& typeof config.url.worker.search === "string"
|
||||
&& typeof config.base === "string"
|
||||
&& typeof config.features === "object"
|
||||
&& typeof config.search === "object"
|
||||
}
|
||||
|
@ -337,13 +337,6 @@
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- Application configuration -->
|
||||
{% block config %}
|
||||
<script>
|
||||
var __config = {}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
<!-- Theme-related JavaScript -->
|
||||
{% block scripts %}
|
||||
<script src="{{ 'assets/javascripts/vendor.js' | url }}"></script>
|
||||
@ -365,20 +358,21 @@
|
||||
{%- set _ = translations.update({ key: lang.t(key) }) -%}
|
||||
{%- endfor -%}
|
||||
<script id="__lang" type="application/json">
|
||||
{{ translations | tojson }}
|
||||
{{- translations | tojson -}}
|
||||
</script>
|
||||
|
||||
<!-- Application configuration -->
|
||||
{% block config %}{% endblock %}
|
||||
|
||||
<!-- Application initialization -->
|
||||
<script>
|
||||
__material = initialize(Object.assign({
|
||||
url: {
|
||||
base: "{{ base_url }}",
|
||||
worker: {
|
||||
search: "{{ 'assets/javascripts/worker/search.js' | url }}"
|
||||
}
|
||||
},
|
||||
features: {{ config.theme.features | tojson }}
|
||||
}, typeof __config !== "undefined" ? __config : {}))
|
||||
app = initialize({
|
||||
base: "{{ base_url }}",
|
||||
features: {{ config.theme.features | tojson }},
|
||||
search: Object.assign({
|
||||
worker: "{{ 'assets/javascripts/worker/search.js' | url }}"
|
||||
}, typeof search !== "undefined" && search)
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- Custom JavaScript -->
|
||||
|
@ -30,7 +30,7 @@
|
||||
}
|
||||
ga.l = +new Date
|
||||
|
||||
/* Setup integration and send page view */
|
||||
/* Set up integration and send page view */
|
||||
ga("create", "{{ analytics[0] }}", "{{ analytics[1] }}")
|
||||
ga("set", "anonymizeIp", true)
|
||||
ga("send", "pageview")
|
||||
|
@ -350,7 +350,7 @@ export default (_env: never, args: Configuration): Configuration[] => {
|
||||
...base,
|
||||
entry: {
|
||||
"assets/javascripts/worker/search":
|
||||
"src/assets/javascripts/workers/search/main"
|
||||
"src/assets/javascripts/integrations/search/worker/main"
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, "material"),
|
||||
|
Loading…
Reference in New Issue
Block a user