Merge branch 'refactor/rxjs-typescript' into feature/landing-page

This commit is contained in:
squidfunk 2020-03-27 17:17:39 +01:00
commit 77e60a9bb8
50 changed files with 518 additions and 437 deletions

View File

@ -387,7 +387,7 @@ from the template.
``` ```
[12]: http://www.materialui.co/colors [12]: http://www.materialui.co/colors
[13]: customization.md/#additional-stylesheets [13]: customization.md#additional-stylesheets
#### Primary color #### Primary color

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,10 @@
{ {
"assets/javascripts/bundle.js": "assets/javascripts/bundle.cad4abbb.min.js", "assets/javascripts/bundle.js": "assets/javascripts/bundle.aa7a7592.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.cad4abbb.min.js.map", "assets/javascripts/bundle.js.map": "assets/javascripts/bundle.aa7a7592.min.js.map",
"assets/javascripts/vendor.js": "assets/javascripts/vendor.0c35f0aa.min.js", "assets/javascripts/vendor.js": "assets/javascripts/vendor.3340e0de.min.js",
"assets/javascripts/vendor.js.map": "assets/javascripts/vendor.0c35f0aa.min.js.map", "assets/javascripts/vendor.js.map": "assets/javascripts/vendor.3340e0de.min.js.map",
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.2613054f.min.js", "assets/javascripts/worker/search.js": "assets/javascripts/worker/search.3bc815f0.min.js",
"assets/javascripts/worker/search.js.map": "assets/javascripts/worker/search.2613054f.min.js.map", "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/main.css": "assets/stylesheets/main.b32d3181.min.css",
"assets/stylesheets/palette.css": "assets/stylesheets/palette.4444686e.min.css" "assets/stylesheets/palette.css": "assets/stylesheets/palette.4444686e.min.css"
} }

View File

@ -173,12 +173,9 @@
{% include "partials/footer.html" %} {% include "partials/footer.html" %}
{% endblock %} {% endblock %}
</div> </div>
{% block config %}
<script>var __config={}</script>
{% endblock %}
{% block scripts %} {% block scripts %}
<script src="{{ 'assets/javascripts/vendor.0c35f0aa.min.js' | url }}"></script> <script src="{{ 'assets/javascripts/vendor.3340e0de.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.cad4abbb.min.js' | url }}"></script> <script src="{{ 'assets/javascripts/bundle.aa7a7592.min.js' | url }}"></script>
{%- set translations = {} -%} {%- set translations = {} -%}
{%- for key in [ {%- for key in [
"clipboard.copy", "clipboard.copy",
@ -194,18 +191,17 @@
{%- set _ = translations.update({ key: lang.t(key) }) -%} {%- set _ = translations.update({ key: lang.t(key) }) -%}
{%- endfor -%} {%- endfor -%}
<script id="__lang" type="application/json"> <script id="__lang" type="application/json">
{{ translations | tojson }} {{- translations | tojson -}}
</script> </script>
{% block config %}{% endblock %}
<script> <script>
__material = initialize(Object.assign({ app = initialize({
url: { base: "{{ base_url }}",
base: "{{ base_url }}", features: {{ config.theme.features | tojson }},
worker: { search: Object.assign({
search: "{{ 'assets/javascripts/worker/search.2613054f.min.js' | url }}" worker: "{{ 'assets/javascripts/worker/search.3bc815f0.min.js' | url }}"
} }, typeof search !== "undefined" && search)
}, })
features: {{ config.theme.features | tojson }}
}, typeof __config !== "undefined" ? __config : {}))
</script> </script>
{% for path in config["extra_javascript"] %} {% for path in config["extra_javascript"] %}
<script src="{{ path | url }}"></script> <script src="{{ path | url }}"></script>

View File

@ -51,7 +51,7 @@ interface WatchOptions {
/** /**
* Watch document switch * 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 * 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 * to the same page, the request is effectively ignored (i.e. when only the
* fragment identifier changes). * fragment identifier changes).

View File

@ -87,3 +87,17 @@ export function getElements<T extends HTMLElement>(
): T[] { ): T[] {
return Array.from(node.querySelectorAll<T>(selector)) 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)
}

View File

@ -21,7 +21,7 @@
*/ */
import { Observable, fromEvent, merge } from "rxjs" import { Observable, fromEvent, merge } from "rxjs"
import { mapTo, shareReplay, startWith } from "rxjs/operators" import { map, shareReplay, startWith } from "rxjs/operators"
import { getActiveElement } from "../_" import { getActiveElement } from "../_"
@ -56,15 +56,12 @@ el: HTMLElement, value: boolean = true
export function watchElementFocus( export function watchElementFocus(
el: HTMLElement el: HTMLElement
): Observable<boolean> { ): Observable<boolean> {
const focus$ = fromEvent(el, "focus")
const blur$ = fromEvent(el, "blur")
/* Map events to boolean state */
return merge( return merge(
focus$.pipe(mapTo(true)), fromEvent<FocusEvent>(el, "focus"),
blur$.pipe(mapTo(false)) fromEvent<FocusEvent>(el, "blur")
) )
.pipe( .pipe(
map(({ type }) => type === "focus"),
startWith(el === getActiveElement()), startWith(el === getActiveElement()),
shareReplay(1) shareReplay(1)
) )

View File

@ -66,8 +66,8 @@ export function watchElementOffset(
el: HTMLElement el: HTMLElement
): Observable<ElementOffset> { ): Observable<ElementOffset> {
return merge( return merge(
fromEvent<UIEvent>(el, "scroll"), fromEvent(el, "scroll"),
fromEvent<UIEvent>(window, "resize") fromEvent(window, "resize")
) )
.pipe( .pipe(
map(() => getElementOffset(el)), map(() => getElementOffset(el)),

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { BehaviorSubject } from "rxjs" import { BehaviorSubject, Subject } from "rxjs"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * 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 url - URL or HTML anchor element
* @param ref - Reference URL * @param ref - Reference URL
* *
* @return Test result * @return Test result
*/ */
export function isLocationInternal( export function isLocalLocation(
url: URL | HTMLAnchorElement, url: URL | HTMLAnchorElement,
ref: URL | Location = location ref: URL | Location = location
): boolean { ): boolean {
@ -75,7 +75,7 @@ export function isLocationInternal(
* *
* @return Test result * @return Test result
*/ */
export function isLocationAnchor( export function isAnchorLocation(
url: URL | HTMLAnchorElement, url: URL | HTMLAnchorElement,
ref: URL | Location = location ref: URL | Location = location
): boolean { ): boolean {
@ -90,6 +90,6 @@ export function isLocationAnchor(
* *
* @return Location subject * @return Location subject
*/ */
export function watchLocation(): BehaviorSubject<URL> { export function watchLocation(): Subject<URL> {
return new BehaviorSubject<URL>(getLocation()) return new BehaviorSubject<URL>(getLocation())
} }

View 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)
)
}

View File

@ -40,9 +40,9 @@ export function getLocationHash(): string {
* Set location hash * Set location hash
* *
* Setting a new fragment identifier via `location.hash` will have no effect * 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, * if the value doesn't change. When a new fragment identifier is set, we want
* we want the browser to target the respective element at all times, which is * the browser to target the respective element at all times, which is why we
* why we use this dirty little trick. * use this dirty little trick.
* *
* @param hash - Location hash * @param hash - Location hash
*/ */

View File

@ -21,4 +21,5 @@
*/ */
export * from "./_" export * from "./_"
export * from "./base"
export * from "./hash" export * from "./hash"

View File

@ -74,8 +74,8 @@ export function setViewportOffset(
*/ */
export function watchViewportOffset(): Observable<ViewportOffset> { export function watchViewportOffset(): Observable<ViewportOffset> {
return merge( return merge(
fromEvent<UIEvent>(window, "scroll", { passive: true }), fromEvent(window, "scroll", { passive: true }),
fromEvent<UIEvent>(window, "resize", { passive: true }) fromEvent(window, "resize", { passive: true })
) )
.pipe( .pipe(
map(getViewportOffset), map(getViewportOffset),

View File

@ -59,7 +59,7 @@ export function getViewportSize(): ViewportSize {
* @return Viewport size observable * @return Viewport size observable
*/ */
export function watchViewportSize(): Observable<ViewportSize> { export function watchViewportSize(): Observable<ViewportSize> {
return fromEvent<UIEvent>(window, "resize") return fromEvent(window, "resize")
.pipe( .pipe(
map(getViewportSize), map(getViewportSize),
startWith(getViewportSize()) startWith(getViewportSize())

View File

@ -38,7 +38,7 @@ import {
*/ */
export interface WorkerMessage { export interface WorkerMessage {
type: unknown /* Message type */ type: unknown /* Message type */
data: unknown /* Message data */ data?: unknown /* Message data */
} }
/** /**

View File

@ -29,7 +29,7 @@ import {
switchMap switchMap
} from "rxjs/operators" } from "rxjs/operators"
import { getElement } from "browser" import { getElement, replaceElement } from "browser"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * 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 * This function will maintain bindings to the elements identified by the given
* names in-between document switches and update the elements in-place. * names in-between document switches and update the elements in-place.
@ -118,7 +118,7 @@ export function setupComponents(
case "container": case "container":
case "skip": case "skip":
if (name in prev && typeof prev[name] !== "undefined") { if (name in prev && typeof prev[name] !== "undefined") {
prev[name]!.replaceWith(next[name]!) replaceElement(prev[name]!, next[name]!)
prev[name] = next[name] prev[name] = next[name]
} }
break break
@ -149,7 +149,7 @@ export function setupComponents(
* *
* @param name - Component name * @param name - Component name
* *
* @return Element observable * @return Component observable
*/ */
export function useComponent<T extends HTMLInputElement>( export function useComponent<T extends HTMLInputElement>(
name: "search-query" name: "search-query"

View File

@ -99,7 +99,7 @@ export function watchMain(
})) }))
) )
), ),
distinctUntilKeyChanged("top"), distinctUntilKeyChanged("bottom"),
shareReplay(1) shareReplay(1)
) )

View File

@ -31,8 +31,9 @@ import { WorkerHandler, setToggle } from "browser"
import { import {
SearchMessage, SearchMessage,
SearchMessageType, SearchMessageType,
SearchQueryMessage SearchQueryMessage,
} from "workers" SearchTransformFn
} from "integrations"
import { watchSearchQuery } from "../react" import { watchSearchQuery } from "../react"
@ -48,6 +49,13 @@ export interface SearchQuery {
focus: boolean /* Query focus */ focus: boolean /* Query focus */
} }
/* ------------------------------------------------------------------------- */
/**
* Search query transform
*/
export type SearchQueryTransform = (value: string) => string
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Helper types * Helper types
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
@ -56,7 +64,7 @@ export interface SearchQuery {
* Mount options * Mount options
*/ */
interface MountOptions { interface MountOptions {
transform?(value: string): string /* Transformation function */ transform?: SearchTransformFn /* Transformation function */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------

View File

@ -29,6 +29,7 @@ import {
} from "rxjs/operators" } from "rxjs/operators"
import { watchElementFocus } from "browser" import { watchElementFocus } from "browser"
import { SearchTransformFn, defaultTransform } from "integrations"
import { SearchQuery } from "../_" import { SearchQuery } from "../_"
@ -40,28 +41,7 @@ import { SearchQuery } from "../_"
* Watch options * Watch options
*/ */
interface WatchOptions { interface WatchOptions {
transform?(value: string): string /* Transformation function */ transform?: SearchTransformFn /* 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, "* ")
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------

View File

@ -31,11 +31,11 @@ import {
} from "rxjs/operators" } from "rxjs/operators"
import { WorkerHandler, watchElementOffset } from "browser" import { WorkerHandler, watchElementOffset } from "browser"
import { SearchResult } from "integrations/search"
import { import {
SearchMessage, SearchMessage,
SearchResult,
isSearchResultMessage isSearchResultMessage
} from "workers" } from "integrations"
import { SearchQuery } from "../../query" import { SearchQuery } from "../../query"
import { applySearchResult } from "../react" import { applySearchResult } from "../react"

View File

@ -73,7 +73,7 @@ export function resetSearchResultMeta(
* @param child - Search result element * @param child - Search result element
*/ */
export function addToSearchResultList( export function addToSearchResultList(
el: HTMLElement, child: HTMLElement el: HTMLElement, child: Element
): void { ): void {
el.appendChild(child) el.appendChild(child)
} }

View File

@ -33,8 +33,10 @@ import {
animationFrameScheduler, animationFrameScheduler,
fromEvent, fromEvent,
of, of,
NEVER NEVER,
from
} from "rxjs" } from "rxjs"
import { ajax } from "rxjs/ajax"
import { import {
delay, delay,
switchMap, switchMap,
@ -44,7 +46,8 @@ import {
observeOn, observeOn,
take, take,
shareReplay, shareReplay,
share share,
pluck
} from "rxjs/operators" } from "rxjs/operators"
import { import {
@ -56,12 +59,11 @@ import {
watchLocation, watchLocation,
watchLocationHash, watchLocationHash,
watchViewport, watchViewport,
isLocationInternal, isLocalLocation,
isLocationAnchor, isAnchorLocation,
setLocationHash setLocationHash,
} from "./browser" watchLocationBase
import { setupSearchWorker } from "./workers" } from "browser"
import { import {
mountHeader, mountHeader,
mountHero, mountHero,
@ -76,10 +78,14 @@ import {
mountSearchReset, mountSearchReset,
mountSearchResult mountSearchResult
} from "components" } from "components"
import { setupClipboard } from "./integrations/clipboard" import {
import { setupDialog } from "integrations/dialog" setupClipboard,
import { setupKeyboard } from "./integrations/keyboard" setupDialog,
import { setupInstantLoading } from "integrations/instant" setupKeyboard,
setupInstantLoading,
setupSearchWorker,
SearchIndex
} from "integrations"
import { import {
patchTables, patchTables,
patchDetails, patchDetails,
@ -91,6 +97,7 @@ import { isConfig } from "utilities"
/* ------------------------------------------------------------------------- */ /* ------------------------------------------------------------------------- */
/* Denote that JavaScript is available */
document.documentElement.classList.remove("no-js") document.documentElement.classList.remove("no-js")
document.documentElement.classList.add("js") document.documentElement.classList.add("js")
@ -139,19 +146,22 @@ export function initialize(config: unknown) {
if (!isConfig(config)) if (!isConfig(config))
throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`) throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`)
/* Setup user interface observables */ /* Set up user interface observables */
const location$ = watchLocation() const location$ = watchLocation()
const base$ = watchLocationBase(config.base, { location$ })
const hash$ = watchLocationHash() const hash$ = watchLocationHash()
const viewport$ = watchViewport() const viewport$ = watchViewport()
const tablet$ = watchMedia("(min-width: 960px)") const tablet$ = watchMedia("(min-width: 960px)")
const screen$ = watchMedia("(min-width: 1220px)") const screen$ = watchMedia("(min-width: 1220px)")
/* Setup document observable */ /* Set up document observable */
const document$ = config.features.includes("instant") const document$ = config.features.includes("instant")
? watchDocument({ location$ }) ? watchDocument({ location$ })
: watchDocument() : watchDocument()
/* Setup component bindings */ /* ----------------------------------------------------------------------- */
/* Set up component bindings */
setupComponents([ setupComponents([
"container", /* Container */ "container", /* Container */
"header", /* Header */ "header", /* Header */
@ -168,17 +178,19 @@ export function initialize(config: unknown) {
"toc" /* Table of contents */ "toc" /* Table of contents */
], { document$ }) ], { document$ })
/* ----------------------------------------------------------------------- */ const keyboard$ = setupKeyboard()
// External index patchDetails({ document$, hash$ })
const index = config.search && config.search.index patchScripts({ document$ })
? config.search.index patchSource({ document$ })
: undefined patchTables({ document$ })
// TODO: pass URL config as first parameter, options as second /* Force 1px scroll offset to trigger overflow scrolling */
const worker = setupSearchWorker(config.url.worker.search, { patchScrollfix({ document$ })
base: config.url.base, index, location$
}) /* 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") const navigation$ = useComponent("navigation")
.pipe( .pipe(
mountNavigation({ header$, main$, viewport$, screen$ }), 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$ }) /* Fetch index if it wasn't passed explicitly */
patchScripts({ document$ }) const index$ = typeof index !== "undefined"
patchSource({ document$ }) ? from(index)
patchTables({ document$ }) : 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 worker = setupSearchWorker(config.search.worker, {
const dialog$ = setupDialog() base$, index$
const clipboard$ = setupClipboard({ document$, dialog$ }) })
/* ----------------------------------------------------------------------- */
/* 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 => { switchMap(ev => {
if (ev.target instanceof HTMLElement) { if (ev.target instanceof HTMLElement) {
const el = ev.target.closest("a") // TODO: abstract as link click? const el = ev.target.closest("a") // TODO: abstract as link click?
if (el && isLocationInternal(el)) { if (el && isLocalLocation(el)) {
if (!isLocationAnchor(el) && config.features.includes("instant")) if (!isAnchorLocation(el) && config.features.includes("instant"))
ev.preventDefault() ev.preventDefault()
return of(el) return of(el)
} }
@ -326,8 +350,6 @@ export function initialize(config: unknown) {
setToggle("drawer", false) setToggle("drawer", false)
}) })
// somehow call this setupNavigation ?
// instant loading // instant loading
if (config.features.includes("instant")) { if (config.features.includes("instant")) {

View File

@ -45,7 +45,7 @@ interface SetupOptions {
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Setup clipboard * Set up clipboard
* *
* This function implements the Clipboard.js integration and injects a button * This function implements the Clipboard.js integration and injects a button
* into all code blocks when the document changes. * 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 => { const clipboard$ = fromEventPattern<ClipboardJS.Event>(next => {
new ClipboardJS(".md-clipboard").on("success", next) new ClipboardJS(".md-clipboard").on("success", next)
}) })

View File

@ -45,7 +45,7 @@ interface SetupOptions {
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Setup dialog * Set up dialog
* *
* @param options - Options * @param options - Options
* *

View File

@ -20,4 +20,8 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
export * from "./clipboard"
export * from "./dialog"
export * from "./instant"
export * from "./keyboard"
export * from "./search" export * from "./search"

View File

@ -38,7 +38,7 @@ import {
Viewport, Viewport,
ViewportOffset, ViewportOffset,
getElement, getElement,
isLocationAnchor, isAnchorLocation,
setLocationHash, setLocationHash,
setViewportOffset setViewportOffset
} from "browser" } from "browser"
@ -72,7 +72,7 @@ interface SetupOptions {
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Setup instant loading * Set up instant loading
* *
* @param options - Options * @param options - Options
* *
@ -91,7 +91,7 @@ export function setupInstantLoading(
const push$ = state$ const push$ = state$
.pipe( .pipe(
distinctUntilChanged((prev, next) => prev.url.href === next.url.href), distinctUntilChanged((prev, next) => prev.url.href === next.url.href),
filter(({ url }) => !isLocationAnchor(url)), filter(({ url }) => !isAnchorLocation(url)),
share() share()
) )
@ -129,7 +129,7 @@ export function setupInstantLoading(
bufferCount(2, 1), bufferCount(2, 1),
filter(([prev, next]) => { filter(([prev, next]) => {
return prev.url.pathname === next.url.pathname return prev.url.pathname === next.url.pathname
&& !isLocationAnchor(next.url) && !isAnchorLocation(next.url)
}), }),
map(([, state]) => state) map(([, state]) => state)
) )

View File

@ -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: * correctly propagated. Currently there are two modes:
* *
* - `global`: This mode is active when the search is closed. It is intended * - `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", mode: getToggle("search") ? "search" : "global",
...key ...key
})), })),
filter(({ mode }) => {
if (mode === "global") {
const active = getActiveElement()
if (typeof active !== "undefined")
return !isSusceptibleToKeyboard(active)
}
return true
}),
share() share()
) )
/* Setup search keyboard handlers */ /* Set up search keyboard handlers */
keyboard$ keyboard$
.pipe( .pipe(
filter(({ mode }) => mode === "search"), filter(({ mode }) => mode === "search"),
@ -147,17 +155,10 @@ export function setupKeyboard(): Observable<Keyboard> {
} }
}) })
/* Setup global keyboard handlers */ /* Set up global keyboard handlers */
keyboard$ keyboard$
.pipe( .pipe(
filter(({ mode }) => { filter(({ mode }) => mode === "global"),
if (mode === "global") {
const active = getActiveElement()
if (typeof active !== "undefined")
return !isSusceptibleToKeyboard(active)
}
return false
}),
withLatestFrom(useComponent("search-query")) withLatestFrom(useComponent("search-query"))
) )
.subscribe(([key, query]) => { .subscribe(([key, query]) => {

View File

@ -29,7 +29,7 @@ import {
import { import {
SearchHighlightFactoryFn, SearchHighlightFactoryFn,
setupSearchHighlighter setupSearchHighlighter
} from "../highlight" } from "../highlighter"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * 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 * This interfaces describes the format of the `search_index.json` file which
* is automatically built by the MkDocs search plugin. * is automatically built by the MkDocs search plugin.
*/ */
export interface SearchIndexOptions { export interface SearchIndex {
config: SearchIndexConfig /* Search index configuration */ config: SearchIndexConfig /* Search index configuration */
docs: SearchIndexDocument[] /* Search index documents */ docs: SearchIndexDocument[] /* Search index documents */
pipeline?: SearchIndexPipeline /* Search index pipeline */
index?: object | string /* Prebuilt or serialized index */ 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 * Note that `lunr` is injected via Webpack, as it will otherwise also be
* bundled in the application bundle. * bundled in the application bundle.
*/ */
export class SearchIndex { export class Search {
/** /**
* Search document mapping * Search document mapping
@ -125,11 +125,11 @@ export class SearchIndex {
protected index: lunr.Index 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.documents = setupSearchDocumentMap(docs)
this.highlight = setupSearchHighlighter(config) this.highlight = setupSearchHighlighter(config)
@ -150,7 +150,7 @@ export class SearchIndex {
this.use((lunr as any).multiLanguage(...config.lang)) this.use((lunr as any).multiLanguage(...config.lang))
} }
/* Setup fields and reference */ /* Set up fields and reference */
this.field("title", { boost: 1000 }) this.field("title", { boost: 1000 })
this.field("text") this.field("text")
this.ref("location") this.ref("location")
@ -182,16 +182,16 @@ export class SearchIndex {
* 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 query - Query string * @param value - Query value
* *
* @return Search results * @return Search results
*/ */
public search(query: string): SearchResult[] { public query(value: string): SearchResult[] {
if (query) { if (value) {
try { try {
/* Group sections by containing article */ /* Group sections by containing article */
const groups = this.index.search(query) const groups = this.index.search(value)
.reduce((results, result) => { .reduce((results, result) => {
const document = this.documents.get(result.ref) const document = this.documents.get(result.ref)
if (typeof document !== "undefined") { if (typeof document !== "undefined") {
@ -207,7 +207,7 @@ export class SearchIndex {
}, new Map<string, lunr.Index.Result[]>()) }, new Map<string, lunr.Index.Result[]>())
/* Create highlighter for query */ /* Create highlighter for query */
const fn = this.highlight(query) const fn = this.highlight(value)
/* Map groups to search documents */ /* Map groups to search documents */
return [...groups].map(([ref, sections]) => ({ return [...groups].map(([ref, sections]) => ({
@ -220,20 +220,11 @@ export class SearchIndex {
/* Log errors to console (for now) */ /* Log errors to console (for now) */
} catch (err) { } catch (err) {
// tslint:disable-next-line no-console // 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 nothing in case of error or empty query */
return [] return []
} }
/**
* Serialize search index
*
* @return String representation
*/
public toString(): string {
return JSON.stringify(this.index)
}
} }

View File

@ -36,18 +36,18 @@ import { SearchDocument } from "../document"
* *
* @return Highlighted document * @return Highlighted document
*/ */
export type SearchHighlightFn = export type SearchHighlightFn = <
<T extends SearchDocument>(document: Readonly<T>) => T T extends SearchDocument
>(document: Readonly<T>) => T
/** /**
* Search highlight factory function * Search highlight factory function
* *
* @param query - Query string * @param value - Query value
* *
* @return Search highlight function * @return Search highlight function
*/ */
export type SearchHighlightFactoryFn = export type SearchHighlightFactoryFn = (value: string) => SearchHighlightFn
(query: string) => SearchHighlightFn
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * Functions
@ -69,15 +69,15 @@ export function setupSearchHighlighter(
} }
/* Return factory function */ /* Return factory function */
return (query: string) => { return (value: string) => {
query = query value = value
.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})(${
query value
.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&") // TODO: taken from escape-string-regexp .replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&")
.replace(separator, "|") .replace(separator, "|")
})`, "img") })`, "img")

View File

@ -21,8 +21,7 @@
*/ */
export * from "./_" export * from "./_"
export { export * from "./document"
ArticleDocument, export * from "./highlighter"
SearchDocument, export * from "./transform"
SectionDocument export * from "./worker"
} from "./document"

View File

@ -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, "* ")
}

View File

@ -20,22 +20,19 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { Observable, Subject, asyncScheduler, from } from "rxjs" import { identity } from "ramda"
import { ajax } from "rxjs/ajax" import { Observable, Subject, asyncScheduler } from "rxjs"
import { import {
map, map,
observeOn, observeOn,
pluck,
shareReplay, shareReplay,
switchMap,
take,
withLatestFrom withLatestFrom
} from "rxjs/operators" } from "rxjs/operators"
import { WorkerHandler, watchWorker } from "browser" import { WorkerHandler, watchWorker } from "browser"
import { SearchIndexConfig, SearchIndexOptions } from "integrations/search"
import { translate } from "utilities" import { translate } from "utilities"
import { SearchIndex, SearchIndexPipeline } from "../../_"
import { import {
SearchMessage, SearchMessage,
SearchMessageType, SearchMessageType,
@ -51,9 +48,40 @@ import {
* Setup options * Setup options
*/ */
interface SetupOptions { interface SetupOptions {
base: string /* Base url */ index$: Observable<SearchIndex> /* Search index observable */
index?: Promise<SearchIndexOptions> /* Promise resolving with index */ base$: Observable<string> /* Location base observable */
location$: Observable<URL> /* Location 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 * This function will create a web worker to set up and query the search index
* which is done using `lunr`. The index can be passed explicitly in order to * which is done using `lunr`. The index must be passed as an observable to
* enable hacks like _localsearch_ via search index embedding as JSON. If no * enable hacks like _localsearch_ via search index embedding as JSON.
* index is given, this function will load it from the default location.
* *
* @param url - Worker url * @param url - Worker URL
* @param options - Options * @param options - Options
* *
* @return Worker handler * @return Worker handler
*/ */
export function setupSearchWorker( export function setupSearchWorker(
url: string, { base, index, location$ }: SetupOptions url: string, { index$, base$ }: SetupOptions
): WorkerHandler<SearchMessage> { ): WorkerHandler<SearchMessage> {
const worker = new Worker(url) 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 */ /* Create communication channels and resolve relative links */
const tx$ = new Subject<SearchMessage>() const tx$ = new Subject<SearchMessage>()
const rx$ = watchWorker(worker, { tx$ }) const rx$ = watchWorker(worker, { tx$ })
.pipe( .pipe(
withLatestFrom(origin$), withLatestFrom(base$),
map(([message, origin]) => { map(([message, base]) => {
if (isSearchResultMessage(message)) { if (isSearchResultMessage(message)) {
for (const { article, sections } of message.data) { for (const { article, sections } of message.data) {
article.location = `${origin}/${article.location}` article.location = `${base}/${article.location}`
for (const section of sections) for (const section of sections)
section.location = `${origin}/${section.location}` section.location = `${base}/${section.location}`
} }
} }
return message return message
@ -106,57 +123,14 @@ export function setupSearchWorker(
shareReplay(1) shareReplay(1)
) )
/* Fetch index if it wasn't passed explicitly */ /* Set up search index */
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\-]+"
}
index$ index$
.pipe( .pipe(
map(({ config, ...rest }) => ({ map<SearchIndex, SearchSetupMessage>(index => ({
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 => ({
type: SearchMessageType.SETUP, type: SearchMessageType.SETUP,
data data: setupSearchIndex(index)
})), })),
observeOn(asyncScheduler) // make sure it runs on the next tick observeOn(asyncScheduler)
) )
.subscribe(tx$.next.bind(tx$)) .subscribe(tx$.next.bind(tx$))

View File

@ -22,8 +22,7 @@
import "expose-loader?lunr!lunr" import "expose-loader?lunr!lunr"
import { SearchIndex, SearchIndexConfig } from "integrations/search" import { Search, SearchIndexConfig } from "../../_"
import { SearchMessage, SearchMessageType } from "../message" 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 * 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 * 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.
@ -83,20 +82,19 @@ function setupLunrLanguages(config: SearchIndexConfig): void {
export function handler(message: SearchMessage): SearchMessage { export function handler(message: SearchMessage): SearchMessage {
switch (message.type) { switch (message.type) {
/* Setup search index */ /* Search setup message */
case SearchMessageType.SETUP: case SearchMessageType.SETUP:
setupLunrLanguages(message.data.config) setupLunrLanguages(message.data.config)
index = new SearchIndex(message.data) search = new Search(message.data)
return { return {
type: SearchMessageType.DUMP, type: SearchMessageType.READY
data: index.toString()
} }
/* Query search index */ /* Search query message */
case SearchMessageType.QUERY: case SearchMessageType.QUERY:
return { return {
type: SearchMessageType.RESULT, type: SearchMessageType.RESULT,
data: index ? index.search(message.data) : [] data: search ? search.query(message.data) : []
} }
/* All other messages */ /* All other messages */

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { SearchIndexOptions, SearchResult } from "integrations/search" import { SearchIndex, SearchResult } from "../../_"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
@ -31,7 +31,7 @@ import { SearchIndexOptions, SearchResult } from "integrations/search"
*/ */
export const enum SearchMessageType { export const enum SearchMessageType {
SETUP, /* Search index setup */ SETUP, /* Search index setup */
DUMP, /* Search index dump */ READY, /* Search index ready */
QUERY, /* Search query */ QUERY, /* Search query */
RESULT /* Search results */ RESULT /* Search results */
} }
@ -43,15 +43,14 @@ export const enum SearchMessageType {
*/ */
export interface SearchSetupMessage { export interface SearchSetupMessage {
type: SearchMessageType.SETUP /* Message type */ 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 { export interface SearchReadyMessage {
type: SearchMessageType.DUMP /* Message type */ type: SearchMessageType.READY /* Message type */
data: string /* Message data */
} }
/** /**
@ -77,7 +76,7 @@ export interface SearchResultMessage {
*/ */
export type SearchMessage = export type SearchMessage =
| SearchSetupMessage | SearchSetupMessage
| SearchDumpMessage | SearchReadyMessage
| SearchQueryMessage | SearchQueryMessage
| SearchResultMessage | 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 * @param message - Search worker message
* *
* @return Test result * @return Test result
*/ */
export function isSearchDumpMessage( export function isSearchReadyMessage(
message: SearchMessage message: SearchMessage
): message is SearchDumpMessage { ): message is SearchReadyMessage {
return message.type === SearchMessageType.DUMP return message.type === SearchMessageType.READY
} }
/** /**

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { SearchIndexConfig, SearchIndexOptions } from "integrations/search" import { SearchIndex, SearchTransformFn } from "integrations"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * 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 * Configuration
*/ */
export interface Config { export interface Config {
url: UrlConfig base: string /* Base URL */
features: Feature[] /* Feature flags */ 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 { export function isConfig(config: any): config is Config {
return typeof config === "object" return typeof config === "object"
&& typeof config.url === "object" && typeof config.base === "string"
&& typeof config.url.base === "string" && typeof config.features === "object"
&& typeof config.url.worker === "object" && typeof config.search === "object"
&& typeof config.url.worker.search === "string"
} }

View File

@ -337,13 +337,6 @@
{% endblock %} {% endblock %}
</div> </div>
<!-- Application configuration -->
{% block config %}
<script>
var __config = {}
</script>
{% endblock %}
<!-- Theme-related JavaScript --> <!-- Theme-related JavaScript -->
{% block scripts %} {% block scripts %}
<script src="{{ 'assets/javascripts/vendor.js' | url }}"></script> <script src="{{ 'assets/javascripts/vendor.js' | url }}"></script>
@ -365,20 +358,21 @@
{%- set _ = translations.update({ key: lang.t(key) }) -%} {%- set _ = translations.update({ key: lang.t(key) }) -%}
{%- endfor -%} {%- endfor -%}
<script id="__lang" type="application/json"> <script id="__lang" type="application/json">
{{ translations | tojson }} {{- translations | tojson -}}
</script> </script>
<!-- Application configuration -->
{% block config %}{% endblock %}
<!-- Application initialization --> <!-- Application initialization -->
<script> <script>
__material = initialize(Object.assign({ app = initialize({
url: { base: "{{ base_url }}",
base: "{{ base_url }}", features: {{ config.theme.features | tojson }},
worker: { search: Object.assign({
search: "{{ 'assets/javascripts/worker/search.js' | url }}" worker: "{{ 'assets/javascripts/worker/search.js' | url }}"
} }, typeof search !== "undefined" && search)
}, })
features: {{ config.theme.features | tojson }}
}, typeof __config !== "undefined" ? __config : {}))
</script> </script>
<!-- Custom JavaScript --> <!-- Custom JavaScript -->

View File

@ -30,7 +30,7 @@
} }
ga.l = +new Date ga.l = +new Date
/* Setup integration and send page view */ /* Set up integration and send page view */
ga("create", "{{ analytics[0] }}", "{{ analytics[1] }}") ga("create", "{{ analytics[0] }}", "{{ analytics[1] }}")
ga("set", "anonymizeIp", true) ga("set", "anonymizeIp", true)
ga("send", "pageview") ga("send", "pageview")

View File

@ -350,7 +350,7 @@ export default (_env: never, args: Configuration): Configuration[] => {
...base, ...base,
entry: { entry: {
"assets/javascripts/worker/search": "assets/javascripts/worker/search":
"src/assets/javascripts/workers/search/main" "src/assets/javascripts/integrations/search/worker/main"
}, },
output: { output: {
path: path.resolve(__dirname, "material"), path: path.resolve(__dirname, "material"),