Formatting + moved search index fetching to top level

This commit is contained in:
squidfunk
2021-02-24 18:02:09 +01:00
parent cb723d4bef
commit d6317dc514
69 changed files with 404 additions and 328 deletions

View File

@@ -123,7 +123,7 @@ export function feature(flag: Flag): boolean {
* Retrieve the translation for the given key
*
* @param key - Key to be translated
* @param value - Value to be replaced
* @param value - Positional value, if any
*
* @returns Translation
*/

View File

@@ -30,9 +30,8 @@ import { mapTo } from "rxjs/operators"
/**
* Watch document
*
* Documents must be implemented as subjects, so all downstream observables are
* automatically updated when a new document is emitted. This enabled features
* like instant loading.
* Documents are implemented as subjects, so all downstream observables are
* automatically updated when a new document is emitted.
*
* @returns Document subject
*/

View File

@@ -21,9 +21,16 @@
*/
import { Observable, fromEvent, merge } from "rxjs"
import { distinctUntilChanged, map, startWith } from "rxjs/operators"
import {
distinctUntilChanged,
map,
startWith
} from "rxjs/operators"
import { getElementContentSize, getElementSize } from "../size"
import {
getElementContentSize,
getElementSize
} from "../size"
/* ----------------------------------------------------------------------------
* Types

View File

@@ -29,10 +29,10 @@ import { Subject } from "rxjs"
/**
* Retrieve location
*
* This function will return a `URL` object (and not `Location`) in order to
* normalize typings across the application. Furthermore, locations need to be
* tracked without setting them and `Location` is a singleton which represents
* the current location.
* This function returns a `URL` object (and not `Location`) to normalize the
* typings across the application. Furthermore, locations need to be tracked
* without setting them and `Location` is a singleton which represents the
* current location.
*
* @returns URL
*/

View File

@@ -31,10 +31,11 @@ import {
switchMap
} from "rxjs/operators"
import { feature } from "./_"
import { configuration, feature } from "./_"
import {
at,
getElement,
requestJSON,
setToggle,
watchDocument,
watchKeyboard,
@@ -60,6 +61,7 @@ import {
watchMain
} from "./components"
import {
SearchIndex,
setupClipboardJS,
setupInstantLoading
} from "./integrations"
@@ -89,6 +91,12 @@ const tablet$ = watchMedia("(min-width: 960px)")
const screen$ = watchMedia("(min-width: 1220px)")
const print$ = watchPrint()
/* Retrieve search index */
const config = configuration()
const index$ = __search?.index || requestJSON<SearchIndex>(
`${config.base}/search/search_index.json`
)
/* Set up Clipboard.js integration */
const alert$ = new Subject<string>()
setupClipboardJS({ alert$ })
@@ -160,11 +168,11 @@ const control$ = merge(
/* Search */
...getComponentElements("search")
.map(el => mountSearch(el, { keyboard$ })),
.map(el => mountSearch(el, { index$, keyboard$ })),
/* Repository information */
...getComponentElements("source")
.map(el => mountSource(el as HTMLAnchorElement)),
.map(el => mountSource(el)),
/* Navigation tabs */
...getComponentElements("tabs")

View File

@@ -61,6 +61,32 @@ export type Component<
ref: U /* Component reference */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Component type map
*/
interface ComponentTypeMap {
"announce": HTMLElement /* Announcement bar */
"container": HTMLElement /* Container */
"content": HTMLElement /* Content */
"dialog": HTMLElement /* Dialog */
"header": HTMLElement /* Header */
"header-title": HTMLElement /* Header title */
"header-topic": HTMLElement /* Header topic */
"main": HTMLElement /* Main area */
"search": HTMLElement /* Search */
"search-query": HTMLInputElement /* Search input */
"search-result": HTMLElement /* Search results */
"sidebar": HTMLElement /* Sidebar */
"skip": HTMLAnchorElement /* Skip link */
"source": HTMLAnchorElement /* Repository information */
"tabs": HTMLElement /* Navigation tabs */
"toc": HTMLElement /* Table of contents */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
@@ -68,31 +94,31 @@ export type Component<
/**
* Retrieve the element for a given component or throw a reference error
*
* @template T - Element type
* @template T - Component type
*
* @param type - Component type
* @param node - Node of reference
*
* @returns Element
*/
export function getComponentElement<T extends HTMLElement>(
type: ComponentType, node: ParentNode = document
): T {
export function getComponentElement<T extends ComponentType>(
type: T, node: ParentNode = document
): ComponentTypeMap[T] {
return getElementOrThrow(`[data-md-component=${type}]`, node)
}
/**
* Retrieve all elements for a given component
*
* @template T - Element type
* @template T - Component type
*
* @param type - Component type
* @param node - Node of reference
*
* @returns Elements
*/
export function getComponentElements<T extends HTMLElement>(
type: ComponentType, node: ParentNode = document
): T[] {
export function getComponentElements<T extends ComponentType>(
type: T, node: ParentNode = document
): ComponentTypeMap[T][] {
return getElements(`[data-md-component=${type}]`, node)
}

View File

@@ -95,8 +95,8 @@ let index = 0
/**
* Watch code block
*
* This function will monitor size changes of the viewport, as well as switches
* of content tabs with embedded code blocks, as both may trigger overflow.
* This function monitors size changes of the viewport, as well as switches of
* content tabs with embedded code blocks, as both may trigger overflow.
*
* @param el - Code block element
* @param options - Options
@@ -163,7 +163,7 @@ export function mountCodeBlock(
resetFocusable(el)
})
/* Inject button for Clipboard.js integration */
/* Render button for Clipboard.js integration */
if (ClipboardJS.isSupported()) {
const parent = el.closest("pre")!
parent.id = `__code_${index++}`

View File

@@ -52,8 +52,8 @@ const sentinel = createElement("table")
/**
* Mount data table
*
* This function wraps a data table in another scrollable container, so they
* can be scrolled on smaller screen sizes and won't break the layout.
* This function wraps a data table in another scrollable container, so it can
* be smoothly scrolled on smaller screen sizes and won't break the layout.
*
* @param el - Data table element
*

View File

@@ -105,8 +105,8 @@ export function watchDialog(
/**
* Mount dialog
*
* This function makes the dialog in the right corner appear when a new alert
* is emitted through the subject that is passed as part of the options.
* This function reveals the dialog in the right cornerwhen a new alert is
* emitted through the subject that is passed as part of the options.
*
* @param el - Dialog element
* @param options - Options

View File

@@ -28,7 +28,6 @@ import {
Keyboard,
getActiveElement,
getElements,
requestJSON,
setElementFocus,
setElementSelection,
setToggle
@@ -63,24 +62,10 @@ export type Search =
* Mount options
*/
interface MountOptions {
index$: ObservableInput<SearchIndex> /* Search index observable */
keyboard$: Observable<Keyboard> /* Keyboard observable */
}
/* ----------------------------------------------------------------------------
* Helper functions
* ------------------------------------------------------------------------- */
/**
* Fetch search index
*
* @param url - Search index URL
*
* @returns Promise or observable
*/
function fetchSearchIndex(url: string): ObservableInput<SearchIndex> {
return __search?.index || requestJSON<SearchIndex>(url)
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
@@ -97,16 +82,14 @@ function fetchSearchIndex(url: string): ObservableInput<SearchIndex> {
* @returns Search component observable
*/
export function mountSearch(
el: HTMLElement, { keyboard$ }: MountOptions
el: HTMLElement, { index$, keyboard$ }: MountOptions
): Observable<Component<Search>> {
if (location.protocol === "file:")
return NEVER
/* Set up search worker */
const config = configuration()
const worker = setupSearchWorker(config.search, fetchSearchIndex(
`${config.base}/search/search_index.json`
))
const worker = setupSearchWorker(config.search, index$)
/* Retrieve nested components */
const query = getComponentElement("search-query", el)
@@ -193,7 +176,7 @@ export function mountSearch(
})
/* Create and return component */
const query$ = mountSearchQuery(query as HTMLInputElement, worker)
const query$ = mountSearchQuery(query, worker)
return merge(
query$,
mountSearchResult(result, worker, { query$ })

View File

@@ -33,7 +33,6 @@ import {
distinctUntilKeyChanged,
finalize,
map,
startWith,
takeLast,
takeUntil,
tap
@@ -96,7 +95,6 @@ export function watchSearchQuery(
)
.pipe(
map(() => fn(el.value)),
startWith(fn(el.value)),
distinctUntilChanged()
)

View File

@@ -89,8 +89,8 @@ interface MountOptions {
/**
* Mount search result list
*
* This function will perform a lazy rendering of the search results, depending
* on the vertical offset of the search result container.
* This function performs a lazy rendering of the search results, depending on
* the vertical offset of the search result container.
*
* @param el - Search result list element
* @param worker - Search worker

View File

@@ -32,7 +32,7 @@ import {
import { setSourceFacts, setSourceState } from "~/actions"
import { renderSourceFacts } from "~/templates"
import { hash } from "~/utilities"
import { digest } from "~/utilities"
import { Component } from "../../_"
import { SourceFacts, fetchSourceFacts } from "../facts"
@@ -53,7 +53,7 @@ export interface Source {
* ------------------------------------------------------------------------- */
/**
* Repository facts observable
* Repository information observable
*/
let fetch$: Observable<Source>
@@ -64,8 +64,8 @@ let fetch$: Observable<Source>
/**
* Watch repository information
*
* This function will try to read the repository facts from session storage,
* and if unsuccessful, fetch them from the underlying provider.
* This function tries to read the repository facts from session storage, and
* if unsuccessful, fetches them from the underlying provider.
*
* @param el - Repository information element
*
@@ -74,18 +74,15 @@ let fetch$: Observable<Source>
export function watchSource(
el: HTMLAnchorElement
): Observable<Source> {
const digest = hash(el.href).toString()
/* Fetch repository facts once */
return fetch$ ||= defer(() => {
const data = sessionStorage.getItem(digest)
const data = sessionStorage.getItem(digest("__repo"))
if (data) {
return of(JSON.parse(data))
return of<SourceFacts>(JSON.parse(data))
} else {
const value$ = fetchSourceFacts(el.href)
value$.subscribe(value => {
try {
sessionStorage.setItem(digest, JSON.stringify(value))
sessionStorage.setItem(digest("__repo"), JSON.stringify(value))
} catch (err) {
/* Uncritical, just swallow */
}

View File

@@ -84,7 +84,7 @@ function setupSearchIndex(
/**
* Set up search worker
*
* This function will create a web worker to set up and query the search index
* This function creates a web worker to set up and query the search index,
* which is done using Lunr.js. The index must be passed as an observable to
* enable hacks like _localsearch_ via search index embedding as JSON.
*

View File

@@ -62,8 +62,8 @@ let index: Search
/**
* Fetch (= import) 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.
* This function automatically imports the stemmers necessary to process the
* languages, which are defined through the search index configuration.
*
* If the worker runs inside of an `iframe` (when using `iframe-worker` as
* a shim), the base URL for the stemmers to be loaded must be determined by

View File

@@ -42,7 +42,7 @@ interface PatchOptions {
/**
* Patch indeterminate checkboxes
*
* This function will replace the indeterminate "pseudo state" with the actual
* This function replaces the indeterminate "pseudo state" with the actual
* indeterminate state, which is used to keep navigation always expanded.
*
* @param options - Options

View File

@@ -20,6 +20,8 @@
* IN THE SOFTWARE.
*/
import { configuration } from "~/_"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
@@ -88,3 +90,18 @@ export function hash(value: string): number {
}
return h
}
/**
* Add a digest to a value to ensure uniqueness
*
* When a single account hosts multiple sites on the same GitHub Pages domain,
* entries would collide, because session and local storage are not unique.
*
* @param value - Value
*
* @returns Value with digest
*/
export function digest(value: string): string {
const config = configuration()
return `${value}[${hash(config.base)}]`
}