Merge branch 'master' into feature/scrollable-content-tabs

This commit is contained in:
squidfunk
2021-09-26 11:29:17 +02:00
539 changed files with 2374 additions and 737 deletions

View File

@@ -33,8 +33,10 @@ export type Flag =
| "header.autohide" /* Hide header */
| "navigation.expand" /* Automatic expansion */
| "navigation.instant" /* Instant loading */
| "navigation.sections" /* Sections navigation */
| "navigation.indexes" /* Section pages */
| "navigation.sections" /* Section navigation */
| "navigation.tabs" /* Tabs navigation */
| "navigation.tabs.sticky" /* Tabs navigation (sticky) */
| "navigation.top" /* Back-to-top button */
| "search.highlight" /* Search highlighting */
| "search.share" /* Search sharing */
@@ -96,7 +98,7 @@ export interface Config {
*/
const script = getElementOrThrow("#__config")
const config: Config = JSON.parse(script.textContent!)
config.base = new URL(config.base, getLocation()).toString()
config.base = `${new URL(config.base, getLocation())}`
/* ----------------------------------------------------------------------------
* Functions

View File

@@ -36,7 +36,7 @@
*/
export function getElement<T extends keyof HTMLElementTagNameMap>(
selector: T, node?: ParentNode
): HTMLElementTagNameMap[T]
): HTMLElementTagNameMap[T] | undefined
export function getElement<T extends HTMLElement>(
selector: string, node?: ParentNode
@@ -74,6 +74,8 @@ export function getElementOrThrow<T extends HTMLElement>(
throw new ReferenceError(
`Missing element: expected "${selector}" to be present`
)
/* Return element */
return el
}
@@ -114,21 +116,6 @@ export function getElements<T extends HTMLElement>(
/* ------------------------------------------------------------------------- */
/**
* Create an element
*
* @template T - Tag name type
*
* @param tagName - Tag name
*
* @returns Element
*/
export function createElement<T extends keyof HTMLElementTagNameMap>(
tagName: T
): HTMLElementTagNameMap[T] {
return document.createElement(tagName)
}
/**
* Replace an element with the given list of nodes
*

View File

@@ -20,10 +20,16 @@
* IN THE SOFTWARE.
*/
import { Observable, fromEvent, of } from "rxjs"
import { filter, map, share, startWith, switchMap } from "rxjs/operators"
import { Observable, fromEvent } from "rxjs"
import {
filter,
map,
shareReplay,
startWith
} from "rxjs/operators"
import { createElement, getElement } from "~/browser"
import { getElement } from "~/browser"
import { h } from "~/utilities"
/* ----------------------------------------------------------------------------
* Functions
@@ -49,8 +55,7 @@ export function getLocationHash(): string {
* @param hash - Location hash
*/
export function setLocationHash(hash: string): void {
const el = createElement("a")
el.href = hash
const el = h("a", { href: hash })
el.addEventListener("click", ev => ev.stopPropagation())
el.click()
}
@@ -68,7 +73,7 @@ export function watchLocationHash(): Observable<string> {
map(getLocationHash),
startWith(getLocationHash()),
filter(hash => hash.length > 0),
share()
shareReplay(1)
)
}
@@ -80,6 +85,7 @@ export function watchLocationHash(): Observable<string> {
export function watchLocationTarget(): Observable<HTMLElement> {
return watchLocationHash()
.pipe(
switchMap(id => of(getElement(`[id="${id}"]`)!))
map(id => getElement(`[id="${id}"]`)!),
filter(el => typeof el !== "undefined")
)
}

View File

@@ -27,7 +27,7 @@ import { getElementOrThrow, getElements } from "~/browser"
* ------------------------------------------------------------------------- */
/**
* Component
* Component type
*/
export type ComponentType =
| "announce" /* Announcement bar */
@@ -52,7 +52,7 @@ export type ComponentType =
| "top" /* Back-to-top button */
/**
* A component
* Component
*
* @template T - Component type
* @template U - Reference type

View File

@@ -31,6 +31,7 @@ import {
} from "rxjs"
import {
distinctUntilKeyChanged,
finalize,
map,
switchMap,
tap,
@@ -175,7 +176,8 @@ export function mountCodeBlock(
/* Create and return component */
return watchCodeBlock(el, options)
.pipe(
tap(internal$),
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -23,6 +23,7 @@
import { Observable, Subject } from "rxjs"
import {
filter,
finalize,
map,
mapTo,
mergeWith,
@@ -38,7 +39,9 @@ import { Component } from "../../_"
/**
* Details
*/
export interface Details {}
export interface Details {
scroll?: boolean /* Scroll into view */
}
/* ----------------------------------------------------------------------------
* Helper types
@@ -79,8 +82,8 @@ export function watchDetails(
.pipe(
map(target => target.closest("details:not([open])")!),
filter(details => el === details),
mergeWith(print$),
mapTo(el)
mapTo({ scroll: true }),
mergeWith(print$.pipe(mapTo({})))
)
}
@@ -99,15 +102,17 @@ export function mountDetails(
el: HTMLDetailsElement, options: MountOptions
): Observable<Component<Details>> {
const internal$ = new Subject<Details>()
internal$.subscribe(() => {
internal$.subscribe(({ scroll }) => {
el.setAttribute("open", "")
el.scrollIntoView()
if (scroll)
el.scrollIntoView()
})
/* Create and return component */
return watchDetails(el, options)
.pipe(
tap(internal$),
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
mapTo({ ref: el })
)
}

View File

@@ -22,8 +22,9 @@
import { Observable, of } from "rxjs"
import { createElement, replaceElement } from "~/browser"
import { replaceElement } from "~/browser"
import { renderTable } from "~/templates"
import { h } from "~/utilities"
import { Component } from "../../_"
@@ -43,7 +44,7 @@ export interface DataTable {}
/**
* Sentinel for replacement
*/
const sentinel = createElement("table")
const sentinel = h("table")
/* ----------------------------------------------------------------------------
* Functions

View File

@@ -29,6 +29,7 @@ import {
} from "rxjs"
import {
delay,
finalize,
map,
observeOn,
switchMap,
@@ -131,7 +132,8 @@ export function mountDialog(
/* Create and return component */
return watchDialog(el, options)
.pipe(
tap(internal$),
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -28,6 +28,7 @@ import {
} from "rxjs"
import {
distinctUntilKeyChanged,
finalize,
map,
observeOn,
tap
@@ -139,7 +140,8 @@ export function mountHeaderTitle(
/* Create and return component */
return watchHeaderTitle(headline, options)
.pipe(
tap(internal$),
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -27,6 +27,7 @@ import {
of
} from "rxjs"
import {
finalize,
map,
mapTo,
mergeMap,
@@ -140,7 +141,8 @@ export function mountPalette(
const inputs = getElements<HTMLInputElement>("input", el)
return watchPalette(inputs)
.pipe(
tap(internal$),
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -95,7 +95,8 @@ export function mountSearch(
): Observable<Component<Search>> {
const config = configuration()
try {
const worker = setupSearchWorker(config.search, index$)
const url = __search?.worker || config.search
const worker = setupSearchWorker(url, index$)
/* Retrieve query and result components */
const query = getComponentElement("search-query", el)
@@ -212,7 +213,7 @@ export function mountSearch(
/* Search sharing */
...getComponentElements("search-share", el)
.map(child => mountSearchShare(child, { query$ })),
.map(child => mountSearchShare(child, { query$ })),
/* Search suggestions */
...getComponentElements("search-suggest", el)

View File

@@ -83,7 +83,7 @@ export function mountSearchHiglight(
)
])
.pipe(
map(([index, url]) => setupSearchHighlighter(index.config)(
map(([index, url]) => setupSearchHighlighter(index.config, true)(
url.searchParams.get("h")!
)),
map(fn => {

View File

@@ -32,6 +32,7 @@ import {
distinctUntilChanged,
distinctUntilKeyChanged,
filter,
finalize,
map,
take,
takeLast,
@@ -172,7 +173,8 @@ export function mountSearchQuery(
/* Create and return component */
return watchSearchQuery(el, { tx$, rx$ })
.pipe(
tap(internal$),
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -30,6 +30,7 @@ import {
import {
bufferCount,
filter,
finalize,
map,
observeOn,
switchMap,
@@ -152,7 +153,8 @@ export function mountSearchResult(
/* Create and return component */
return result$
.pipe(
tap(internal$),
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -25,7 +25,11 @@ import {
Subject,
fromEvent
} from "rxjs"
import { map, tap } from "rxjs/operators"
import {
finalize,
map,
tap
} from "rxjs/operators"
import { getLocation } from "~/browser"
@@ -112,7 +116,8 @@ export function mountSearchShare(
/* Create and return component */
return watchSearchShare(el, options)
.pipe(
tap(internal$),
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -30,6 +30,7 @@ import {
combineLatestWith,
distinctUntilChanged,
filter,
finalize,
map,
observeOn,
tap
@@ -144,7 +145,8 @@ export function mountSearchSuggest(
/* Create and return component */
return result$
.pipe(
tap(internal$),
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(() => ({ ref: el }))
)
}

View File

@@ -28,6 +28,7 @@ import {
} from "rxjs"
import {
distinctUntilChanged,
finalize,
map,
observeOn,
tap,
@@ -157,7 +158,8 @@ export function mountSidebar(
/* Create and return component */
return watchSidebar(el, options)
.pipe(
tap(internal$),
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -24,6 +24,7 @@ import { NEVER, Observable, Subject, defer, of } from "rxjs"
import {
catchError,
filter,
finalize,
map,
shareReplay,
tap
@@ -117,7 +118,8 @@ export function mountSource(
/* Create and return component */
return watchSource(el)
.pipe(
tap(internal$),
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -20,15 +20,22 @@
* IN THE SOFTWARE.
*/
import { Observable, Subject, animationFrameScheduler } from "rxjs"
import {
Observable,
Subject,
animationFrameScheduler,
of
} from "rxjs"
import {
distinctUntilKeyChanged,
finalize,
map,
observeOn,
switchMap,
tap
} from "rxjs/operators"
import { feature } from "~/_"
import { resetTabsState, setTabsState } from "~/actions"
import {
Viewport,
@@ -133,9 +140,14 @@ export function mountTabs(
})
/* Create and return component */
return watchTabs(el, options)
return (
feature("navigation.tabs.sticky")
? of({ hidden: false })
: watchTabs(el, options)
)
.pipe(
tap(internal$),
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -30,6 +30,7 @@ import {
bufferCount,
distinctUntilChanged,
distinctUntilKeyChanged,
finalize,
map,
observeOn,
scan,
@@ -267,7 +268,8 @@ export function mountTableOfContents(
const anchors = getElements<HTMLAnchorElement>("[href^=\\#]", el)
return watchTableOfContents(anchors, options)
.pipe(
tap(internal$),
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -30,6 +30,7 @@ import {
bufferCount,
distinctUntilChanged,
distinctUntilKeyChanged,
finalize,
map,
observeOn,
tap,
@@ -39,8 +40,10 @@ import {
import {
resetBackToTopOffset,
resetBackToTopState,
resetFocusable,
setBackToTopOffset,
setBackToTopState
setBackToTopState,
setFocusable
} from "~/actions"
import { Viewport, setElementFocus } from "~/browser"
@@ -155,8 +158,10 @@ export function mountBackToTop(
if (hidden) {
setBackToTopState(el, "hidden")
setElementFocus(el, false)
setFocusable(el, -1)
} else {
resetBackToTopState(el)
resetFocusable(el)
}
},
@@ -164,13 +169,15 @@ export function mountBackToTop(
complete() {
resetBackToTopOffset(el)
resetBackToTopState(el)
resetFocusable(el)
}
})
/* Create and return component */
return watchBackToTop(el, { viewport$, header$, main$ })
.pipe(
tap(internal$),
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -45,11 +45,10 @@ import {
switchMap
} from "rxjs/operators"
import { configuration } from "~/_"
import { configuration, feature } from "~/_"
import {
Viewport,
ViewportOffset,
createElement,
getElement,
getElements,
replaceElement,
@@ -60,6 +59,7 @@ import {
setViewportOffset
} from "~/browser"
import { getComponentElement } from "~/components"
import { h } from "~/utilities"
/* ----------------------------------------------------------------------------
* Types
@@ -283,7 +283,10 @@ export function setupInstantLoading(
"[data-md-component=container]",
"[data-md-component=header-topic]",
"[data-md-component=logo], .md-logo", // compat
"[data-md-component=skip]"
"[data-md-component=skip]",
...feature("navigation.tabs.sticky")
? ["[data-md-component=tabs]"]
: []
]) {
const source = getElement(selector)
const target = getElement(selector, replacement)
@@ -303,7 +306,7 @@ export function setupInstantLoading(
map(() => getComponentElement("container")),
switchMap(el => of(...getElements("script", el))),
concatMap(el => {
const script = createElement("script")
const script = h("script")
if (el.src) {
for (const name of el.getAttributeNames())
script.setAttribute(name, el.getAttribute(name)!)

View File

@@ -167,7 +167,7 @@ export class Search {
/* Set up document map and highlighter factory */
this.documents = setupSearchDocumentMap(docs)
this.highlight = setupSearchHighlighter(config)
this.highlight = setupSearchHighlighter(config, false)
/* Set separator for tokenizer */
lunr.tokenizer.separator = new RegExp(config.separator)

View File

@@ -54,11 +54,12 @@ export type SearchHighlightFactoryFn = (query: string) => SearchHighlightFn
* Create a search highlighter
*
* @param config - Search index configuration
* @param escape - Whether to escape HTML
*
* @returns Search highlight factory function
*/
export function setupSearchHighlighter(
config: SearchIndexConfig
config: SearchIndexConfig, escape: boolean
): SearchHighlightFactoryFn {
const separator = new RegExp(config.separator, "img")
const highlight = (_: unknown, data: string, term: string) => {
@@ -79,8 +80,12 @@ export function setupSearchHighlighter(
})`, "img")
/* Highlight string value */
return value => escapeHTML(value)
.replace(match, highlight)
.replace(/<\/mark>(\s+)<mark[^>]*>/img, "$1")
return value => (
escape
? escapeHTML(value)
: value
)
.replace(match, highlight)
.replace(/<\/mark>(\s+)<mark[^>]*>/img, "$1")
}
}

View File

@@ -39,7 +39,7 @@ export const enum SearchMessageType {
/* ------------------------------------------------------------------------- */
/**
* A message containing the data necessary to setup the search index
* Message containing the data necessary to setup the search index
*/
export interface SearchSetupMessage {
type: SearchMessageType.SETUP /* Message type */
@@ -47,14 +47,14 @@ export interface SearchSetupMessage {
}
/**
* A message indicating the search index is ready
* Message indicating the search index is ready
*/
export interface SearchReadyMessage {
type: SearchMessageType.READY /* Message type */
}
/**
* A message containing a search query
* Message containing a search query
*/
export interface SearchQueryMessage {
type: SearchMessageType.QUERY /* Message type */
@@ -62,7 +62,7 @@ export interface SearchQueryMessage {
}
/**
* A message containing results for a search query
* Message containing results for a search query
*/
export interface SearchResultMessage {
type: SearchMessageType.RESULT /* Message type */
@@ -72,7 +72,7 @@ export interface SearchResultMessage {
/* ------------------------------------------------------------------------- */
/**
* A message exchanged with the search worker
* Message exchanged with the search worker
*/
export type SearchMessage =
| SearchSetupMessage

View File

@@ -33,7 +33,7 @@ import { Version, renderVersionSelector } from "~/templates"
*/
export function setupVersionSelector(): void {
const config = configuration()
requestJSON<Version[]>(new URL("versions.json", config.base))
requestJSON<Version[]>(new URL("../versions.json", config.base))
.subscribe(versions => {
const topic = getElementOrThrow(".md-header__topic")
topic.appendChild(renderVersionSelector(versions))

View File

@@ -51,7 +51,7 @@ function renderVersion(version: Version): HTMLElement {
const config = configuration()
/* Ensure trailing slash, see https://bit.ly/3rL5u3f */
const url = new URL(`${version.version}/`, config.base)
const url = new URL(`../${version.version}/`, config.base)
return (
<li class="md-version__item">
<a href={url.toString()} class="md-version__link">

View File

@@ -1,6 +1,7 @@
{
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-namespace": "off"
"@typescript-eslint/no-namespace": "off",
"jsdoc/require-jsdoc": "off"
}
}

View File

@@ -77,15 +77,25 @@ function appendChild(el: HTMLElement, child: Child | Child[]): void {
/**
* JSX factory
*
* @template T - Element type
*
* @param tag - HTML tag
* @param attributes - HTML attributes
* @param children - Child elements
*
* @returns Element
*/
export function h(
tag: string, attributes: Attributes | null, ...children: Child[]
): HTMLElement {
export function h<T extends keyof HTMLElementTagNameMap>(
tag: T, attributes?: Attributes | null, ...children: Child[]
): HTMLElementTagNameMap[T]
export function h<T extends h.JSX.Element>(
tag: string, attributes?: Attributes | null, ...children: Child[]
): T
export function h<T extends h.JSX.Element>(
tag: string, attributes?: Attributes | null, ...children: Child[]
): T {
const el = document.createElement(tag)
/* Set attributes, if any */
@@ -101,7 +111,7 @@ export function h(
appendChild(el, child)
/* Return element */
return el
return el as T
}
/* ----------------------------------------------------------------------------