diff options
| author | 2023-12-15 10:08:56 +0800 | |
|---|---|---|
| committer | 2023-12-15 10:08:56 +0800 | |
| commit | d697a1774ad8571e9314e3bd85aa141170587d19 (patch) | |
| tree | cd3c3d84e3288db1f8e574a0b4dfb85e2e40f764 /src/templates/assets/javascripts | |
| parent | 4943d5eff52a75caaccda6a1d84183032f06be26 (diff) | |
| parent | 4dafb0f0a81255193f2a44df5d203239325e2236 (diff) | |
| download | infini-d697a1774ad8571e9314e3bd85aa141170587d19.tar.gz infini-d697a1774ad8571e9314e3bd85aa141170587d19.zip | |
Merge branch 'master' into master
Diffstat (limited to 'src/templates/assets/javascripts')
117 files changed, 0 insertions, 12322 deletions
diff --git a/src/templates/assets/javascripts/_/index.ts b/src/templates/assets/javascripts/_/index.ts deleted file mode 100644 index be0f4a42..00000000 --- a/src/templates/assets/javascripts/_/index.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { getElement, getLocation } from "~/browser" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Feature flag - */ -export type Flag = - | "announce.dismiss" /* Dismissable announcement bar */ - | "content.code.annotate" /* Code annotations */ - | "content.code.copy" /* Code copy button */ - | "content.lazy" /* Lazy content elements */ - | "content.tabs.link" /* Link content tabs */ - | "header.autohide" /* Hide header */ - | "navigation.expand" /* Automatic expansion */ - | "navigation.indexes" /* Section pages */ - | "navigation.instant" /* Instant navigation */ - | "navigation.instant.progress" /* Instant navigation progress */ - | "navigation.sections" /* Section navigation */ - | "navigation.tabs" /* Tabs navigation */ - | "navigation.tabs.sticky" /* Tabs navigation (sticky) */ - | "navigation.top" /* Back-to-top button */ - | "navigation.tracking" /* Anchor tracking */ - | "search.highlight" /* Search highlighting */ - | "search.share" /* Search sharing */ - | "search.suggest" /* Search suggestions */ - | "toc.follow" /* Following table of contents */ - | "toc.integrate" /* Integrated table of contents */ - -/* ------------------------------------------------------------------------- */ - -/** - * Translation - */ -export type Translation = - | "clipboard.copy" /* Copy to clipboard */ - | "clipboard.copied" /* Copied to clipboard */ - | "search.result.placeholder" /* Type to start searching */ - | "search.result.none" /* No matching documents */ - | "search.result.one" /* 1 matching document */ - | "search.result.other" /* # matching documents */ - | "search.result.more.one" /* 1 more on this page */ - | "search.result.more.other" /* # more on this page */ - | "search.result.term.missing" /* Missing */ - | "select.version" /* Version selector */ - -/** - * Translations - */ -export type Translations = - Record<Translation, string> - -/* ------------------------------------------------------------------------- */ - -/** - * Versioning - */ -export interface Versioning { - provider: "mike" /* Version provider */ - default?: string | string[] /* Default version */ -} - -/** - * Configuration - */ -export interface Config { - base: string /* Base URL */ - features: Flag[] /* Feature flags */ - translations: Translations /* Translations */ - search: string /* Search worker URL */ - tags?: Record<string, string> /* Tags mapping */ - version?: Versioning /* Versioning */ -} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Retrieve global configuration and make base URL absolute - */ -const script = getElement("#__config") -const config: Config = JSON.parse(script.textContent!) -config.base = `${new URL(config.base, getLocation())}` - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve global configuration - * - * @returns Global configuration - */ -export function configuration(): Config { - return config -} - -/** - * Check whether a feature flag is enabled - * - * @param flag - Feature flag - * - * @returns Test result - */ -export function feature(flag: Flag): boolean { - return config.features.includes(flag) -} - -/** - * Retrieve the translation for the given key - * - * @param key - Key to be translated - * @param value - Positional value, if any - * - * @returns Translation - */ -export function translation( - key: Translation, value?: string | number -): string { - return typeof value !== "undefined" - ? config.translations[key].replace("#", value.toString()) - : config.translations[key] -} diff --git a/src/templates/assets/javascripts/browser/document/index.ts b/src/templates/assets/javascripts/browser/document/index.ts deleted file mode 100644 index 354c9b5c..00000000 --- a/src/templates/assets/javascripts/browser/document/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - ReplaySubject, - Subject, - fromEvent -} from "rxjs" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch document - * - * Documents are implemented as subjects, so all downstream observables are - * automatically updated when a new document is emitted. - * - * @returns Document subject - */ -export function watchDocument(): Subject<Document> { - const document$ = new ReplaySubject<Document>(1) - fromEvent(document, "DOMContentLoaded", { once: true }) - .subscribe(() => document$.next(document)) - - /* Return document */ - return document$ -} diff --git a/src/templates/assets/javascripts/browser/element/_/.eslintrc b/src/templates/assets/javascripts/browser/element/_/.eslintrc deleted file mode 100644 index 16973760..00000000 --- a/src/templates/assets/javascripts/browser/element/_/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "jsdoc/require-jsdoc": "off", - "jsdoc/require-returns-check": "off" - } -} diff --git a/src/templates/assets/javascripts/browser/element/_/index.ts b/src/templates/assets/javascripts/browser/element/_/index.ts deleted file mode 100644 index b7beb462..00000000 --- a/src/templates/assets/javascripts/browser/element/_/index.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2016-2023 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. - */ - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve all elements matching the query selector - * - * @template T - Element type - * - * @param selector - Query selector - * @param node - Node of reference - * - * @returns Elements - */ -export function getElements<T extends keyof HTMLElementTagNameMap>( - selector: T, node?: ParentNode -): HTMLElementTagNameMap[T][] - -export function getElements<T extends HTMLElement>( - selector: string, node?: ParentNode -): T[] - -export function getElements<T extends HTMLElement>( - selector: string, node: ParentNode = document -): T[] { - return Array.from(node.querySelectorAll<T>(selector)) -} - -/** - * Retrieve an element matching a query selector or throw a reference error - * - * Note that this function assumes that the element is present. If unsure if an - * element is existent, use the `getOptionalElement` function instead. - * - * @template T - Element type - * - * @param selector - Query selector - * @param node - Node of reference - * - * @returns Element - */ -export function getElement<T extends keyof HTMLElementTagNameMap>( - selector: T, node?: ParentNode -): HTMLElementTagNameMap[T] - -export function getElement<T extends HTMLElement>( - selector: string, node?: ParentNode -): T - -export function getElement<T extends HTMLElement>( - selector: string, node: ParentNode = document -): T { - const el = getOptionalElement<T>(selector, node) - if (typeof el === "undefined") - throw new ReferenceError( - `Missing element: expected "${selector}" to be present` - ) - - /* Return element */ - return el -} - -/* ------------------------------------------------------------------------- */ - -/** - * Retrieve an optional element matching the query selector - * - * @template T - Element type - * - * @param selector - Query selector - * @param node - Node of reference - * - * @returns Element or nothing - */ -export function getOptionalElement<T extends keyof HTMLElementTagNameMap>( - selector: T, node?: ParentNode -): HTMLElementTagNameMap[T] | undefined - -export function getOptionalElement<T extends HTMLElement>( - selector: string, node?: ParentNode -): T | undefined - -export function getOptionalElement<T extends HTMLElement>( - selector: string, node: ParentNode = document -): T | undefined { - return node.querySelector<T>(selector) || undefined -} - -/** - * Retrieve the currently active element - * - * @returns Element or nothing - */ -export function getActiveElement(): HTMLElement | undefined { - return document.activeElement instanceof HTMLElement - ? document.activeElement || undefined - : undefined -} diff --git a/src/templates/assets/javascripts/browser/element/focus/index.ts b/src/templates/assets/javascripts/browser/element/focus/index.ts deleted file mode 100644 index f31fe276..00000000 --- a/src/templates/assets/javascripts/browser/element/focus/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - debounceTime, - distinctUntilChanged, - fromEvent, - map, - merge, - shareReplay, - startWith -} from "rxjs" - -import { getActiveElement } from "../_" - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Focus observable - * - * Previously, this observer used `focus` and `blur` events to determine whether - * an element is focused, but this doesn't work if there are focusable elements - * within the elements itself. A better solutions are `focusin` and `focusout` - * events, which bubble up the tree and allow for more fine-grained control. - * - * `debounceTime` is necessary, because when a focus change happens inside an - * element, the observable would first emit `false` and then `true` again. - */ -const observer$ = merge( - fromEvent(document.body, "focusin"), - fromEvent(document.body, "focusout") -) - .pipe( - debounceTime(1), - startWith(undefined), - map(() => getActiveElement() || document.body), - shareReplay(1) - ) - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch element focus - * - * @param el - Element - * - * @returns Element focus observable - */ -export function watchElementFocus( - el: HTMLElement -): Observable<boolean> { - return observer$ - .pipe( - map(active => el.contains(active)), - distinctUntilChanged() - ) -} diff --git a/src/templates/assets/javascripts/browser/element/index.ts b/src/templates/assets/javascripts/browser/element/index.ts deleted file mode 100644 index 50ce84b2..00000000 --- a/src/templates/assets/javascripts/browser/element/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./focus" -export * from "./offset" -export * from "./size" -export * from "./visibility" diff --git a/src/templates/assets/javascripts/browser/element/offset/_/index.ts b/src/templates/assets/javascripts/browser/element/offset/_/index.ts deleted file mode 100644 index 6dd229d5..00000000 --- a/src/templates/assets/javascripts/browser/element/offset/_/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - animationFrameScheduler, - auditTime, - fromEvent, - map, - merge, - startWith -} from "rxjs" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Element offset - */ -export interface ElementOffset { - x: number /* Horizontal offset */ - y: number /* Vertical offset */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve element offset - * - * @param el - Element - * - * @returns Element offset - */ -export function getElementOffset( - el: HTMLElement -): ElementOffset { - return { - x: el.offsetLeft, - y: el.offsetTop - } -} - -/* ------------------------------------------------------------------------- */ - -/** - * Watch element offset - * - * @param el - Element - * - * @returns Element offset observable - */ -export function watchElementOffset( - el: HTMLElement -): Observable<ElementOffset> { - return merge( - fromEvent(window, "load"), - fromEvent(window, "resize") - ) - .pipe( - auditTime(0, animationFrameScheduler), - map(() => getElementOffset(el)), - startWith(getElementOffset(el)) - ) -} diff --git a/src/templates/assets/javascripts/browser/element/offset/content/index.ts b/src/templates/assets/javascripts/browser/element/offset/content/index.ts deleted file mode 100644 index 557301a6..00000000 --- a/src/templates/assets/javascripts/browser/element/offset/content/index.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - animationFrameScheduler, - auditTime, - fromEvent, - map, - merge, - startWith -} from "rxjs" - -import { ElementOffset } from "../_" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve element content offset (= scroll offset) - * - * @param el - Element - * - * @returns Element content offset - */ -export function getElementContentOffset( - el: HTMLElement -): ElementOffset { - return { - x: el.scrollLeft, - y: el.scrollTop - } -} - -/* ------------------------------------------------------------------------- */ - -/** - * Watch element content offset - * - * @param el - Element - * - * @returns Element content offset observable - */ -export function watchElementContentOffset( - el: HTMLElement -): Observable<ElementOffset> { - return merge( - fromEvent(el, "scroll"), - fromEvent(window, "resize") - ) - .pipe( - auditTime(0, animationFrameScheduler), - map(() => getElementContentOffset(el)), - startWith(getElementContentOffset(el)) - ) -} diff --git a/src/templates/assets/javascripts/browser/element/offset/index.ts b/src/templates/assets/javascripts/browser/element/offset/index.ts deleted file mode 100644 index 602ff2cf..00000000 --- a/src/templates/assets/javascripts/browser/element/offset/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./content" diff --git a/src/templates/assets/javascripts/browser/element/size/_/index.ts b/src/templates/assets/javascripts/browser/element/size/_/index.ts deleted file mode 100644 index 35a5e68b..00000000 --- a/src/templates/assets/javascripts/browser/element/size/_/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - NEVER, - Observable, - Subject, - defer, - filter, - finalize, - map, - merge, - of, - shareReplay, - startWith, - switchMap, - tap -} from "rxjs" - -import { watchScript } from "../../../script" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Element offset - */ -export interface ElementSize { - width: number /* Element width */ - height: number /* Element height */ -} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Resize observer entry subject - */ -const entry$ = new Subject<ResizeObserverEntry>() - -/** - * Resize observer observable - * - * This observable will create a `ResizeObserver` on the first subscription - * and will automatically terminate it when there are no more subscribers. - * It's quite important to centralize observation in a single `ResizeObserver`, - * as the performance difference can be quite dramatic, as the link shows. - * - * If the browser doesn't have a `ResizeObserver` implementation available, a - * polyfill is automatically downloaded from unpkg.com. This is also compatible - * with the built-in privacy plugin, which will download the polyfill and put - * it alongside the built site for self-hosting. - * - * @see https://bit.ly/3iIYfEm - Google Groups on performance - */ -const observer$ = defer(() => ( - typeof ResizeObserver === "undefined" - ? watchScript("https://unpkg.com/resize-observer-polyfill") - : of(undefined) -)) - .pipe( - map(() => new ResizeObserver(entries => { - for (const entry of entries) - entry$.next(entry) - })), - switchMap(observer => merge(NEVER, of(observer)) - .pipe( - finalize(() => observer.disconnect()) - ) - ), - shareReplay(1) - ) - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve element size - * - * @param el - Element - * - * @returns Element size - */ -export function getElementSize( - el: HTMLElement -): ElementSize { - return { - width: el.offsetWidth, - height: el.offsetHeight - } -} - -/* ------------------------------------------------------------------------- */ - -/** - * Watch element size - * - * This function returns an observable that subscribes to a single internal - * instance of `ResizeObserver` upon subscription, and emit resize events until - * termination. Note that this function should not be called with the same - * element twice, as the first unsubscription will terminate observation. - * - * Sadly, we can't use the `DOMRect` objects returned by the observer, because - * we need the emitted values to be consistent with `getElementSize`, which will - * return the used values (rounded) and not actual values (unrounded). Thus, we - * use the `offset*` properties. See the linked GitHub issue. - * - * @see https://bit.ly/3m0k3he - GitHub issue - * - * @param el - Element - * - * @returns Element size observable - */ -export function watchElementSize( - el: HTMLElement -): Observable<ElementSize> { - return observer$ - .pipe( - tap(observer => observer.observe(el)), - switchMap(observer => entry$ - .pipe( - filter(({ target }) => target === el), - finalize(() => observer.unobserve(el)), - map(() => getElementSize(el)) - ) - ), - startWith(getElementSize(el)) - ) -} diff --git a/src/templates/assets/javascripts/browser/element/size/content/index.ts b/src/templates/assets/javascripts/browser/element/size/content/index.ts deleted file mode 100644 index 5ed388cf..00000000 --- a/src/templates/assets/javascripts/browser/element/size/content/index.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { ElementSize } from "../_" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve element content size (= scroll width and height) - * - * @param el - Element - * - * @returns Element content size - */ -export function getElementContentSize( - el: HTMLElement -): ElementSize { - return { - width: el.scrollWidth, - height: el.scrollHeight - } -} - -/** - * Retrieve the overflowing container of an element, if any - * - * @param el - Element - * - * @returns Overflowing container or nothing - */ -export function getElementContainer( - el: HTMLElement -): HTMLElement | undefined { - let parent = el.parentElement - while (parent) - if ( - el.scrollWidth <= parent.scrollWidth && - el.scrollHeight <= parent.scrollHeight - ) - parent = (el = parent).parentElement - else - break - - /* Return overflowing container */ - return parent ? el : undefined -} diff --git a/src/templates/assets/javascripts/browser/element/size/index.ts b/src/templates/assets/javascripts/browser/element/size/index.ts deleted file mode 100644 index 602ff2cf..00000000 --- a/src/templates/assets/javascripts/browser/element/size/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./content" diff --git a/src/templates/assets/javascripts/browser/element/visibility/index.ts b/src/templates/assets/javascripts/browser/element/visibility/index.ts deleted file mode 100644 index 1ffe0b8d..00000000 --- a/src/templates/assets/javascripts/browser/element/visibility/index.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - NEVER, - Observable, - Subject, - defer, - distinctUntilChanged, - filter, - finalize, - map, - merge, - of, - shareReplay, - switchMap, - tap -} from "rxjs" - -import { - getElementContentSize, - getElementSize, - watchElementContentOffset -} from "~/browser" - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Intersection observer entry subject - */ -const entry$ = new Subject<IntersectionObserverEntry>() - -/** - * Intersection observer observable - * - * This observable will create an `IntersectionObserver` on first subscription - * and will automatically terminate it when there are no more subscribers. - * - * @see https://bit.ly/3iIYfEm - Google Groups on performance - */ -const observer$ = defer(() => of( - new IntersectionObserver(entries => { - for (const entry of entries) - entry$.next(entry) - }, { - threshold: 0 - }) -)) - .pipe( - switchMap(observer => merge(NEVER, of(observer)) - .pipe( - finalize(() => observer.disconnect()) - ) - ), - shareReplay(1) - ) - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch element visibility - * - * @param el - Element - * - * @returns Element visibility observable - */ -export function watchElementVisibility( - el: HTMLElement -): Observable<boolean> { - return observer$ - .pipe( - tap(observer => observer.observe(el)), - switchMap(observer => entry$ - .pipe( - filter(({ target }) => target === el), - finalize(() => observer.unobserve(el)), - map(({ isIntersecting }) => isIntersecting) - ) - ) - ) -} - -/** - * Watch element boundary - * - * This function returns an observable which emits whether the bottom content - * boundary (= scroll offset) of an element is within a certain threshold. - * - * @param el - Element - * @param threshold - Threshold - * - * @returns Element boundary observable - */ -export function watchElementBoundary( - el: HTMLElement, threshold = 16 -): Observable<boolean> { - return watchElementContentOffset(el) - .pipe( - map(({ y }) => { - const visible = getElementSize(el) - const content = getElementContentSize(el) - return y >= ( - content.height - visible.height - threshold - ) - }), - distinctUntilChanged() - ) -} diff --git a/src/templates/assets/javascripts/browser/index.ts b/src/templates/assets/javascripts/browser/index.ts deleted file mode 100644 index f1ee2bae..00000000 --- a/src/templates/assets/javascripts/browser/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./document" -export * from "./element" -export * from "./keyboard" -export * from "./location" -export * from "./media" -export * from "./request" -export * from "./script" -export * from "./toggle" -export * from "./viewport" -export * from "./worker" diff --git a/src/templates/assets/javascripts/browser/keyboard/index.ts b/src/templates/assets/javascripts/browser/keyboard/index.ts deleted file mode 100644 index 783f2cda..00000000 --- a/src/templates/assets/javascripts/browser/keyboard/index.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - EMPTY, - Observable, - filter, - fromEvent, - map, - merge, - share, - startWith, - switchMap -} from "rxjs" - -import { getActiveElement } from "../element" -import { getToggle } from "../toggle" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Keyboard mode - */ -export type KeyboardMode = - | "global" /* Global */ - | "search" /* Search is open */ - -/* ------------------------------------------------------------------------- */ - -/** - * Keyboard - */ -export interface Keyboard { - mode: KeyboardMode /* Keyboard mode */ - type: string /* Key type */ - claim(): void /* Key claim */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Check whether an element may receive keyboard input - * - * @param el - Element - * @param type - Key type - * - * @returns Test result - */ -function isSusceptibleToKeyboard( - el: HTMLElement, type: string -): boolean { - switch (el.constructor) { - - /* Input elements */ - case HTMLInputElement: - /* @ts-expect-error - omit unnecessary type cast */ - if (el.type === "radio") - return /^Arrow/.test(type) - else - return true - - /* Select element and textarea */ - case HTMLSelectElement: - case HTMLTextAreaElement: - return true - - /* Everything else */ - default: - return el.isContentEditable - } -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch composition events - * - * @returns Composition observable - */ -export function watchComposition(): Observable<boolean> { - return merge( - fromEvent(window, "compositionstart").pipe(map(() => true)), - fromEvent(window, "compositionend").pipe(map(() => false)) - ) - .pipe( - startWith(false) - ) -} - -/** - * Watch keyboard - * - * @returns Keyboard observable - */ -export function watchKeyboard(): Observable<Keyboard> { - const keyboard$ = fromEvent<KeyboardEvent>(window, "keydown") - .pipe( - filter(ev => !(ev.metaKey || ev.ctrlKey)), - map(ev => ({ - mode: getToggle("search") ? "search" : "global", - type: ev.key, - claim() { - ev.preventDefault() - ev.stopPropagation() - } - } as Keyboard)), - filter(({ mode, type }) => { - if (mode === "global") { - const active = getActiveElement() - if (typeof active !== "undefined") - return !isSusceptibleToKeyboard(active, type) - } - return true - }), - share() - ) - - /* Don't emit during composition events - see https://bit.ly/3te3Wl8 */ - return watchComposition() - .pipe( - switchMap(active => !active ? keyboard$ : EMPTY) - ) -} diff --git a/src/templates/assets/javascripts/browser/location/_/index.ts b/src/templates/assets/javascripts/browser/location/_/index.ts deleted file mode 100644 index 2672fa74..00000000 --- a/src/templates/assets/javascripts/browser/location/_/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { Subject } from "rxjs" - -import { feature } from "~/_" -import { h } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve 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 - */ -export function getLocation(): URL { - return new URL(location.href) -} - -/** - * Set location - * - * If instant navigation is enabled, this function creates a temporary anchor - * element, sets the `href` attribute, appends it to the body, clicks it, and - * then removes it again. The event will bubble up the DOM and trigger be - * intercepted by the instant loading business logic. - * - * Note that we must append and remove the anchor element, or the event will - * not bubble up the DOM, making it impossible to intercept it. - * - * @param url - URL to navigate to - * @param navigate - Force navigation - */ -export function setLocation( - url: URL | HTMLLinkElement, navigate = false -): void { - if (feature("navigation.instant") && !navigate) { - const el = h("a", { href: url.href }) - document.body.appendChild(el) - el.click() - el.remove() - - // If we're not using instant navigation, and the page should not be reloaded - // just instruct the browser to navigate to the given URL - } else { - location.href = url.href - } -} - -/* ------------------------------------------------------------------------- */ - -/** - * Watch location - * - * @returns Location subject - */ -export function watchLocation(): Subject<URL> { - return new Subject<URL>() -} diff --git a/src/templates/assets/javascripts/browser/location/hash/index.ts b/src/templates/assets/javascripts/browser/location/hash/index.ts deleted file mode 100644 index 5d3a134a..00000000 --- a/src/templates/assets/javascripts/browser/location/hash/index.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - filter, - fromEvent, - map, - merge, - shareReplay, - startWith -} from "rxjs" - -import { getOptionalElement } from "~/browser" -import { h } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve location hash - * - * @returns Location hash - */ -export function getLocationHash(): string { - return location.hash.slice(1) -} - -/** - * Set location hash - * - * Setting a new fragment identifier via `location.hash` will have no effect - * 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 - */ -export function setLocationHash(hash: string): void { - const el = h("a", { href: hash }) - el.addEventListener("click", ev => ev.stopPropagation()) - el.click() -} - -/* ------------------------------------------------------------------------- */ - -/** - * Watch location hash - * - * @param location$ - Location observable - * - * @returns Location hash observable - */ -export function watchLocationHash( - location$: Observable<URL> -): Observable<string> { - return merge( - fromEvent<HashChangeEvent>(window, "hashchange"), - location$ - ) - .pipe( - map(getLocationHash), - startWith(getLocationHash()), - filter(hash => hash.length > 0), - shareReplay(1) - ) -} - -/** - * Watch location target - * - * @param location$ - Location observable - * - * @returns Location target observable - */ -export function watchLocationTarget( - location$: Observable<URL> -): Observable<HTMLElement> { - return watchLocationHash(location$) - .pipe( - map(id => getOptionalElement(`[id="${id}"]`)!), - filter(el => typeof el !== "undefined") - ) -} diff --git a/src/templates/assets/javascripts/browser/location/index.ts b/src/templates/assets/javascripts/browser/location/index.ts deleted file mode 100644 index d77a5444..00000000 --- a/src/templates/assets/javascripts/browser/location/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./hash" diff --git a/src/templates/assets/javascripts/browser/media/index.ts b/src/templates/assets/javascripts/browser/media/index.ts deleted file mode 100644 index dd7400d4..00000000 --- a/src/templates/assets/javascripts/browser/media/index.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - EMPTY, - Observable, - fromEvent, - fromEventPattern, - map, - merge, - startWith, - switchMap -} from "rxjs" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch media query - * - * Note that although `MediaQueryList.addListener` is deprecated we have to - * use it, because it's the only way to ensure proper downward compatibility. - * - * @see https://bit.ly/3dUBH2m - GitHub issue - * - * @param query - Media query - * - * @returns Media observable - */ -export function watchMedia(query: string): Observable<boolean> { - const media = matchMedia(query) - return fromEventPattern<boolean>(next => ( - media.addListener(() => next(media.matches)) - )) - .pipe( - startWith(media.matches) - ) -} - -/** - * Watch print mode - * - * @returns Print observable - */ -export function watchPrint(): Observable<boolean> { - const media = matchMedia("print") - return merge( - fromEvent(window, "beforeprint").pipe(map(() => true)), - fromEvent(window, "afterprint").pipe(map(() => false)) - ) - .pipe( - startWith(media.matches) - ) -} - -/* ------------------------------------------------------------------------- */ - -/** - * Toggle an observable with a media observable - * - * @template T - Data type - * - * @param query$ - Media observable - * @param factory - Observable factory - * - * @returns Toggled observable - */ -export function at<T>( - query$: Observable<boolean>, factory: () => Observable<T> -): Observable<T> { - return query$ - .pipe( - switchMap(active => active ? factory() : EMPTY) - ) -} diff --git a/src/templates/assets/javascripts/browser/request/index.ts b/src/templates/assets/javascripts/browser/request/index.ts deleted file mode 100644 index 74a56a64..00000000 --- a/src/templates/assets/javascripts/browser/request/index.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - map, - shareReplay, - switchMap -} from "rxjs" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Options - */ -interface Options { - progress$?: Subject<number> // Progress subject -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch the given URL - * - * If the request fails (e.g. when dispatched from `file://` locations), the - * observable will complete without emitting a value. - * - * @param url - Request URL - * @param options - Options - * - * @returns Response observable - */ -export function request( - url: URL | string, options?: Options -): Observable<Blob> { - return new Observable<Blob>(observer => { - const req = new XMLHttpRequest() - req.open("GET", `${url}`) - req.responseType = "blob" - - // Handle response - req.addEventListener("load", () => { - if (req.status >= 200 && req.status < 300) { - observer.next(req.response) - observer.complete() - } else { - observer.error(new Error(req.statusText)) - } - }) - - // Handle network errors - req.addEventListener("error", () => { - observer.error(new Error("Network Error")) - }) - - // Handle aborted requests - req.addEventListener("abort", () => { - observer.error(new Error("Request aborted")) - }) - - // Handle download progress - if (typeof options?.progress$ !== "undefined") { - req.addEventListener("progress", event => { - options.progress$!.next((event.loaded / event.total) * 100) - }) - - // Immediately set progress to 5% to indicate that we're loading - options.progress$.next(5) - } - - // Send request - req.send() - }) -} - -/* ------------------------------------------------------------------------- */ - -/** - * Fetch JSON from the given URL - * - * @template T - Data type - * - * @param url - Request URL - * @param options - Options - * - * @returns Data observable - */ -export function requestJSON<T>( - url: URL | string, options?: Options -): Observable<T> { - return request(url, options) - .pipe( - switchMap(res => res.text()), - map(body => JSON.parse(body) as T), - shareReplay(1) - ) -} - -/** - * Fetch XML from the given URL - * - * @param url - Request URL - * @param options - Options - * - * @returns Data observable - */ -export function requestXML( - url: URL | string, options?: Options -): Observable<Document> { - const dom = new DOMParser() - return request(url, options) - .pipe( - switchMap(res => res.text()), - map(res => dom.parseFromString(res, "text/xml")), - shareReplay(1) - ) -} diff --git a/src/templates/assets/javascripts/browser/script/index.ts b/src/templates/assets/javascripts/browser/script/index.ts deleted file mode 100644 index ef5c89e6..00000000 --- a/src/templates/assets/javascripts/browser/script/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - defer, - finalize, - fromEvent, - map, - merge, - switchMap, - take, - throwError -} from "rxjs" - -import { h } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Create and load a `script` element - * - * This function returns an observable that will emit when the script was - * successfully loaded, or throw an error if it wasn't. - * - * @param src - Script URL - * - * @returns Script observable - */ -export function watchScript(src: string): Observable<void> { - const script = h("script", { src }) - return defer(() => { - document.head.appendChild(script) - return merge( - fromEvent(script, "load"), - fromEvent(script, "error") - .pipe( - switchMap(() => ( - throwError(() => new ReferenceError(`Invalid script: ${src}`)) - )) - ) - ) - .pipe( - map(() => undefined), - finalize(() => document.head.removeChild(script)), - take(1) - ) - }) -} diff --git a/src/templates/assets/javascripts/browser/toggle/index.ts b/src/templates/assets/javascripts/browser/toggle/index.ts deleted file mode 100644 index 0be4b29d..00000000 --- a/src/templates/assets/javascripts/browser/toggle/index.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - fromEvent, - map, - startWith -} from "rxjs" - -import { getElement } from "../element" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Toggle - */ -export type Toggle = - | "drawer" /* Toggle for drawer */ - | "search" /* Toggle for search */ - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Toggle map - */ -const toggles: Record<Toggle, HTMLInputElement> = { - drawer: getElement("[data-md-toggle=drawer]"), - search: getElement("[data-md-toggle=search]") -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve the value of a toggle - * - * @param name - Toggle - * - * @returns Toggle value - */ -export function getToggle(name: Toggle): boolean { - return toggles[name].checked -} - -/** - * Set toggle - * - * Simulating a click event seems to be the most cross-browser compatible way - * of changing the value while also emitting a `change` event. Before, Material - * used `CustomEvent` to programmatically change the value of a toggle, but this - * is a much simpler and cleaner solution which doesn't require a polyfill. - * - * @param name - Toggle - * @param value - Toggle value - */ -export function setToggle(name: Toggle, value: boolean): void { - if (toggles[name].checked !== value) - toggles[name].click() -} - -/* ------------------------------------------------------------------------- */ - -/** - * Watch toggle - * - * @param name - Toggle - * - * @returns Toggle value observable - */ -export function watchToggle(name: Toggle): Observable<boolean> { - const el = toggles[name] - return fromEvent(el, "change") - .pipe( - map(() => el.checked), - startWith(el.checked) - ) -} diff --git a/src/templates/assets/javascripts/browser/viewport/_/index.ts b/src/templates/assets/javascripts/browser/viewport/_/index.ts deleted file mode 100644 index 09c45f32..00000000 --- a/src/templates/assets/javascripts/browser/viewport/_/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - combineLatest, - map, - shareReplay -} from "rxjs" - -import { - ViewportOffset, - watchViewportOffset -} from "../offset" -import { - ViewportSize, - watchViewportSize -} from "../size" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Viewport - */ -export interface Viewport { - offset: ViewportOffset /* Viewport offset */ - size: ViewportSize /* Viewport size */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch viewport - * - * @returns Viewport observable - */ -export function watchViewport(): Observable<Viewport> { - return combineLatest([ - watchViewportOffset(), - watchViewportSize() - ]) - .pipe( - map(([offset, size]) => ({ offset, size })), - shareReplay(1) - ) -} diff --git a/src/templates/assets/javascripts/browser/viewport/at/index.ts b/src/templates/assets/javascripts/browser/viewport/at/index.ts deleted file mode 100644 index 8769cf3b..00000000 --- a/src/templates/assets/javascripts/browser/viewport/at/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - combineLatest, - distinctUntilKeyChanged, - map -} from "rxjs" - -import { Header } from "~/components" - -import { getElementOffset } from "../../element" -import { Viewport } from "../_" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch viewport relative to element - * - * @param el - Element - * @param options - Options - * - * @returns Viewport observable - */ -export function watchViewportAt( - el: HTMLElement, { viewport$, header$ }: WatchOptions -): Observable<Viewport> { - const size$ = viewport$ - .pipe( - distinctUntilKeyChanged("size") - ) - - /* Compute element offset */ - const offset$ = combineLatest([size$, header$]) - .pipe( - map(() => getElementOffset(el)) - ) - - /* Compute relative viewport, return hot observable */ - return combineLatest([header$, viewport$, offset$]) - .pipe( - map(([{ height }, { offset, size }, { x, y }]) => ({ - offset: { - x: offset.x - x, - y: offset.y - y + height - }, - size - })) - ) -} diff --git a/src/templates/assets/javascripts/browser/viewport/index.ts b/src/templates/assets/javascripts/browser/viewport/index.ts deleted file mode 100644 index b3d135e9..00000000 --- a/src/templates/assets/javascripts/browser/viewport/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./at" -export * from "./offset" -export * from "./size" diff --git a/src/templates/assets/javascripts/browser/viewport/offset/index.ts b/src/templates/assets/javascripts/browser/viewport/offset/index.ts deleted file mode 100644 index 63d37dd2..00000000 --- a/src/templates/assets/javascripts/browser/viewport/offset/index.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - fromEvent, - map, - merge, - startWith -} from "rxjs" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Viewport offset - */ -export interface ViewportOffset { - x: number /* Horizontal offset */ - y: number /* Vertical offset */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve viewport offset - * - * On iOS Safari, viewport offset can be negative due to overflow scrolling. - * As this may induce strange behaviors downstream, we'll just limit it to 0. - * - * @returns Viewport offset - */ -export function getViewportOffset(): ViewportOffset { - return { - x: Math.max(0, scrollX), - y: Math.max(0, scrollY) - } -} - -/* ------------------------------------------------------------------------- */ - -/** - * Watch viewport offset - * - * @returns Viewport offset observable - */ -export function watchViewportOffset(): Observable<ViewportOffset> { - return merge( - fromEvent(window, "scroll", { passive: true }), - fromEvent(window, "resize", { passive: true }) - ) - .pipe( - map(getViewportOffset), - startWith(getViewportOffset()) - ) -} diff --git a/src/templates/assets/javascripts/browser/viewport/size/index.ts b/src/templates/assets/javascripts/browser/viewport/size/index.ts deleted file mode 100644 index 06694888..00000000 --- a/src/templates/assets/javascripts/browser/viewport/size/index.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - fromEvent, - map, - startWith -} from "rxjs" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Viewport size - */ -export interface ViewportSize { - width: number /* Viewport width */ - height: number /* Viewport height */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve viewport size - * - * @returns Viewport size - */ -export function getViewportSize(): ViewportSize { - return { - width: innerWidth, - height: innerHeight - } -} - -/* ------------------------------------------------------------------------- */ - -/** - * Watch viewport size - * - * @returns Viewport size observable - */ -export function watchViewportSize(): Observable<ViewportSize> { - return fromEvent(window, "resize", { passive: true }) - .pipe( - map(getViewportSize), - startWith(getViewportSize()) - ) -} diff --git a/src/templates/assets/javascripts/browser/worker/index.ts b/src/templates/assets/javascripts/browser/worker/index.ts deleted file mode 100644 index 12e4e63b..00000000 --- a/src/templates/assets/javascripts/browser/worker/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - endWith, - fromEvent, - ignoreElements, - mergeWith, - share, - takeUntil -} from "rxjs" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Worker message - */ -export interface WorkerMessage { - type: unknown /* Message type */ - data?: unknown /* Message data */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Create an observable for receiving from a web worker - * - * @template T - Data type - * - * @param worker - Web worker - * - * @returns Message observable - */ -function recv<T>(worker: Worker): Observable<T> { - return fromEvent<MessageEvent<T>, T>(worker, "message", ev => ev.data) -} - -/** - * Create a subject for sending to a web worker - * - * @template T - Data type - * - * @param worker - Web worker - * - * @returns Message subject - */ -function send<T>(worker: Worker): Subject<T> { - const send$ = new Subject<T>() - send$.subscribe(data => worker.postMessage(data)) - - /* Return message subject */ - return send$ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Create a bidirectional communication channel to a web worker - * - * @template T - Data type - * - * @param url - Worker URL - * @param worker - Worker - * - * @returns Worker subject - */ -export function watchWorker<T extends WorkerMessage>( - url: string, worker = new Worker(url) -): Subject<T> { - const recv$ = recv<T>(worker) - const send$ = send<T>(worker) - - /* Create worker subject and forward messages */ - const worker$ = new Subject<T>() - worker$.subscribe(send$) - - /* Return worker subject */ - const done$ = send$.pipe(ignoreElements(), endWith(true)) - return worker$ - .pipe( - ignoreElements(), - mergeWith(recv$.pipe(takeUntil(done$))), - share() - ) as Subject<T> -} diff --git a/src/templates/assets/javascripts/bundle.ts b/src/templates/assets/javascripts/bundle.ts deleted file mode 100644 index 141789c9..00000000 --- a/src/templates/assets/javascripts/bundle.ts +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 "focus-visible" - -import { - EMPTY, - NEVER, - Observable, - Subject, - defer, - delay, - filter, - map, - merge, - mergeWith, - shareReplay, - switchMap -} from "rxjs" - -import { configuration, feature } from "./_" -import { - at, - getActiveElement, - getOptionalElement, - requestJSON, - setLocation, - setToggle, - watchDocument, - watchKeyboard, - watchLocation, - watchLocationTarget, - watchMedia, - watchPrint, - watchScript, - watchViewport -} from "./browser" -import { - getComponentElement, - getComponentElements, - mountAnnounce, - mountBackToTop, - mountConsent, - mountContent, - mountDialog, - mountHeader, - mountHeaderTitle, - mountPalette, - mountProgress, - mountSearch, - mountSearchHiglight, - mountSidebar, - mountSource, - mountTableOfContents, - mountTabs, - watchHeader, - watchMain -} from "./components" -import { - SearchIndex, - setupClipboardJS, - setupInstantNavigation, - setupVersionSelector -} from "./integrations" -import { - patchIndeterminate, - patchScrollfix, - patchScrolllock -} from "./patches" -import "./polyfills" - -/* ---------------------------------------------------------------------------- - * Functions - @todo refactor - * ------------------------------------------------------------------------- */ - -/** - * Fetch search index - * - * @returns Search index observable - */ -function fetchSearchIndex(): Observable<SearchIndex> { - if (location.protocol === "file:") { - return watchScript( - `${new URL("search/search_index.js", config.base)}` - ) - .pipe( - // @ts-ignore - @todo fix typings - map(() => __index), - shareReplay(1) - ) - } else { - return requestJSON<SearchIndex>( - new URL("search/search_index.json", config.base) - ) - } -} - -/* ---------------------------------------------------------------------------- - * Application - * ------------------------------------------------------------------------- */ - -/* Yay, JavaScript is available */ -document.documentElement.classList.remove("no-js") -document.documentElement.classList.add("js") - -/* Set up navigation observables and subjects */ -const document$ = watchDocument() -const location$ = watchLocation() -const target$ = watchLocationTarget(location$) -const keyboard$ = watchKeyboard() - -/* Set up media observables */ -const viewport$ = watchViewport() -const tablet$ = watchMedia("(min-width: 960px)") -const screen$ = watchMedia("(min-width: 1220px)") -const print$ = watchPrint() - -/* Retrieve search index, if search is enabled */ -const config = configuration() -const index$ = document.forms.namedItem("search") - ? fetchSearchIndex() - : NEVER - -/* Set up Clipboard.js integration */ -const alert$ = new Subject<string>() -setupClipboardJS({ alert$ }) - -/* Set up progress indicator */ -const progress$ = new Subject<number>() - -/* Set up instant navigation, if enabled */ -if (feature("navigation.instant")) - setupInstantNavigation({ location$, viewport$, progress$ }) - .subscribe(document$) - -/* Set up version selector */ -if (config.version?.provider === "mike") - setupVersionSelector({ document$ }) - -/* Always close drawer and search on navigation */ -merge(location$, target$) - .pipe( - delay(125) - ) - .subscribe(() => { - setToggle("drawer", false) - setToggle("search", false) - }) - -/* Set up global keyboard handlers */ -keyboard$ - .pipe( - filter(({ mode }) => mode === "global") - ) - .subscribe(key => { - switch (key.type) { - - /* Go to previous page */ - case "p": - case ",": - const prev = getOptionalElement<HTMLLinkElement>("link[rel=prev]") - if (typeof prev !== "undefined") - setLocation(prev) - break - - /* Go to next page */ - case "n": - case ".": - const next = getOptionalElement<HTMLLinkElement>("link[rel=next]") - if (typeof next !== "undefined") - setLocation(next) - break - - /* Expand navigation, see https://bit.ly/3ZjG5io */ - case "Enter": - const active = getActiveElement() - if (active instanceof HTMLLabelElement) - active.click() - } - }) - -/* Set up patches */ -patchIndeterminate({ document$, tablet$ }) -patchScrollfix({ document$ }) -patchScrolllock({ viewport$, tablet$ }) - -/* Set up header and main area observable */ -const header$ = watchHeader(getComponentElement("header"), { viewport$ }) -const main$ = document$ - .pipe( - map(() => getComponentElement("main")), - switchMap(el => watchMain(el, { viewport$, header$ })), - shareReplay(1) - ) - -/* Set up control component observables */ -const control$ = merge( - - /* Consent */ - ...getComponentElements("consent") - .map(el => mountConsent(el, { target$ })), - - /* Dialog */ - ...getComponentElements("dialog") - .map(el => mountDialog(el, { alert$ })), - - /* Header */ - ...getComponentElements("header") - .map(el => mountHeader(el, { viewport$, header$, main$ })), - - /* Color palette */ - ...getComponentElements("palette") - .map(el => mountPalette(el)), - - /* Progress bar */ - ...getComponentElements("progress") - .map(el => mountProgress(el, { progress$ })), - - /* Search */ - ...getComponentElements("search") - .map(el => mountSearch(el, { index$, keyboard$ })), - - /* Repository information */ - ...getComponentElements("source") - .map(el => mountSource(el)) -) - -/* Set up content component observables */ -const content$ = defer(() => merge( - - /* Announcement bar */ - ...getComponentElements("announce") - .map(el => mountAnnounce(el)), - - /* Content */ - ...getComponentElements("content") - .map(el => mountContent(el, { viewport$, target$, print$ })), - - /* Search highlighting */ - ...getComponentElements("content") - .map(el => feature("search.highlight") - ? mountSearchHiglight(el, { index$, location$ }) - : EMPTY - ), - - /* Header title */ - ...getComponentElements("header-title") - .map(el => mountHeaderTitle(el, { viewport$, header$ })), - - /* Sidebar */ - ...getComponentElements("sidebar") - .map(el => el.getAttribute("data-md-type") === "navigation" - ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ })) - : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ })) - ), - - /* Navigation tabs */ - ...getComponentElements("tabs") - .map(el => mountTabs(el, { viewport$, header$ })), - - /* Table of contents */ - ...getComponentElements("toc") - .map(el => mountTableOfContents(el, { - viewport$, header$, main$, target$ - })), - - /* Back-to-top button */ - ...getComponentElements("top") - .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ })) -)) - -/* Set up component observables */ -const component$ = document$ - .pipe( - switchMap(() => content$), - mergeWith(control$), - shareReplay(1) - ) - -/* Subscribe to all components */ -component$.subscribe() - -/* ---------------------------------------------------------------------------- - * Exports - * ------------------------------------------------------------------------- */ - -window.document$ = document$ /* Document observable */ -window.location$ = location$ /* Location subject */ -window.target$ = target$ /* Location target observable */ -window.keyboard$ = keyboard$ /* Keyboard observable */ -window.viewport$ = viewport$ /* Viewport observable */ -window.tablet$ = tablet$ /* Media tablet observable */ -window.screen$ = screen$ /* Media screen observable */ -window.print$ = print$ /* Media print observable */ -window.alert$ = alert$ /* Alert subject */ -window.progress$ = progress$ /* Progress indicator subject */ -window.component$ = component$ /* Component observable */ diff --git a/src/templates/assets/javascripts/components/_/index.ts b/src/templates/assets/javascripts/components/_/index.ts deleted file mode 100644 index 61c471d9..00000000 --- a/src/templates/assets/javascripts/components/_/index.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { getElement, getElements } from "~/browser" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Component type - */ -export type ComponentType = - | "announce" /* Announcement bar */ - | "container" /* Container */ - | "consent" /* Consent */ - | "content" /* Content */ - | "dialog" /* Dialog */ - | "header" /* Header */ - | "header-title" /* Header title */ - | "header-topic" /* Header topic */ - | "main" /* Main area */ - | "outdated" /* Version warning */ - | "palette" /* Color palette */ - | "progress" /* Progress indicator */ - | "search" /* Search */ - | "search-query" /* Search input */ - | "search-result" /* Search results */ - | "search-share" /* Search sharing */ - | "search-suggest" /* Search suggestions */ - | "sidebar" /* Sidebar */ - | "skip" /* Skip link */ - | "source" /* Repository information */ - | "tabs" /* Navigation tabs */ - | "toc" /* Table of contents */ - | "top" /* Back-to-top button */ - -/** - * Component - * - * @template T - Component type - * @template U - Reference type - */ -export type Component< - T extends {} = {}, - U extends HTMLElement = HTMLElement -> = - T & { - ref: U /* Component reference */ - } - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Component type map - */ -interface ComponentTypeMap { - "announce": HTMLElement /* Announcement bar */ - "container": HTMLElement /* Container */ - "consent": HTMLElement /* Consent */ - "content": HTMLElement /* Content */ - "dialog": HTMLElement /* Dialog */ - "header": HTMLElement /* Header */ - "header-title": HTMLElement /* Header title */ - "header-topic": HTMLElement /* Header topic */ - "main": HTMLElement /* Main area */ - "outdated": HTMLElement /* Version warning */ - "palette": HTMLElement /* Color palette */ - "progress": HTMLElement /* Progress indicator */ - "search": HTMLElement /* Search */ - "search-query": HTMLInputElement /* Search input */ - "search-result": HTMLElement /* Search results */ - "search-share": HTMLAnchorElement /* Search sharing */ - "search-suggest": HTMLElement /* Search suggestions */ - "sidebar": HTMLElement /* Sidebar */ - "skip": HTMLAnchorElement /* Skip link */ - "source": HTMLAnchorElement /* Repository information */ - "tabs": HTMLElement /* Navigation tabs */ - "toc": HTMLElement /* Table of contents */ - "top": HTMLAnchorElement /* Back-to-top button */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve the element for a given component or throw a reference error - * - * @template T - Component type - * - * @param type - Component type - * @param node - Node of reference - * - * @returns Element - */ -export function getComponentElement<T extends ComponentType>( - type: T, node: ParentNode = document -): ComponentTypeMap[T] { - return getElement(`[data-md-component=${type}]`, node) -} - -/** - * Retrieve all elements for a given component - * - * @template T - Component type - * - * @param type - Component type - * @param node - Node of reference - * - * @returns Elements - */ -export function getComponentElements<T extends ComponentType>( - type: T, node: ParentNode = document -): ComponentTypeMap[T][] { - return getElements(`[data-md-component=${type}]`, node) -} diff --git a/src/templates/assets/javascripts/components/announce/index.ts b/src/templates/assets/javascripts/components/announce/index.ts deleted file mode 100644 index dd04b4ff..00000000 --- a/src/templates/assets/javascripts/components/announce/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - EMPTY, - Observable, - Subject, - defer, - finalize, - fromEvent, - map, - tap -} from "rxjs" - -import { feature } from "~/_" -import { getElement } from "~/browser" - -import { Component } from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Announcement bar - */ -export interface Announce { - hash: number /* Content hash */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch announcement bar - * - * @param el - Announcement bar element - * - * @returns Announcement bar observable - */ -export function watchAnnounce( - el: HTMLElement -): Observable<Announce> { - const button = getElement(".md-typeset > :first-child", el) - return fromEvent(button, "click", { once: true }) - .pipe( - map(() => getElement(".md-typeset", el)), - map(content => ({ hash: __md_hash(content.innerHTML) })) - ) -} - -/** - * Mount announcement bar - * - * @param el - Announcement bar element - * - * @returns Announcement bar component observable - */ -export function mountAnnounce( - el: HTMLElement -): Observable<Component<Announce>> { - if (!feature("announce.dismiss") || !el.childElementCount) - return EMPTY - - /* Support instant navigation - see https://t.ly/3FTme */ - if (!el.hidden) { - const content = getElement(".md-typeset", el) - if (__md_hash(content.innerHTML) === __md_get("__announce")) - el.hidden = true - } - - /* Mount component on subscription */ - return defer(() => { - const push$ = new Subject<Announce>() - push$.subscribe(({ hash }) => { - el.hidden = true - - /* Persist preference in local storage */ - __md_set<number>("__announce", hash) - }) - - /* Create and return component */ - return watchAnnounce(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/consent/index.ts b/src/templates/assets/javascripts/components/consent/index.ts deleted file mode 100644 index bc99db58..00000000 --- a/src/templates/assets/javascripts/components/consent/index.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - finalize, - map, - tap -} from "rxjs" - -import { Component } from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Consent - */ -export interface Consent { - hidden: boolean /* Consent is hidden */ -} - -/** - * Consent defaults - */ -export interface ConsentDefaults { - analytics?: boolean /* Consent for Analytics */ - github?: boolean /* Consent for GitHub */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - target$: Observable<HTMLElement> /* Target observable */ -} - -/** - * Mount options - */ -interface MountOptions { - target$: Observable<HTMLElement> /* Target observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch consent - * - * @param el - Consent element - * @param options - Options - * - * @returns Consent observable - */ -export function watchConsent( - el: HTMLElement, { target$ }: WatchOptions -): Observable<Consent> { - return target$ - .pipe( - map(target => ({ hidden: target !== el })) - ) -} - -/* ------------------------------------------------------------------------- */ - -/** - * Mount consent - * - * @param el - Consent element - * @param options - Options - * - * @returns Consent component observable - */ -export function mountConsent( - el: HTMLElement, options: MountOptions -): Observable<Component<Consent>> { - const internal$ = new Subject<Consent>() - internal$.subscribe(({ hidden }) => { - el.hidden = hidden - }) - - /* Create and return component */ - return watchConsent(el, options) - .pipe( - tap(state => internal$.next(state)), - finalize(() => internal$.complete()), - map(state => ({ ref: el, ...state })) - ) -} diff --git a/src/templates/assets/javascripts/components/content/_/index.ts b/src/templates/assets/javascripts/components/content/_/index.ts deleted file mode 100644 index 899a695c..00000000 --- a/src/templates/assets/javascripts/components/content/_/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, merge } from "rxjs" - -import { Viewport, getElements } from "~/browser" - -import { Component } from "../../_" -import { - Annotation, - mountAnnotationBlock -} from "../annotation" -import { - CodeBlock, - mountCodeBlock -} from "../code" -import { - Details, - mountDetails -} from "../details" -import { - Mermaid, - mountMermaid -} from "../mermaid" -import { - DataTable, - mountDataTable -} from "../table" -import { - ContentTabs, - mountContentTabs -} from "../tabs" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Content - */ -export type Content = - | Annotation - | CodeBlock - | ContentTabs - | DataTable - | Details - | Mermaid - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - target$: Observable<HTMLElement> /* Location target observable */ - print$: Observable<boolean> /* Media print observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount content - * - * This function mounts all components that are found in the content of the - * actual article, including code blocks, data tables and details. - * - * @param el - Content element - * @param options - Options - * - * @returns Content component observable - */ -export function mountContent( - el: HTMLElement, { viewport$, target$, print$ }: MountOptions -): Observable<Component<Content>> { - return merge( - - /* Annotations */ - ...getElements(".annotate:not(.highlight)", el) - .map(child => mountAnnotationBlock(child, { target$, print$ })), - - /* Code blocks */ - ...getElements("pre:not(.mermaid) > code", el) - .map(child => mountCodeBlock(child, { target$, print$ })), - - /* Mermaid diagrams */ - ...getElements("pre.mermaid", el) - .map(child => mountMermaid(child)), - - /* Data tables */ - ...getElements("table:not([class])", el) - .map(child => mountDataTable(child)), - - /* Details */ - ...getElements("details", el) - .map(child => mountDetails(child, { target$, print$ })), - - /* Content tabs */ - ...getElements("[data-tabs]", el) - .map(child => mountContentTabs(child, { viewport$ })) - ) -} diff --git a/src/templates/assets/javascripts/components/content/annotation/_/index.ts b/src/templates/assets/javascripts/components/content/annotation/_/index.ts deleted file mode 100644 index c5138fa4..00000000 --- a/src/templates/assets/javascripts/components/content/annotation/_/index.ts +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - animationFrameScheduler, - auditTime, - combineLatest, - debounceTime, - defer, - delay, - endWith, - filter, - finalize, - fromEvent, - ignoreElements, - map, - merge, - switchMap, - take, - takeUntil, - tap, - throttleTime, - withLatestFrom -} from "rxjs" - -import { - ElementOffset, - getActiveElement, - getElementSize, - watchElementContentOffset, - watchElementFocus, - watchElementOffset, - watchElementVisibility -} from "~/browser" - -import { Component } from "../../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Annotation - */ -export interface Annotation { - active: boolean /* Annotation is active */ - offset: ElementOffset /* Annotation offset */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - target$: Observable<HTMLElement> /* Location target observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch annotation - * - * @param el - Annotation element - * @param container - Containing element - * - * @returns Annotation observable - */ -export function watchAnnotation( - el: HTMLElement, container: HTMLElement -): Observable<Annotation> { - const offset$ = defer(() => combineLatest([ - watchElementOffset(el), - watchElementContentOffset(container) - ])) - .pipe( - map(([{ x, y }, scroll]): ElementOffset => { - const { width, height } = getElementSize(el) - return ({ - x: x - scroll.x + width / 2, - y: y - scroll.y + height / 2 - }) - }) - ) - - /* Actively watch annotation on focus */ - return watchElementFocus(el) - .pipe( - switchMap(active => offset$ - .pipe( - map(offset => ({ active, offset })), - take(+!active || Infinity) - ) - ) - ) -} - -/** - * Mount annotation - * - * @param el - Annotation element - * @param container - Containing element - * @param options - Options - * - * @returns Annotation component observable - */ -export function mountAnnotation( - el: HTMLElement, container: HTMLElement, { target$ }: MountOptions -): Observable<Component<Annotation>> { - const [tooltip, index] = Array.from(el.children) - - /* Mount component on subscription */ - return defer(() => { - const push$ = new Subject<Annotation>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$.subscribe({ - - /* Handle emission */ - next({ offset }) { - el.style.setProperty("--md-tooltip-x", `${offset.x}px`) - el.style.setProperty("--md-tooltip-y", `${offset.y}px`) - }, - - /* Handle complete */ - complete() { - el.style.removeProperty("--md-tooltip-x") - el.style.removeProperty("--md-tooltip-y") - } - }) - - /* Start animation only when annotation is visible */ - watchElementVisibility(el) - .pipe( - takeUntil(done$) - ) - .subscribe(visible => { - el.toggleAttribute("data-md-visible", visible) - }) - - /* Toggle tooltip presence to mitigate empty lines when copying */ - merge( - push$.pipe(filter(({ active }) => active)), - push$.pipe(debounceTime(250), filter(({ active }) => !active)) - ) - .subscribe({ - - /* Handle emission */ - next({ active }) { - if (active) - el.prepend(tooltip) - else - tooltip.remove() - }, - - /* Handle complete */ - complete() { - el.prepend(tooltip) - } - }) - - /* Toggle tooltip visibility */ - push$ - .pipe( - auditTime(16, animationFrameScheduler) - ) - .subscribe(({ active }) => { - tooltip.classList.toggle("md-tooltip--active", active) - }) - - /* Track relative origin of tooltip */ - push$ - .pipe( - throttleTime(125, animationFrameScheduler), - filter(() => !!el.offsetParent), - map(() => el.offsetParent!.getBoundingClientRect()), - map(({ x }) => x) - ) - .subscribe({ - - /* Handle emission */ - next(origin) { - if (origin) - el.style.setProperty("--md-tooltip-0", `${-origin}px`) - else - el.style.removeProperty("--md-tooltip-0") - }, - - /* Handle complete */ - complete() { - el.style.removeProperty("--md-tooltip-0") - } - }) - - /* Allow to copy link without scrolling to anchor */ - fromEvent<MouseEvent>(index, "click") - .pipe( - takeUntil(done$), - filter(ev => !(ev.metaKey || ev.ctrlKey)) - ) - .subscribe(ev => { - ev.stopPropagation() - ev.preventDefault() - }) - - /* Allow to open link in new tab or blur on close */ - fromEvent<MouseEvent>(index, "mousedown") - .pipe( - takeUntil(done$), - withLatestFrom(push$) - ) - .subscribe(([ev, { active }]) => { - - /* Open in new tab */ - if (ev.button !== 0 || ev.metaKey || ev.ctrlKey) { - ev.preventDefault() - - /* Close annotation */ - } else if (active) { - ev.preventDefault() - - /* Focus parent annotation, if any */ - const parent = el.parentElement!.closest(".md-annotation") - if (parent instanceof HTMLElement) - parent.focus() - else - getActiveElement()?.blur() - } - }) - - /* Open and focus annotation on location target */ - target$ - .pipe( - takeUntil(done$), - filter(target => target === tooltip), - delay(125) - ) - .subscribe(() => el.focus()) - - /* Create and return component */ - return watchAnnotation(el, container) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/content/annotation/block/index.ts b/src/templates/assets/javascripts/components/content/annotation/block/index.ts deleted file mode 100644 index c73b01fa..00000000 --- a/src/templates/assets/javascripts/components/content/annotation/block/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { EMPTY, Observable, defer } from "rxjs" - -import { Component } from "../../../_" -import { Annotation } from "../_" -import { mountAnnotationList } from "../list" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - target$: Observable<HTMLElement> /* Location target observable */ - print$: Observable<boolean> /* Media print observable */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Find list element directly following a block - * - * @param el - Annotation block element - * - * @returns List element or nothing - */ -function findList(el: HTMLElement): HTMLElement | undefined { - if (el.nextElementSibling) { - const sibling = el.nextElementSibling as HTMLElement - if (sibling.tagName === "OL") - return sibling - - /* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */ - else if (sibling.tagName === "P" && !sibling.children.length) - return findList(sibling) - } - - /* Everything else */ - return undefined -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount annotation block - * - * @param el - Annotation block element - * @param options - Options - * - * @returns Annotation component observable - */ -export function mountAnnotationBlock( - el: HTMLElement, options: MountOptions -): Observable<Component<Annotation>> { - return defer(() => { - const list = findList(el) - return typeof list !== "undefined" - ? mountAnnotationList(list, el, options) - : EMPTY - }) -} diff --git a/src/templates/assets/javascripts/components/content/annotation/index.ts b/src/templates/assets/javascripts/components/content/annotation/index.ts deleted file mode 100644 index c593b723..00000000 --- a/src/templates/assets/javascripts/components/content/annotation/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./block" -export * from "./list" diff --git a/src/templates/assets/javascripts/components/content/annotation/list/index.ts b/src/templates/assets/javascripts/components/content/annotation/list/index.ts deleted file mode 100644 index 725dd583..00000000 --- a/src/templates/assets/javascripts/components/content/annotation/list/index.ts +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - EMPTY, - Observable, - Subject, - defer, - endWith, - finalize, - ignoreElements, - merge, - share, - takeUntil -} from "rxjs" - -import { - getElement, - getElements, - getOptionalElement -} from "~/browser" -import { renderAnnotation } from "~/templates" - -import { Component } from "../../../_" -import { - Annotation, - mountAnnotation -} from "../_" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - target$: Observable<HTMLElement> /* Location target observable */ - print$: Observable<boolean> /* Media print observable */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Find all annotation hosts in the containing element - * - * @param container - Containing element - * - * @returns Annotation hosts - */ -function findHosts(container: HTMLElement): HTMLElement[] { - return container.tagName === "CODE" - ? getElements(".c, .c1, .cm", container) - : [container] -} - -/** - * Find all annotation markers in the containing element - * - * @param container - Containing element - * - * @returns Annotation markers - */ -function findMarkers(container: HTMLElement): Text[] { - const markers: Text[] = [] - for (const el of findHosts(container)) { - const nodes: Text[] = [] - - /* Find all text nodes in current element */ - const it = document.createNodeIterator(el, NodeFilter.SHOW_TEXT) - for (let node = it.nextNode(); node; node = it.nextNode()) - nodes.push(node as Text) - - /* Find all markers in each text node */ - for (let text of nodes) { - let match: RegExpExecArray | null - - /* Split text at marker and add to list */ - while ((match = /(\(\d+\))(!)?/.exec(text.textContent!))) { - const [, id, force] = match - if (typeof force === "undefined") { - const marker = text.splitText(match.index) - text = marker.splitText(id.length) - markers.push(marker) - - /* Replace entire text with marker */ - } else { - text.textContent = id - markers.push(text) - break - } - } - } - } - return markers -} - -/** - * Swap the child nodes of two elements - * - * @param source - Source element - * @param target - Target element - */ -function swap(source: HTMLElement, target: HTMLElement): void { - target.append(...Array.from(source.childNodes)) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount annotation list - * - * This function analyzes the containing code block and checks for markers - * referring to elements in the given annotation list. If no markers are found, - * the list is left untouched. Otherwise, list elements are rendered as - * annotations inside the code block. - * - * @param el - Annotation list element - * @param container - Containing element - * @param options - Options - * - * @returns Annotation component observable - */ -export function mountAnnotationList( - el: HTMLElement, container: HTMLElement, { target$, print$ }: MountOptions -): Observable<Component<Annotation>> { - - /* Compute prefix for tooltip anchors */ - const parent = container.closest("[id]") - const prefix = parent?.id - - /* Find and replace all markers with empty annotations */ - const annotations = new Map<string, HTMLElement>() - for (const marker of findMarkers(container)) { - const [, id] = marker.textContent!.match(/\((\d+)\)/)! - if (getOptionalElement(`:scope > li:nth-child(${id})`, el)) { - annotations.set(id, renderAnnotation(id, prefix)) - marker.replaceWith(annotations.get(id)!) - } - } - - /* Keep list if there are no annotations to render */ - if (annotations.size === 0) - return EMPTY - - /* Mount component on subscription */ - return defer(() => { - const push$ = new Subject() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - - /* Retrieve container pairs for swapping */ - const pairs: [HTMLElement, HTMLElement][] = [] - for (const [id, annotation] of annotations) - pairs.push([ - getElement(".md-typeset", annotation), - getElement(`:scope > li:nth-child(${id})`, el) - ]) - - /* Handle print mode - see https://bit.ly/3rgPdpt */ - print$.pipe(takeUntil(done$)) - .subscribe(active => { - el.hidden = !active - - /* Add class to discern list element */ - el.classList.toggle("md-annotation-list", active) - - /* Show annotations in code block or list (print) */ - for (const [inner, child] of pairs) - if (!active) - swap(child, inner) - else - swap(inner, child) - }) - - /* Create and return component */ - return merge(...[...annotations] - .map(([, annotation]) => ( - mountAnnotation(annotation, container, { target$ }) - )) - ) - .pipe( - finalize(() => push$.complete()), - share() - ) - }) -} diff --git a/src/templates/assets/javascripts/components/content/code/_/index.ts b/src/templates/assets/javascripts/components/content/code/_/index.ts deleted file mode 100644 index ccc09339..00000000 --- a/src/templates/assets/javascripts/components/content/code/_/index.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 ClipboardJS from "clipboard" -import { - EMPTY, - Observable, - Subject, - defer, - distinctUntilChanged, - distinctUntilKeyChanged, - filter, - finalize, - map, - mergeWith, - switchMap, - take, - tap -} from "rxjs" - -import { feature } from "~/_" -import { - getElementContentSize, - watchElementSize, - watchElementVisibility -} from "~/browser" -import { renderClipboardButton } from "~/templates" - -import { Component } from "../../../_" -import { - Annotation, - mountAnnotationList -} from "../../annotation" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Code block - */ -export interface CodeBlock { - scrollable: boolean /* Code block overflows */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - target$: Observable<HTMLElement> /* Location target observable */ - print$: Observable<boolean> /* Media print observable */ -} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Global sequence number for code blocks - */ -let sequence = 0 - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Find candidate list element directly following a code block - * - * @param el - Code block element - * - * @returns List element or nothing - */ -function findCandidateList(el: HTMLElement): HTMLElement | undefined { - if (el.nextElementSibling) { - const sibling = el.nextElementSibling as HTMLElement - if (sibling.tagName === "OL") - return sibling - - /* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */ - else if (sibling.tagName === "P" && !sibling.children.length) - return findCandidateList(sibling) - } - - /* Everything else */ - return undefined -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch code block - * - * 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 - * - * @returns Code block observable - */ -export function watchCodeBlock( - el: HTMLElement -): Observable<CodeBlock> { - return watchElementSize(el) - .pipe( - map(({ width }) => { - const content = getElementContentSize(el) - return { - scrollable: content.width > width - } - }), - distinctUntilKeyChanged("scrollable") - ) -} - -/** - * Mount code block - * - * This function ensures that an overflowing code block is focusable through - * keyboard, so it can be scrolled without a mouse to improve on accessibility. - * Furthermore, if code annotations are enabled, they are mounted if and only - * if the code block is currently visible, e.g., not in a hidden content tab. - * - * Note that code blocks may be mounted eagerly or lazily. If they're mounted - * lazily (on first visibility), code annotation anchor links will not work, - * as they are evaluated on initial page load, and code annotations in general - * might feel a little bumpier. - * - * @param el - Code block element - * @param options - Options - * - * @returns Code block and annotation component observable - */ -export function mountCodeBlock( - el: HTMLElement, options: MountOptions -): Observable<Component<CodeBlock | Annotation>> { - const { matches: hover } = matchMedia("(hover)") - - /* Defer mounting of code block - see https://bit.ly/3vHVoVD */ - const factory$ = defer(() => { - const push$ = new Subject<CodeBlock>() - push$.subscribe(({ scrollable }) => { - if (scrollable && hover) - el.setAttribute("tabindex", "0") - else - el.removeAttribute("tabindex") - }) - - /* Render button for Clipboard.js integration */ - if (ClipboardJS.isSupported()) { - if (el.closest(".copy") || ( - feature("content.code.copy") && !el.closest(".no-copy") - )) { - const parent = el.closest("pre")! - parent.id = `__code_${sequence++}` - parent.insertBefore( - renderClipboardButton(parent.id), - el - ) - } - } - - /* Handle code annotations */ - const container = el.closest(".highlight") - if (container instanceof HTMLElement) { - const list = findCandidateList(container) - - /* Mount code annotations, if enabled */ - if (typeof list !== "undefined" && ( - container.classList.contains("annotate") || - feature("content.code.annotate") - )) { - const annotations$ = mountAnnotationList(list, el, options) - - /* Create and return component */ - return watchCodeBlock(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })), - mergeWith( - watchElementSize(container) - .pipe( - map(({ width, height }) => width && height), - distinctUntilChanged(), - switchMap(active => active ? annotations$ : EMPTY) - ) - ) - ) - } - } - - /* Create and return component */ - return watchCodeBlock(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) - - /* Mount code block lazily */ - if (feature("content.lazy")) - return watchElementVisibility(el) - .pipe( - filter(visible => visible), - take(1), - switchMap(() => factory$) - ) - - /* Mount code block */ - return factory$ -} diff --git a/src/templates/assets/javascripts/components/content/code/index.ts b/src/templates/assets/javascripts/components/content/code/index.ts deleted file mode 100644 index 3f86e2b4..00000000 --- a/src/templates/assets/javascripts/components/content/code/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" diff --git a/src/templates/assets/javascripts/components/content/details/index.ts b/src/templates/assets/javascripts/components/content/details/index.ts deleted file mode 100644 index 17bfae45..00000000 --- a/src/templates/assets/javascripts/components/content/details/index.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - defer, - filter, - finalize, - map, - merge, - tap -} from "rxjs" - -import { Component } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Details - */ -export interface Details { - action: "open" | "close" /* Details state */ - reveal?: boolean /* Details is revealed */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - target$: Observable<HTMLElement> /* Location target observable */ - print$: Observable<boolean> /* Media print observable */ -} - -/** - * Mount options - */ -interface MountOptions { - target$: Observable<HTMLElement> /* Location target observable */ - print$: Observable<boolean> /* Media print observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch details - * - * @param el - Details element - * @param options - Options - * - * @returns Details observable - */ -export function watchDetails( - el: HTMLDetailsElement, { target$, print$ }: WatchOptions -): Observable<Details> { - let open = true - return merge( - - /* Open and focus details on location target */ - target$ - .pipe( - map(target => target.closest("details:not([open])")!), - filter(details => el === details), - map(() => ({ - action: "open", reveal: true - }) as Details) - ), - - /* Open details on print and close afterwards */ - print$ - .pipe( - filter(active => active || !open), - tap(() => open = el.open), - map(active => ({ - action: active ? "open" : "close" - }) as Details) - ) - ) -} - -/** - * Mount details - * - * This function ensures that `details` tags are opened on anchor jumps and - * prior to printing, so the whole content of the page is visible. - * - * @param el - Details element - * @param options - Options - * - * @returns Details component observable - */ -export function mountDetails( - el: HTMLDetailsElement, options: MountOptions -): Observable<Component<Details>> { - return defer(() => { - const push$ = new Subject<Details>() - push$.subscribe(({ action, reveal }) => { - el.toggleAttribute("open", action === "open") - if (reveal) - el.scrollIntoView() - }) - - /* Create and return component */ - return watchDetails(el, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/content/index.ts b/src/templates/assets/javascripts/components/content/index.ts deleted file mode 100644 index a29d8b41..00000000 --- a/src/templates/assets/javascripts/components/content/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./annotation" -export * from "./code" -export * from "./details" -export * from "./table" -export * from "./tabs" diff --git a/src/templates/assets/javascripts/components/content/mermaid/index.css b/src/templates/assets/javascripts/components/content/mermaid/index.css deleted file mode 100644 index 3092b8ec..00000000 --- a/src/templates/assets/javascripts/components/content/mermaid/index.css +++ /dev/null @@ -1,430 +0,0 @@ -/* - * Copyright (c) 2016-2023 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. - */ - -/* ---------------------------------------------------------------------------- - * Rules: general - * ------------------------------------------------------------------------- */ - -/* General node */ -.node circle, -.node ellipse, -.node path, -.node polygon, -.node rect { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* General marker */ -marker { - fill: var(--md-mermaid-edge-color) !important; -} - -/* General edge label */ -.edgeLabel .label rect { - fill: transparent; -} - -/* ---------------------------------------------------------------------------- - * Rules: flowcharts - * ------------------------------------------------------------------------- */ - -/* Flowchart node label */ -.label { - color: var(--md-mermaid-label-fg-color); - font-family: var(--md-mermaid-font-family); -} - -/* Flowchart node label container */ -.label foreignObject { - overflow: visible; - line-height: initial; -} - -/* Flowchart edge label in node label */ -.label div .edgeLabel { - color: var(--md-mermaid-label-fg-color); - background-color: var(--md-mermaid-label-bg-color); -} - -/* Flowchart edge label */ -.edgeLabel, -.edgeLabel rect { - color: var(--md-mermaid-edge-color); - background-color: var(--md-mermaid-label-bg-color); - fill: var(--md-mermaid-label-bg-color); -} - -/* Flowchart edge path */ -.edgePath .path, -.flowchart-link { - stroke: var(--md-mermaid-edge-color); - stroke-width: .05rem; -} - -/* Flowchart arrow head */ -.edgePath .arrowheadPath { - fill: var(--md-mermaid-edge-color); - stroke: none; -} - -/* Flowchart subgraph */ -.cluster rect { - fill: var(--md-default-fg-color--lightest); - stroke: var(--md-default-fg-color--lighter); -} - -/* Flowchart subgraph labels */ -.cluster span { - color: var(--md-mermaid-label-fg-color); - font-family: var(--md-mermaid-font-family); -} - -/* Flowchart markers */ -g #flowchart-circleStart, -g #flowchart-circleEnd, -g #flowchart-crossStart, -g #flowchart-crossEnd, -g #flowchart-pointStart, -g #flowchart-pointEnd { - stroke: none; -} - -/* ---------------------------------------------------------------------------- - * Rules: class diagrams - * ------------------------------------------------------------------------- */ - -/* Class group node */ -g.classGroup line, -g.classGroup rect { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* Class group node text */ -g.classGroup text { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color); -} - -/* Class label box */ -.classLabel .box { - background-color: var(--md-mermaid-label-bg-color); - opacity: 1; - fill: var(--md-mermaid-label-bg-color); -} - -/* Class label text */ -.classLabel .label { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color); -} - -/* Class group divider */ -.node .divider { - stroke: var(--md-mermaid-node-fg-color); -} - -/* Class relation */ -.relation { - stroke: var(--md-mermaid-edge-color); -} - -/* Class relation cardinality */ -.cardinality { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color); -} - -/* Class relation cardinality text */ -.cardinality text { - fill: inherit !important; -} - -/* Class extension, composition and dependency marker */ -defs #classDiagram-extensionStart, -defs #classDiagram-extensionEnd, -defs #classDiagram-compositionStart, -defs #classDiagram-compositionEnd, -defs #classDiagram-dependencyStart, -defs #classDiagram-dependencyEnd { - fill: var(--md-mermaid-edge-color) !important; - stroke: var(--md-mermaid-edge-color) !important; -} - -/* Class aggregation marker */ -defs #classDiagram-aggregationStart, -defs #classDiagram-aggregationEnd { - fill: var(--md-mermaid-label-bg-color) !important; - stroke: var(--md-mermaid-edge-color) !important; -} - -/* ---------------------------------------------------------------------------- - * Rules: state diagrams - * ------------------------------------------------------------------------- */ - -/* State group node */ -g.stateGroup rect { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* State group title */ -g.stateGroup .state-title { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color) !important; -} - -/* State group background */ -g.stateGroup .composit { - fill: var(--md-mermaid-label-bg-color); -} - -/* State node label */ -.nodeLabel { - color: var(--md-mermaid-label-fg-color); - font-family: var(--md-mermaid-font-family); -} - -/* State start and end marker */ -.start-state, -.node circle.state-start, -.node circle.state-end { - fill: var(--md-mermaid-edge-color); - stroke: none; -} - -/* State end marker */ -.end-state-outer, -.end-state-inner { - fill: var(--md-mermaid-edge-color); -} - -/* State end marker */ -.end-state-inner, -.node circle.state-end { - stroke: var(--md-mermaid-label-bg-color); -} - -/* State transition */ -.transition { - stroke: var(--md-mermaid-edge-color); -} - -/* State fork and join */ -[id^=state-fork] rect, -[id^=state-join] rect { - fill: var(--md-mermaid-edge-color) !important; - stroke: none !important; -} - -/* State cluster (yes, 2x... Mermaid WTF) */ -.statediagram-cluster.statediagram-cluster .inner { - fill: var(--md-default-bg-color); -} - -/* State cluster node */ -.statediagram-cluster rect { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* State cluster divider */ -.statediagram-state rect.divider { - fill: var(--md-default-fg-color--lightest); - stroke: var(--md-default-fg-color--lighter); -} - -/* State diagram markers */ -defs #statediagram-barbEnd { - stroke: var(--md-mermaid-edge-color); -} - -/* ---------------------------------------------------------------------------- - * Rules: entity-relationship diagrams - * ------------------------------------------------------------------------- */ - -/* Attribute box */ -.attributeBoxEven, -.attributeBoxOdd { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* Entity node */ -.entityBox { - fill: var(--md-mermaid-label-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* Entity node label */ -.entityLabel { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color); -} - -/* Entity relationship label container */ -.relationshipLabelBox { - background-color: var(--md-mermaid-label-bg-color); - opacity: 1; - fill: var(--md-mermaid-label-bg-color); - fill-opacity: 1; -} - -/* Entity relationship label */ -.relationshipLabel { - fill: var(--md-mermaid-label-fg-color); -} - -/* Entity relationship line { */ -.relationshipLine { - stroke: var(--md-mermaid-edge-color); -} - -/* Entity relationship line markers */ -defs #ZERO_OR_ONE_START *, -defs #ZERO_OR_ONE_END *, -defs #ZERO_OR_MORE_START *, -defs #ZERO_OR_MORE_END *, -defs #ONLY_ONE_START *, -defs #ONLY_ONE_END *, -defs #ONE_OR_MORE_START *, -defs #ONE_OR_MORE_END * { - stroke: var(--md-mermaid-edge-color) !important; -} - -/* Entity relationship line markers */ -defs #ZERO_OR_MORE_START circle, -defs #ZERO_OR_MORE_END circle { - fill: var(--md-mermaid-label-bg-color); -} - -/* ---------------------------------------------------------------------------- - * Rules: sequence diagrams - * ------------------------------------------------------------------------- */ - -/* Sequence actor */ -.actor { - fill: var(--md-mermaid-sequence-actor-bg-color); - stroke: var(--md-mermaid-sequence-actor-border-color); -} - -/* Sequence actor text */ -text.actor > tspan { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-sequence-actor-fg-color); -} - -/* Sequence actor line */ -line { - stroke: var(--md-mermaid-sequence-actor-line-color); -} - -/* Sequence actor */ -.actor-man circle, -.actor-man line { - fill: var(--md-mermaid-sequence-actorman-bg-color); - stroke: var(--md-mermaid-sequence-actorman-line-color); -} - -/* Sequence message line */ -.messageLine0, -.messageLine1 { - stroke: var(--md-mermaid-sequence-message-line-color); -} - -/* Sequence note */ -.note { - fill: var(--md-mermaid-sequence-note-bg-color); - stroke: var(--md-mermaid-sequence-note-border-color); -} - -/* Sequence message, loop and note text */ -.messageText, -.loopText, -.loopText > tspan, -.noteText > tspan { - font-family: var(--md-mermaid-font-family) !important; - stroke: none; -} - -/* Sequence message text */ -.messageText { - fill: var(--md-mermaid-sequence-message-fg-color); -} - -/* Sequence loop text */ -.loopText, -.loopText > tspan { - fill: var(--md-mermaid-sequence-loop-fg-color); -} - -/* Sequence note text */ -.noteText > tspan { - fill: var(--md-mermaid-sequence-note-fg-color); -} - -/* Sequence arrow head */ -#arrowhead path { - fill: var(--md-mermaid-sequence-message-line-color); - stroke: none; -} - -/* Sequence loop line */ -.loopLine { - fill: var(--md-mermaid-sequence-loop-bg-color); - stroke: var(--md-mermaid-sequence-loop-border-color); -} - -/* Sequence label box */ -.labelBox { - fill: var(--md-mermaid-sequence-label-bg-color); - stroke: none; -} - -/* Sequence label text */ -.labelText, -.labelText > span { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-sequence-label-fg-color); -} - -/* Sequence number */ -.sequenceNumber { - fill: var(--md-mermaid-sequence-number-fg-color); -} - -/* Sequence rectangle */ -rect.rect { - fill: var(--md-mermaid-sequence-box-bg-color); - stroke: none; -} - -/* Sequence rectangle text */ -rect.rect + text.text { - fill: var(--md-mermaid-sequence-box-fg-color); -} - -/* Sequence diagram markers */ -defs #sequencenumber { - fill: var(--md-mermaid-sequence-number-bg-color) !important; -} diff --git a/src/templates/assets/javascripts/components/content/mermaid/index.ts b/src/templates/assets/javascripts/components/content/mermaid/index.ts deleted file mode 100644 index 3f6480fd..00000000 --- a/src/templates/assets/javascripts/components/content/mermaid/index.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - map, - of, - shareReplay, - tap -} from "rxjs" - -import { watchScript } from "~/browser" -import { h } from "~/utilities" - -import { Component } from "../../_" - -import themeCSS from "./index.css" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Mermaid diagram - */ -export interface Mermaid {} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Mermaid instance observable - */ -let mermaid$: Observable<void> - -/** - * Global sequence number for diagrams - */ -let sequence = 0 - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch Mermaid script - * - * @returns Mermaid scripts observable - */ -function fetchScripts(): Observable<void> { - return typeof mermaid === "undefined" || mermaid instanceof Element - ? watchScript("https://unpkg.com/mermaid@9.4.3/dist/mermaid.min.js") - : of(undefined) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount Mermaid diagram - * - * @param el - Code block element - * - * @returns Mermaid diagram component observable - */ -export function mountMermaid( - el: HTMLElement -): Observable<Component<Mermaid>> { - el.classList.remove("mermaid") // Hack: mitigate https://bit.ly/3CiN6Du - mermaid$ ||= fetchScripts() - .pipe( - tap(() => mermaid.initialize({ - startOnLoad: false, - themeCSS, - sequence: { - actorFontSize: "16px", // Hack: mitigate https://bit.ly/3y0NEi3 - messageFontSize: "16px", - noteFontSize: "16px" - } - })), - map(() => undefined), - shareReplay(1) - ) - - /* Render diagram */ - mermaid$.subscribe(() => { - el.classList.add("mermaid") // Hack: mitigate https://bit.ly/3CiN6Du - const id = `__mermaid_${sequence++}` - - /* Create host element to replace code block */ - const host = h("div", { class: "mermaid" }) - const text = el.textContent - - /* Render and inject diagram */ - mermaid.mermaidAPI.render(id, text, (svg: string, fn: Function) => { - - /* Create a shadow root and inject diagram */ - const shadow = host.attachShadow({ mode: "closed" }) - shadow.innerHTML = svg - - /* Replace code block with diagram and bind functions */ - el.replaceWith(host) - fn?.(shadow) - }) - }) - - /* Create and return component */ - return mermaid$ - .pipe( - map(() => ({ ref: el })) - ) -} diff --git a/src/templates/assets/javascripts/components/content/table/index.ts b/src/templates/assets/javascripts/components/content/table/index.ts deleted file mode 100644 index c318e7a6..00000000 --- a/src/templates/assets/javascripts/components/content/table/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, of } from "rxjs" - -import { renderTable } from "~/templates" -import { h } from "~/utilities" - -import { Component } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Data table - */ -export interface DataTable {} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Sentinel for replacement - */ -const sentinel = h("table") - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount data table - * - * 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 - * - * @returns Data table component observable - */ -export function mountDataTable( - el: HTMLElement -): Observable<Component<DataTable>> { - el.replaceWith(sentinel) - sentinel.replaceWith(renderTable(el)) - - /* Create and return component */ - return of({ ref: el }) -} diff --git a/src/templates/assets/javascripts/components/content/tabs/index.ts b/src/templates/assets/javascripts/components/content/tabs/index.ts deleted file mode 100644 index f57447e2..00000000 --- a/src/templates/assets/javascripts/components/content/tabs/index.ts +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - animationFrameScheduler, - asyncScheduler, - auditTime, - combineLatest, - defer, - endWith, - finalize, - fromEvent, - ignoreElements, - map, - merge, - skip, - startWith, - subscribeOn, - takeUntil, - tap, - withLatestFrom -} from "rxjs" - -import { feature } from "~/_" -import { - Viewport, - getElement, - getElementContentOffset, - getElementContentSize, - getElementOffset, - getElementSize, - getElements, - watchElementContentOffset, - watchElementSize -} from "~/browser" -import { renderTabbedControl } from "~/templates" - -import { Component } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Content tabs - */ -export interface ContentTabs { - active: HTMLLabelElement /* Active tab label */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch content tabs - * - * @param el - Content tabs element - * - * @returns Content tabs observable - */ -export function watchContentTabs( - el: HTMLElement -): Observable<ContentTabs> { - const inputs = getElements<HTMLInputElement>(":scope > input", el) - const initial = inputs.find(input => input.checked) || inputs[0] - return merge(...inputs.map(input => fromEvent(input, "change") - .pipe( - map(() => getElement<HTMLLabelElement>(`label[for="${input.id}"]`)) - ) - )) - .pipe( - startWith(getElement<HTMLLabelElement>(`label[for="${initial.id}"]`)), - map(active => ({ active })) - ) -} - -/** - * Mount content tabs - * - * This function scrolls the active tab into view. While this functionality is - * provided by browsers as part of `scrollInfoView`, browsers will always also - * scroll the vertical axis, which we do not want. Thus, we decided to provide - * this functionality ourselves. - * - * @param el - Content tabs element - * @param options - Options - * - * @returns Content tabs component observable - */ -export function mountContentTabs( - el: HTMLElement, { viewport$ }: MountOptions -): Observable<Component<ContentTabs>> { - - /* Render content tab previous button for pagination */ - const prev = renderTabbedControl("prev") - el.append(prev) - - /* Render content tab next button for pagination */ - const next = renderTabbedControl("next") - el.append(next) - - /* Mount component on subscription */ - const container = getElement(".tabbed-labels", el) - return defer(() => { - const push$ = new Subject<ContentTabs>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - combineLatest([push$, watchElementSize(el)]) - .pipe( - auditTime(1, animationFrameScheduler), - takeUntil(done$) - ) - .subscribe({ - - /* Handle emission */ - next([{ active }, size]) { - const offset = getElementOffset(active) - const { width } = getElementSize(active) - - /* Set tab indicator offset and width */ - el.style.setProperty("--md-indicator-x", `${offset.x}px`) - el.style.setProperty("--md-indicator-width", `${width}px`) - - /* Scroll container to active content tab */ - const content = getElementContentOffset(container) - if ( - offset.x < content.x || - offset.x + width > content.x + size.width - ) - container.scrollTo({ - left: Math.max(0, offset.x - 16), - behavior: "smooth" - }) - }, - - /* Handle complete */ - complete() { - el.style.removeProperty("--md-indicator-x") - el.style.removeProperty("--md-indicator-width") - } - }) - - /* Hide content tab buttons on borders */ - combineLatest([ - watchElementContentOffset(container), - watchElementSize(container) - ]) - .pipe( - takeUntil(done$) - ) - .subscribe(([offset, size]) => { - const content = getElementContentSize(container) - prev.hidden = offset.x < 16 - next.hidden = offset.x > content.width - size.width - 16 - }) - - /* Paginate content tab container on click */ - merge( - fromEvent(prev, "click").pipe(map(() => -1)), - fromEvent(next, "click").pipe(map(() => +1)) - ) - .pipe( - takeUntil(done$) - ) - .subscribe(direction => { - const { width } = getElementSize(container) - container.scrollBy({ - left: width * direction, - behavior: "smooth" - }) - }) - - /* Set up linking of content tabs, if enabled */ - if (feature("content.tabs.link")) - push$.pipe( - skip(1), - withLatestFrom(viewport$) - ) - .subscribe(([{ active }, { offset }]) => { - const tab = active.innerText.trim() - if (active.hasAttribute("data-md-switching")) { - active.removeAttribute("data-md-switching") - - /* Determine viewport offset of active tab */ - } else { - const y = el.offsetTop - offset.y - - /* Passively activate other tabs */ - for (const set of getElements("[data-tabs]")) - for (const input of getElements<HTMLInputElement>( - ":scope > input", set - )) { - const label = getElement(`label[for="${input.id}"]`) - if ( - label !== active && - label.innerText.trim() === tab - ) { - label.setAttribute("data-md-switching", "") - input.click() - break - } - } - - /* Bring active tab into view */ - window.scrollTo({ - top: el.offsetTop - y - }) - - /* Persist active tabs in local storage */ - const tabs = __md_get<string[]>("__tabs") || [] - __md_set("__tabs", [...new Set([tab, ...tabs])]) - } - }) - - /* Pause media (audio, video) on switch - see https://bit.ly/3Bk6cel */ - push$.pipe(takeUntil(done$)) - .subscribe(() => { - for (const media of getElements<HTMLAudioElement>("audio, video", el)) - media.pause() - }) - - /* Create and return component */ - return watchContentTabs(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) - .pipe( - subscribeOn(asyncScheduler) - ) -} diff --git a/src/templates/assets/javascripts/components/dialog/index.ts b/src/templates/assets/javascripts/components/dialog/index.ts deleted file mode 100644 index 6ff1bd44..00000000 --- a/src/templates/assets/javascripts/components/dialog/index.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - defer, - delay, - finalize, - map, - merge, - of, - switchMap, - tap -} from "rxjs" - -import { getElement } from "~/browser" - -import { Component } from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Dialog - */ -export interface Dialog { - message: string /* Dialog message */ - active: boolean /* Dialog is active */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - alert$: Subject<string> /* Alert subject */ -} - -/** - * Mount options - */ -interface MountOptions { - alert$: Subject<string> /* Alert subject */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch dialog - * - * @param _el - Dialog element - * @param options - Options - * - * @returns Dialog observable - */ -export function watchDialog( - _el: HTMLElement, { alert$ }: WatchOptions -): Observable<Dialog> { - return alert$ - .pipe( - switchMap(message => merge( - of(true), - of(false).pipe(delay(2000)) - ) - .pipe( - map(active => ({ message, active })) - ) - ) - ) -} - -/** - * Mount dialog - * - * This function reveals the dialog in the right corner when a new alert is - * emitted through the subject that is passed as part of the options. - * - * @param el - Dialog element - * @param options - Options - * - * @returns Dialog component observable - */ -export function mountDialog( - el: HTMLElement, options: MountOptions -): Observable<Component<Dialog>> { - const inner = getElement(".md-typeset", el) - return defer(() => { - const push$ = new Subject<Dialog>() - push$.subscribe(({ message, active }) => { - el.classList.toggle("md-dialog--active", active) - inner.textContent = message - }) - - /* Create and return component */ - return watchDialog(el, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/header/_/index.ts b/src/templates/assets/javascripts/components/header/_/index.ts deleted file mode 100644 index 0f33eb48..00000000 --- a/src/templates/assets/javascripts/components/header/_/index.ts +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - bufferCount, - combineLatest, - combineLatestWith, - defer, - distinctUntilChanged, - distinctUntilKeyChanged, - endWith, - filter, - ignoreElements, - map, - of, - shareReplay, - startWith, - switchMap, - takeUntil -} from "rxjs" - -import { feature } from "~/_" -import { - Viewport, - watchElementSize, - watchToggle -} from "~/browser" - -import { Component } from "../../_" -import { Main } from "../../main" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Header - */ -export interface Header { - height: number /* Header visible height */ - hidden: boolean /* Header is hidden */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ - main$: Observable<Main> /* Main area observable */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Compute whether the header is hidden - * - * If the user scrolls past a certain threshold, the header can be hidden when - * scrolling down, and shown when scrolling up. - * - * @param options - Options - * - * @returns Toggle observable - */ -function isHidden({ viewport$ }: WatchOptions): Observable<boolean> { - if (!feature("header.autohide")) - return of(false) - - /* Compute direction and turning point */ - const direction$ = viewport$ - .pipe( - map(({ offset: { y } }) => y), - bufferCount(2, 1), - map(([a, b]) => [a < b, b] as const), - distinctUntilKeyChanged(0) - ) - - /* Compute whether header should be hidden */ - const hidden$ = combineLatest([viewport$, direction$]) - .pipe( - filter(([{ offset }, [, y]]) => Math.abs(y - offset.y) > 100), - map(([, [direction]]) => direction), - distinctUntilChanged() - ) - - /* Compute threshold for hiding */ - const search$ = watchToggle("search") - return combineLatest([viewport$, search$]) - .pipe( - map(([{ offset }, search]) => offset.y > 400 && !search), - distinctUntilChanged(), - switchMap(active => active ? hidden$ : of(false)), - startWith(false) - ) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch header - * - * @param el - Header element - * @param options - Options - * - * @returns Header observable - */ -export function watchHeader( - el: HTMLElement, options: WatchOptions -): Observable<Header> { - return defer(() => combineLatest([ - watchElementSize(el), - isHidden(options) - ])) - .pipe( - map(([{ height }, hidden]) => ({ - height, - hidden - })), - distinctUntilChanged((a, b) => ( - a.height === b.height && - a.hidden === b.hidden - )), - shareReplay(1) - ) -} - -/** - * Mount header - * - * This function manages the different states of the header, i.e. whether it's - * hidden or rendered with a shadow. This depends heavily on the main area. - * - * @param el - Header element - * @param options - Options - * - * @returns Header component observable - */ -export function mountHeader( - el: HTMLElement, { header$, main$ }: MountOptions -): Observable<Component<Header>> { - return defer(() => { - const push$ = new Subject<Main>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$ - .pipe( - distinctUntilKeyChanged("active"), - combineLatestWith(header$) - ) - .subscribe(([{ active }, { hidden }]) => { - el.classList.toggle("md-header--shadow", active && !hidden) - el.hidden = hidden - }) - - /* Link to main area */ - main$.subscribe(push$) - - /* Create and return component */ - return header$ - .pipe( - takeUntil(done$), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/header/index.ts b/src/templates/assets/javascripts/components/header/index.ts deleted file mode 100644 index cf23ec1a..00000000 --- a/src/templates/assets/javascripts/components/header/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./title" diff --git a/src/templates/assets/javascripts/components/header/title/index.ts b/src/templates/assets/javascripts/components/header/title/index.ts deleted file mode 100644 index f3bc0d08..00000000 --- a/src/templates/assets/javascripts/components/header/title/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - EMPTY, - Observable, - Subject, - defer, - distinctUntilKeyChanged, - finalize, - map, - tap -} from "rxjs" - -import { - Viewport, - getElementSize, - getOptionalElement, - watchViewportAt -} from "~/browser" - -import { Component } from "../../_" -import { Header } from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Header - */ -export interface HeaderTitle { - active: boolean /* Header title is active */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch header title - * - * @param el - Heading element - * @param options - Options - * - * @returns Header title observable - */ -export function watchHeaderTitle( - el: HTMLElement, { viewport$, header$ }: WatchOptions -): Observable<HeaderTitle> { - return watchViewportAt(el, { viewport$, header$ }) - .pipe( - map(({ offset: { y } }) => { - const { height } = getElementSize(el) - return { - active: y >= height - } - }), - distinctUntilKeyChanged("active") - ) -} - -/** - * Mount header title - * - * This function swaps the header title from the site title to the title of the - * current page when the user scrolls past the first headline. - * - * @param el - Header title element - * @param options - Options - * - * @returns Header title component observable - */ -export function mountHeaderTitle( - el: HTMLElement, options: MountOptions -): Observable<Component<HeaderTitle>> { - return defer(() => { - const push$ = new Subject<HeaderTitle>() - push$.subscribe({ - - /* Handle emission */ - next({ active }) { - el.classList.toggle("md-header__title--active", active) - }, - - /* Handle complete */ - complete() { - el.classList.remove("md-header__title--active") - } - }) - - /* Obtain headline, if any */ - const heading = getOptionalElement(".md-content h1") - if (typeof heading === "undefined") - return EMPTY - - /* Create and return component */ - return watchHeaderTitle(heading, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/index.ts b/src/templates/assets/javascripts/components/index.ts deleted file mode 100644 index 3d4391d1..00000000 --- a/src/templates/assets/javascripts/components/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./announce" -export * from "./consent" -export * from "./content" -export * from "./dialog" -export * from "./header" -export * from "./main" -export * from "./palette" -export * from "./progress" -export * from "./search" -export * from "./sidebar" -export * from "./source" -export * from "./tabs" -export * from "./toc" -export * from "./top" diff --git a/src/templates/assets/javascripts/components/main/index.ts b/src/templates/assets/javascripts/components/main/index.ts deleted file mode 100644 index 2509f9b9..00000000 --- a/src/templates/assets/javascripts/components/main/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - combineLatest, - distinctUntilChanged, - distinctUntilKeyChanged, - map, - switchMap -} from "rxjs" - -import { - Viewport, - watchElementSize -} from "~/browser" - -import { Header } from "../header" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Main area - */ -export interface Main { - offset: number /* Main area top offset */ - height: number /* Main area visible height */ - active: boolean /* Main area is active */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch main area - * - * This function returns an observable that computes the visual parameters of - * the main area which depends on the viewport vertical offset and height, as - * well as the height of the header element, if the header is fixed. - * - * @param el - Main area element - * @param options - Options - * - * @returns Main area observable - */ -export function watchMain( - el: HTMLElement, { viewport$, header$ }: WatchOptions -): Observable<Main> { - - /* Compute necessary adjustment for header */ - const adjust$ = header$ - .pipe( - map(({ height }) => height), - distinctUntilChanged() - ) - - /* Compute the main area's top and bottom borders */ - const border$ = adjust$ - .pipe( - switchMap(() => watchElementSize(el) - .pipe( - map(({ height }) => ({ - top: el.offsetTop, - bottom: el.offsetTop + height - })), - distinctUntilKeyChanged("bottom") - ) - ) - ) - - /* Compute the main area's offset, visible height and if we scrolled past */ - return combineLatest([adjust$, border$, viewport$]) - .pipe( - map(([header, { top, bottom }, { offset: { y }, size: { height } }]) => { - height = Math.max(0, height - - Math.max(0, top - y, header) - - Math.max(0, height + y - bottom) - ) - return { - offset: top - header, - height, - active: top - header <= y - } - }), - distinctUntilChanged((a, b) => ( - a.offset === b.offset && - a.height === b.height && - a.active === b.active - )) - ) -} diff --git a/src/templates/assets/javascripts/components/palette/index.ts b/src/templates/assets/javascripts/components/palette/index.ts deleted file mode 100644 index cf578f60..00000000 --- a/src/templates/assets/javascripts/components/palette/index.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - asyncScheduler, - defer, - finalize, - fromEvent, - map, - mergeMap, - observeOn, - of, - shareReplay, - startWith, - tap -} from "rxjs" - -import { getElements } from "~/browser" -import { h } from "~/utilities" - -import { - Component, - getComponentElement -} from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Palette colors - */ -export interface PaletteColor { - scheme?: string /* Color scheme */ - primary?: string /* Primary color */ - accent?: string /* Accent color */ -} - -/** - * Palette - */ -export interface Palette { - index: number /* Palette index */ - color: PaletteColor /* Palette colors */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch color palette - * - * @param inputs - Color palette element - * - * @returns Color palette observable - */ -export function watchPalette( - inputs: HTMLInputElement[] -): Observable<Palette> { - const current = __md_get<Palette>("__palette") || { - index: inputs.findIndex(input => matchMedia( - input.getAttribute("data-md-color-media")! - ).matches) - } - - /* Emit changes in color palette */ - return of(...inputs) - .pipe( - mergeMap(input => fromEvent(input, "change") - .pipe( - map(() => input) - ) - ), - startWith(inputs[Math.max(0, current.index)]), - map(input => ({ - index: inputs.indexOf(input), - color: { - scheme: input.getAttribute("data-md-color-scheme"), - primary: input.getAttribute("data-md-color-primary"), - accent: input.getAttribute("data-md-color-accent") - } - } as Palette)), - shareReplay(1) - ) -} - -/** - * Mount color palette - * - * @param el - Color palette element - * - * @returns Color palette component observable - */ -export function mountPalette( - el: HTMLElement -): Observable<Component<Palette>> { - const meta = h("meta", { name: "theme-color" }) - document.head.appendChild(meta) - - // Add color scheme meta tag - const scheme = h("meta", { name: "color-scheme" }) - document.head.appendChild(scheme) - - /* Mount component on subscription */ - return defer(() => { - const push$ = new Subject<Palette>() - push$.subscribe(palette => { - document.body.setAttribute("data-md-color-switching", "") - - /* Set color palette */ - for (const [key, value] of Object.entries(palette.color)) - document.body.setAttribute(`data-md-color-${key}`, value) - - /* Toggle visibility */ - for (let index = 0; index < inputs.length; index++) { - const label = inputs[index].nextElementSibling - if (label instanceof HTMLElement) - label.hidden = palette.index !== index - } - - /* Persist preference in local storage */ - __md_set("__palette", palette) - }) - - /* Update theme-color meta tag */ - push$ - .pipe( - map(() => { - const header = getComponentElement("header") - const style = window.getComputedStyle(header) - - // Set color scheme - scheme.content = style.colorScheme - - /* Return color in hexadecimal format */ - return style.backgroundColor.match(/\d+/g)! - .map(value => (+value).toString(16).padStart(2, "0")) - .join("") - }) - ) - .subscribe(color => meta.content = `#${color}`) - - /* Revert transition durations after color switch */ - push$.pipe(observeOn(asyncScheduler)) - .subscribe(() => { - document.body.removeAttribute("data-md-color-switching") - }) - - /* Create and return component */ - const inputs = getElements<HTMLInputElement>("input", el) - return watchPalette(inputs) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/progress/index.ts b/src/templates/assets/javascripts/components/progress/index.ts deleted file mode 100644 index 30c722b8..00000000 --- a/src/templates/assets/javascripts/components/progress/index.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - defer, - finalize, - map, - tap -} from "rxjs" - -import { Component } from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Progress indicator - */ -export interface Progress { - value: number // Progress value -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - progress$: Subject<number> // Progress subject -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount progress indicator - * - * @param el - Progress indicator element - * @param options - Options - * - * @returns Progress indicator component observable - */ -export function mountProgress( - el: HTMLElement, { progress$ }: MountOptions -): Observable<Component<Progress>> { - - // Mount component on subscription - return defer(() => { - const push$ = new Subject<Progress>() - push$.subscribe(({ value }) => { - el.style.setProperty("--md-progress-value", `${value}`) - }) - - // Create and return component - return progress$ - .pipe( - tap(value => push$.next({ value })), - finalize(() => push$.complete()), - map(value => ({ ref: el, value })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/search/_/index.ts b/src/templates/assets/javascripts/components/search/_/index.ts deleted file mode 100644 index aa963b47..00000000 --- a/src/templates/assets/javascripts/components/search/_/index.ts +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - NEVER, - Observable, - ObservableInput, - filter, - fromEvent, - merge, - mergeWith -} from "rxjs" - -import { configuration } from "~/_" -import { - Keyboard, - getActiveElement, - getElements, - setToggle -} from "~/browser" -import { - SearchIndex, - SearchResult, - setupSearchWorker -} from "~/integrations" - -import { - Component, - getComponentElement, - getComponentElements -} from "../../_" -import { - SearchQuery, - mountSearchQuery -} from "../query" -import { mountSearchResult } from "../result" -import { - SearchShare, - mountSearchShare -} from "../share" -import { - SearchSuggest, - mountSearchSuggest -} from "../suggest" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search - */ -export type Search = - | SearchQuery - | SearchResult - | SearchShare - | SearchSuggest - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - index$: ObservableInput<SearchIndex> /* Search index observable */ - keyboard$: Observable<Keyboard> /* Keyboard observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount search - * - * This function sets up the search functionality, including the underlying - * web worker and all keyboard bindings. - * - * @param el - Search element - * @param options - Options - * - * @returns Search component observable - */ -export function mountSearch( - el: HTMLElement, { index$, keyboard$ }: MountOptions -): Observable<Component<Search>> { - const config = configuration() - try { - const worker$ = setupSearchWorker(config.search, index$) - - /* Retrieve query and result components */ - const query = getComponentElement("search-query", el) - const result = getComponentElement("search-result", el) - - /* Always close search on result selection */ - fromEvent<PointerEvent>(el, "click") - .pipe( - filter(({ target }) => ( - target instanceof Element && !!target.closest("a") - )) - ) - .subscribe(() => setToggle("search", false)) - - /* Set up search keyboard handlers */ - keyboard$ - .pipe( - filter(({ mode }) => mode === "search") - ) - .subscribe(key => { - const active = getActiveElement() - switch (key.type) { - - /* Enter: go to first (best) result */ - case "Enter": - if (active === query) { - const anchors = new Map<HTMLAnchorElement, number>() - for (const anchor of getElements<HTMLAnchorElement>( - ":first-child [href]", result - )) { - const article = anchor.firstElementChild! - anchors.set(anchor, parseFloat( - article.getAttribute("data-md-score")! - )) - } - - /* Go to result with highest score, if any */ - if (anchors.size) { - const [[best]] = [...anchors].sort(([, a], [, b]) => b - a) - best.click() - } - - /* Otherwise omit form submission */ - key.claim() - } - break - - /* Escape or Tab: close search */ - case "Escape": - case "Tab": - setToggle("search", false) - query.blur() - break - - /* Vertical arrows: select previous or next search result */ - case "ArrowUp": - case "ArrowDown": - if (typeof active === "undefined") { - query.focus() - } else { - const els = [query, ...getElements( - ":not(details) > [href], summary, details[open] [href]", - result - )] - const i = Math.max(0, ( - Math.max(0, els.indexOf(active)) + els.length + ( - key.type === "ArrowUp" ? -1 : +1 - ) - ) % els.length) - els[i].focus() - } - - /* Prevent scrolling of page */ - key.claim() - break - - /* All other keys: hand to search query */ - default: - if (query !== getActiveElement()) - query.focus() - } - }) - - /* Set up global keyboard handlers */ - keyboard$ - .pipe( - filter(({ mode }) => mode === "global") - ) - .subscribe(key => { - switch (key.type) { - - /* Open search and select query */ - case "f": - case "s": - case "/": - query.focus() - query.select() - - /* Prevent scrolling of page */ - key.claim() - break - } - }) - - /* Create and return component */ - const query$ = mountSearchQuery(query, { worker$ }) - return merge( - query$, - mountSearchResult(result, { worker$, query$ }) - ) - .pipe( - mergeWith( - - /* Search sharing */ - ...getComponentElements("search-share", el) - .map(child => mountSearchShare(child, { query$ })), - - /* Search suggestions */ - ...getComponentElements("search-suggest", el) - .map(child => mountSearchSuggest(child, { worker$, keyboard$ })) - ) - ) - - /* Gracefully handle broken search */ - } catch (err) { - el.hidden = true - return NEVER - } -} diff --git a/src/templates/assets/javascripts/components/search/highlight/.eslintrc b/src/templates/assets/javascripts/components/search/highlight/.eslintrc deleted file mode 100644 index 38a5714d..00000000 --- a/src/templates/assets/javascripts/components/search/highlight/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "no-null/no-null": "off" - } -} diff --git a/src/templates/assets/javascripts/components/search/highlight/index.ts b/src/templates/assets/javascripts/components/search/highlight/index.ts deleted file mode 100644 index bc3f94c9..00000000 --- a/src/templates/assets/javascripts/components/search/highlight/index.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - ObservableInput, - combineLatest, - filter, - map, - startWith -} from "rxjs" - -import { getLocation } from "~/browser" -import { - SearchIndex, - setupSearchHighlighter -} from "~/integrations" -import { h } from "~/utilities" - -import { Component } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search highlighting - */ -export interface SearchHighlight { - nodes: Map<ChildNode, string> /* Map of replacements */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - index$: ObservableInput<SearchIndex> /* Search index observable */ - location$: Observable<URL> /* Location observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount search highlighting - * - * @param el - Content element - * @param options - Options - * - * @returns Search highlighting component observable - */ -export function mountSearchHiglight( - el: HTMLElement, { index$, location$ }: MountOptions -): Observable<Component<SearchHighlight>> { - return combineLatest([ - index$, - location$ - .pipe( - startWith(getLocation()), - filter(url => !!url.searchParams.get("h")) - ) - ]) - .pipe( - map(([index, url]) => setupSearchHighlighter(index.config)( - url.searchParams.get("h")! - )), - map(fn => { - const nodes = new Map<ChildNode, string>() - - /* Traverse text nodes and collect matches */ - const it = document.createNodeIterator(el, NodeFilter.SHOW_TEXT) - for (let node = it.nextNode(); node; node = it.nextNode()) { - if (node.parentElement?.offsetHeight) { - const original = node.textContent! - const replaced = fn(original) - if (replaced.length > original.length) - nodes.set(node as ChildNode, replaced) - } - } - - /* Replace original nodes with matches */ - for (const [node, text] of nodes) { - const { childNodes } = h("span", null, text) - node.replaceWith(...Array.from(childNodes)) - } - - /* Return component */ - return { ref: el, nodes } - }) - ) -} diff --git a/src/templates/assets/javascripts/components/search/index.ts b/src/templates/assets/javascripts/components/search/index.ts deleted file mode 100644 index 846d8685..00000000 --- a/src/templates/assets/javascripts/components/search/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./highlight" -export * from "./query" -export * from "./result" -export * from "./share" -export * from "./suggest" diff --git a/src/templates/assets/javascripts/components/search/query/index.ts b/src/templates/assets/javascripts/components/search/query/index.ts deleted file mode 100644 index 4ce21279..00000000 --- a/src/templates/assets/javascripts/components/search/query/index.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - combineLatest, - distinctUntilChanged, - distinctUntilKeyChanged, - endWith, - finalize, - first, - fromEvent, - ignoreElements, - map, - merge, - shareReplay, - takeUntil, - tap -} from "rxjs" - -import { - getElement, - getLocation, - setToggle, - watchElementFocus, - watchToggle -} from "~/browser" -import { - SearchMessage, - SearchMessageType, - isSearchReadyMessage -} from "~/integrations" - -import { Component } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search query - */ -export interface SearchQuery { - value: string /* Query value */ - focus: boolean /* Query focus */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - worker$: Subject<SearchMessage> /* Search worker */ -} - -/** - * Mount options - */ -interface MountOptions { - worker$: Subject<SearchMessage> /* Search worker */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch search query - * - * Note that the focus event which triggers re-reading the current query value - * is delayed by `1ms` so the input's empty state is allowed to propagate. - * - * @param el - Search query element - * @param options - Options - * - * @returns Search query observable - */ -export function watchSearchQuery( - el: HTMLInputElement, { worker$ }: WatchOptions -): Observable<SearchQuery> { - - /* Support search deep linking */ - const { searchParams } = getLocation() - if (searchParams.has("q")) { - setToggle("search", true) - - /* Set query from parameter */ - el.value = searchParams.get("q")! - el.focus() - - /* Remove query parameter on close */ - watchToggle("search") - .pipe( - first(active => !active) - ) - .subscribe(() => { - const url = getLocation() - url.searchParams.delete("q") - history.replaceState({}, "", `${url}`) - }) - } - - /* Intercept focus and input events */ - const focus$ = watchElementFocus(el) - const value$ = merge( - worker$.pipe(first(isSearchReadyMessage)), - fromEvent(el, "keyup"), - focus$ - ) - .pipe( - map(() => el.value), - distinctUntilChanged() - ) - - /* Combine into single observable */ - return combineLatest([value$, focus$]) - .pipe( - map(([value, focus]) => ({ value, focus })), - shareReplay(1) - ) -} - -/** - * Mount search query - * - * @param el - Search query element - * @param options - Options - * - * @returns Search query component observable - */ -export function mountSearchQuery( - el: HTMLInputElement, { worker$ }: MountOptions -): Observable<Component<SearchQuery, HTMLInputElement>> { - const push$ = new Subject<SearchQuery>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - - /* Handle value change */ - combineLatest([ - worker$.pipe(first(isSearchReadyMessage)), - push$ - ], (_, query) => query) - .pipe( - distinctUntilKeyChanged("value") - ) - .subscribe(({ value }) => worker$.next({ - type: SearchMessageType.QUERY, - data: value - })) - - /* Handle focus change */ - push$ - .pipe( - distinctUntilKeyChanged("focus") - ) - .subscribe(({ focus }) => { - if (focus) - setToggle("search", focus) - }) - - /* Handle reset */ - fromEvent(el.form!, "reset") - .pipe( - takeUntil(done$) - ) - .subscribe(() => el.focus()) - - // Focus search query on label click - note that this is necessary to bring - // up the keyboard on iOS and other mobile platforms, as the search dialog is - // not visible at first, and programatically focusing an input element must - // be triggered by a user interaction - see https://t.ly/Cb30n - const label = getElement("header [for=__search]") - fromEvent(label, "click") - .subscribe(() => el.focus()) - - /* Create and return component */ - return watchSearchQuery(el, { worker$ }) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })), - shareReplay(1) - ) -} diff --git a/src/templates/assets/javascripts/components/search/result/index.ts b/src/templates/assets/javascripts/components/search/result/index.ts deleted file mode 100644 index c3c9ef20..00000000 --- a/src/templates/assets/javascripts/components/search/result/index.ts +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - EMPTY, - Observable, - Subject, - bufferCount, - filter, - finalize, - first, - fromEvent, - map, - merge, - mergeMap, - of, - share, - skipUntil, - switchMap, - takeUntil, - tap, - withLatestFrom, - zipWith -} from "rxjs" - -import { translation } from "~/_" -import { - getElement, - getOptionalElement, - watchElementBoundary, - watchToggle -} from "~/browser" -import { - SearchMessage, - SearchResult, - isSearchReadyMessage, - isSearchResultMessage -} from "~/integrations" -import { renderSearchResultItem } from "~/templates" -import { round } from "~/utilities" - -import { Component } from "../../_" -import { SearchQuery } from "../query" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - query$: Observable<SearchQuery> /* Search query observable */ - worker$: Subject<SearchMessage> /* Search worker */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount search result list - * - * 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 options - Options - * - * @returns Search result list component observable - */ -export function mountSearchResult( - el: HTMLElement, { worker$, query$ }: MountOptions -): Observable<Component<SearchResult>> { - const push$ = new Subject<SearchResult>() - const boundary$ = watchElementBoundary(el.parentElement!) - .pipe( - filter(Boolean) - ) - - /* Retrieve container */ - const container = el.parentElement! - - /* Retrieve nested components */ - const meta = getElement(":scope > :first-child", el) - const list = getElement(":scope > :last-child", el) - - /* Reveal to accessibility tree – see https://bit.ly/3iAA7t8 */ - watchToggle("search") - .subscribe(active => list.setAttribute( - "role", active ? "list" : "presentation" - )) - - /* Update search result metadata */ - push$ - .pipe( - withLatestFrom(query$), - skipUntil(worker$.pipe(first(isSearchReadyMessage))) - ) - .subscribe(([{ items }, { value }]) => { - switch (items.length) { - - /* No results */ - case 0: - meta.textContent = value.length - ? translation("search.result.none") - : translation("search.result.placeholder") - break - - /* One result */ - case 1: - meta.textContent = translation("search.result.one") - break - - /* Multiple result */ - default: - const count = round(items.length) - meta.textContent = translation("search.result.other", count) - } - }) - - /* Render search result item */ - const render$ = push$ - .pipe( - tap(() => list.innerHTML = ""), - switchMap(({ items }) => merge( - of(...items.slice(0, 10)), - of(...items.slice(10)) - .pipe( - bufferCount(4), - zipWith(boundary$), - switchMap(([chunk]) => chunk) - ) - )), - map(renderSearchResultItem), - share() - ) - - /* Update search result list */ - render$.subscribe(item => list.appendChild(item)) - render$ - .pipe( - mergeMap(item => { - const details = getOptionalElement("details", item) - if (typeof details === "undefined") - return EMPTY - - /* Keep position of details element stable */ - return fromEvent(details, "toggle") - .pipe( - takeUntil(push$), - map(() => details) - ) - }) - ) - .subscribe(details => { - if ( - details.open === false && - details.offsetTop <= container.scrollTop - ) - container.scrollTo({ top: details.offsetTop }) - }) - - /* Filter search result message */ - const result$ = worker$ - .pipe( - filter(isSearchResultMessage), - map(({ data }) => data) - ) - - /* Create and return component */ - return result$ - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) -} diff --git a/src/templates/assets/javascripts/components/search/share/index.ts b/src/templates/assets/javascripts/components/search/share/index.ts deleted file mode 100644 index 3db382c8..00000000 --- a/src/templates/assets/javascripts/components/search/share/index.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - endWith, - finalize, - fromEvent, - ignoreElements, - map, - takeUntil, - tap -} from "rxjs" - -import { getLocation } from "~/browser" - -import { Component } from "../../_" -import { SearchQuery } from "../query" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search sharing - */ -export interface SearchShare { - url: URL /* Deep link for sharing */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - query$: Observable<SearchQuery> /* Search query observable */ -} - -/** - * Mount options - */ -interface MountOptions { - query$: Observable<SearchQuery> /* Search query observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount search sharing - * - * @param _el - Search sharing element - * @param options - Options - * - * @returns Search sharing observable - */ -export function watchSearchShare( - _el: HTMLElement, { query$ }: WatchOptions -): Observable<SearchShare> { - return query$ - .pipe( - map(({ value }) => { - const url = getLocation() - url.hash = "" - - /* Compute readable query strings */ - value = value - .replace(/\s+/g, "+") /* Collapse whitespace */ - .replace(/&/g, "%26") /* Escape '&' character */ - .replace(/=/g, "%3D") /* Escape '=' character */ - - /* Replace query string */ - url.search = `q=${value}` - return { url } - }) - ) -} - -/** - * Mount search sharing - * - * @param el - Search sharing element - * @param options - Options - * - * @returns Search sharing component observable - */ -export function mountSearchShare( - el: HTMLAnchorElement, options: MountOptions -): Observable<Component<SearchShare>> { - const push$ = new Subject<SearchShare>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$.subscribe(({ url }) => { - el.setAttribute("data-clipboard-text", el.href) - el.href = `${url}` - }) - - /* Prevent following of link */ - fromEvent(el, "click") - .pipe( - takeUntil(done$) - ) - .subscribe(ev => ev.preventDefault()) - - /* Create and return component */ - return watchSearchShare(el, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) -} diff --git a/src/templates/assets/javascripts/components/search/suggest/index.ts b/src/templates/assets/javascripts/components/search/suggest/index.ts deleted file mode 100644 index e7881475..00000000 --- a/src/templates/assets/javascripts/components/search/suggest/index.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - asyncScheduler, - combineLatestWith, - distinctUntilChanged, - filter, - finalize, - fromEvent, - map, - merge, - observeOn, - tap -} from "rxjs" - -import { Keyboard } from "~/browser" -import { - SearchMessage, - SearchResult, - isSearchResultMessage -} from "~/integrations" - -import { Component, getComponentElement } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search suggestions - */ -export interface SearchSuggest {} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - keyboard$: Observable<Keyboard> /* Keyboard observable */ - worker$: Subject<SearchMessage> /* Search worker */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount search suggestions - * - * This function will perform a lazy rendering of the search results, depending - * on the vertical offset of the search result container. - * - * @param el - Search result list element - * @param options - Options - * - * @returns Search result list component observable - */ -export function mountSearchSuggest( - el: HTMLElement, { worker$, keyboard$ }: MountOptions -): Observable<Component<SearchSuggest>> { - const push$ = new Subject<SearchResult>() - - /* Retrieve query component and track all changes */ - const query = getComponentElement("search-query") - const query$ = merge( - fromEvent(query, "keydown"), - fromEvent(query, "focus") - ) - .pipe( - observeOn(asyncScheduler), - map(() => query.value), - distinctUntilChanged(), - ) - - /* Update search suggestions */ - push$ - .pipe( - combineLatestWith(query$), - map(([{ suggest }, value]) => { - const words = value.split(/([\s-]+)/) - if (suggest?.length && words[words.length - 1]) { - const last = suggest[suggest.length - 1] - if (last.startsWith(words[words.length - 1])) - words[words.length - 1] = last - } else { - words.length = 0 - } - return words - }) - ) - .subscribe(words => el.innerHTML = words - .join("") - .replace(/\s/g, " ") - ) - - /* Set up search keyboard handlers */ - keyboard$ - .pipe( - filter(({ mode }) => mode === "search") - ) - .subscribe(key => { - switch (key.type) { - - /* Right arrow: accept current suggestion */ - case "ArrowRight": - if ( - el.innerText.length && - query.selectionStart === query.value.length - ) - query.value = el.innerText - break - } - }) - - /* Filter search result message */ - const result$ = worker$ - .pipe( - filter(isSearchResultMessage), - map(({ data }) => data) - ) - - /* Create and return component */ - return result$ - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(() => ({ ref: el })) - ) -} diff --git a/src/templates/assets/javascripts/components/sidebar/index.ts b/src/templates/assets/javascripts/components/sidebar/index.ts deleted file mode 100644 index 82f3d03e..00000000 --- a/src/templates/assets/javascripts/components/sidebar/index.ts +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - animationFrameScheduler, - asyncScheduler, - auditTime, - combineLatest, - defer, - distinctUntilChanged, - endWith, - finalize, - first, - from, - fromEvent, - ignoreElements, - map, - mergeMap, - observeOn, - takeUntil, - tap, - withLatestFrom -} from "rxjs" - -import { - Viewport, - getElement, - getElementContainer, - getElementOffset, - getElementSize, - getElements -} from "~/browser" - -import { Component } from "../_" -import { Header } from "../header" -import { Main } from "../main" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Sidebar - */ -export interface Sidebar { - height: number /* Sidebar height */ - locked: boolean /* Sidebar is locked */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - main$: Observable<Main> /* Main area observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ - main$: Observable<Main> /* Main area observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch sidebar - * - * This function returns an observable that computes the visual parameters of - * the sidebar which depends on the vertical viewport offset, as well as the - * height of the main area. When the page is scrolled beyond the header, the - * sidebar is locked and fills the remaining space. - * - * @param el - Sidebar element - * @param options - Options - * - * @returns Sidebar observable - */ -export function watchSidebar( - el: HTMLElement, { viewport$, main$ }: WatchOptions -): Observable<Sidebar> { - const parent = el.closest<HTMLElement>(".md-grid")! - const adjust = - parent.offsetTop - - parent.parentElement!.offsetTop - - /* Compute the sidebar's available height and if it should be locked */ - return combineLatest([main$, viewport$]) - .pipe( - map(([{ offset, height }, { offset: { y } }]) => { - height = height - + Math.min(adjust, Math.max(0, y - offset)) - - adjust - return { - height, - locked: y >= offset + adjust - } - }), - distinctUntilChanged((a, b) => ( - a.height === b.height && - a.locked === b.locked - )) - ) -} - -/** - * Mount sidebar - * - * This function doesn't set the height of the actual sidebar, but of its first - * child – the `.md-sidebar__scrollwrap` element in order to mitigiate jittery - * sidebars when the footer is scrolled into view. At some point we switched - * from `absolute` / `fixed` positioning to `sticky` positioning, significantly - * reducing jitter in some browsers (respectively Firefox and Safari) when - * scrolling from the top. However, top-aligned sticky positioning means that - * the sidebar snaps to the bottom when the end of the container is reached. - * This is what leads to the mentioned jitter, as the sidebar's height may be - * updated too slowly. - * - * This behaviour can be mitigiated by setting the height of the sidebar to `0` - * while preserving the padding, and the height on its first element. - * - * @param el - Sidebar element - * @param options - Options - * - * @returns Sidebar component observable - */ -export function mountSidebar( - el: HTMLElement, { header$, ...options }: MountOptions -): Observable<Component<Sidebar>> { - const inner = getElement(".md-sidebar__scrollwrap", el) - const { y } = getElementOffset(inner) - return defer(() => { - const push$ = new Subject<Sidebar>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - const next$ = push$ - .pipe( - auditTime(0, animationFrameScheduler) - ) - - /* Update sidebar height and offset */ - next$.pipe(withLatestFrom(header$)) - .subscribe({ - - /* Handle emission */ - next([{ height }, { height: offset }]) { - inner.style.height = `${height - 2 * y}px` - el.style.top = `${offset}px` - }, - - /* Handle complete */ - complete() { - inner.style.height = "" - el.style.top = "" - } - }) - - /* Bring active item into view on initial load */ - next$.pipe(first()) - .subscribe(() => { - for (const item of getElements(".md-nav__link--active[href]", el)) { - const container = getElementContainer(item) - if (typeof container !== "undefined") { - const offset = item.offsetTop - container.offsetTop - const { height } = getElementSize(container) - container.scrollTo({ - top: offset - height / 2 - }) - } - } - }) - - /* Handle accessibility for expandable items, see https://bit.ly/3jaod9p */ - from(getElements<HTMLLabelElement>("label[tabindex]", el)) - .pipe( - mergeMap(label => fromEvent(label, "click") - .pipe( - observeOn(asyncScheduler), - map(() => label), - takeUntil(done$) - ) - ) - ) - .subscribe(label => { - const input = getElement<HTMLInputElement>(`[id="${label.htmlFor}"]`) - const nav = getElement(`[aria-labelledby="${label.id}"]`) - nav.setAttribute("aria-expanded", `${input.checked}`) - }) - - /* Create and return component */ - return watchSidebar(el, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/source/_/index.ts b/src/templates/assets/javascripts/components/source/_/index.ts deleted file mode 100644 index 5f6c4d11..00000000 --- a/src/templates/assets/javascripts/components/source/_/index.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - EMPTY, - Observable, - Subject, - catchError, - defer, - filter, - finalize, - map, - of, - shareReplay, - tap -} from "rxjs" - -import { getElement } from "~/browser" -import { ConsentDefaults } from "~/components/consent" -import { renderSourceFacts } from "~/templates" - -import { - Component, - getComponentElements -} from "../../_" -import { - SourceFacts, - fetchSourceFacts -} from "../facts" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Repository information - */ -export interface Source { - facts: SourceFacts /* Repository facts */ -} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Repository information observable - */ -let fetch$: Observable<Source> - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch repository information - * - * 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 - * - * @returns Repository information observable - */ -export function watchSource( - el: HTMLAnchorElement -): Observable<Source> { - return fetch$ ||= defer(() => { - const cached = __md_get<SourceFacts>("__source", sessionStorage) - if (cached) { - return of(cached) - } else { - - /* Check if consent is configured and was given */ - const els = getComponentElements("consent") - if (els.length) { - const consent = __md_get<ConsentDefaults>("__consent") - if (!(consent && consent.github)) - return EMPTY - } - - /* Fetch repository facts */ - return fetchSourceFacts(el.href) - .pipe( - tap(facts => __md_set("__source", facts, sessionStorage)) - ) - } - }) - .pipe( - catchError(() => EMPTY), - filter(facts => Object.keys(facts).length > 0), - map(facts => ({ facts })), - shareReplay(1) - ) -} - -/** - * Mount repository information - * - * @param el - Repository information element - * - * @returns Repository information component observable - */ -export function mountSource( - el: HTMLAnchorElement -): Observable<Component<Source>> { - const inner = getElement(":scope > :last-child", el) - return defer(() => { - const push$ = new Subject<Source>() - push$.subscribe(({ facts }) => { - inner.appendChild(renderSourceFacts(facts)) - inner.classList.add("md-source__repository--active") - }) - - /* Create and return component */ - return watchSource(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/source/facts/_/index.ts b/src/templates/assets/javascripts/components/source/facts/_/index.ts deleted file mode 100644 index 154f229f..00000000 --- a/src/templates/assets/javascripts/components/source/facts/_/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { EMPTY, Observable } from "rxjs" - -import { fetchSourceFactsFromGitHub } from "../github" -import { fetchSourceFactsFromGitLab } from "../gitlab" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Repository facts for repositories - */ -export interface RepositoryFacts { - stars?: number /* Number of stars */ - forks?: number /* Number of forks */ - version?: string /* Latest version */ -} - -/** - * Repository facts for organizations - */ -export interface OrganizationFacts { - repositories?: number /* Number of repositories */ -} - -/* ------------------------------------------------------------------------- */ - -/** - * Repository facts - */ -export type SourceFacts = - | RepositoryFacts - | OrganizationFacts - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch repository facts - * - * @param url - Repository URL - * - * @returns Repository facts observable - */ -export function fetchSourceFacts( - url: string -): Observable<SourceFacts> { - - /* Try to match GitHub repository */ - let match = url.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i) - if (match) { - const [, user, repo] = match - return fetchSourceFactsFromGitHub(user, repo) - } - - /* Try to match GitLab repository */ - match = url.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i) - if (match) { - const [, base, slug] = match - return fetchSourceFactsFromGitLab(base, slug) - } - - /* Fallback */ - return EMPTY -} diff --git a/src/templates/assets/javascripts/components/source/facts/github/index.ts b/src/templates/assets/javascripts/components/source/facts/github/index.ts deleted file mode 100644 index 12cc55e0..00000000 --- a/src/templates/assets/javascripts/components/source/facts/github/index.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { Repo, User } from "github-types" -import { - EMPTY, - Observable, - catchError, - defaultIfEmpty, - map, - zip -} from "rxjs" - -import { requestJSON } from "~/browser" - -import { SourceFacts } from "../_" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * GitHub release (partial) - */ -interface Release { - tag_name: string /* Tag name */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch GitHub repository facts - * - * @param user - GitHub user or organization - * @param repo - GitHub repository - * - * @returns Repository facts observable - */ -export function fetchSourceFactsFromGitHub( - user: string, repo?: string -): Observable<SourceFacts> { - if (typeof repo !== "undefined") { - const url = `https://api.github.com/repos/${user}/${repo}` - return zip( - - /* Fetch version */ - requestJSON<Release>(`${url}/releases/latest`) - .pipe( - catchError(() => EMPTY), // @todo refactor instant loading - map(release => ({ - version: release.tag_name - })), - defaultIfEmpty({}) - ), - - /* Fetch stars and forks */ - requestJSON<Repo>(url) - .pipe( - catchError(() => EMPTY), // @todo refactor instant loading - map(info => ({ - stars: info.stargazers_count, - forks: info.forks_count - })), - defaultIfEmpty({}) - ) - ) - .pipe( - map(([release, info]) => ({ ...release, ...info })) - ) - - /* User or organization */ - } else { - const url = `https://api.github.com/users/${user}` - return requestJSON<User>(url) - .pipe( - map(info => ({ - repositories: info.public_repos - })), - defaultIfEmpty({}) - ) - } -} diff --git a/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts b/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts deleted file mode 100644 index d85d4afd..00000000 --- a/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { ProjectSchema } from "gitlab" -import { - EMPTY, - Observable, - catchError, - defaultIfEmpty, - map -} from "rxjs" - -import { requestJSON } from "~/browser" - -import { SourceFacts } from "../_" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch GitLab repository facts - * - * @param base - GitLab base - * @param project - GitLab project - * - * @returns Repository facts observable - */ -export function fetchSourceFactsFromGitLab( - base: string, project: string -): Observable<SourceFacts> { - const url = `https://${base}/api/v4/projects/${encodeURIComponent(project)}` - return requestJSON<ProjectSchema>(url) - .pipe( - catchError(() => EMPTY), // @todo refactor instant loading - map(({ star_count, forks_count }) => ({ - stars: star_count, - forks: forks_count - })), - defaultIfEmpty({}) - ) -} diff --git a/src/templates/assets/javascripts/components/source/facts/index.ts b/src/templates/assets/javascripts/components/source/facts/index.ts deleted file mode 100644 index f9bda64d..00000000 --- a/src/templates/assets/javascripts/components/source/facts/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./github" -export * from "./gitlab" diff --git a/src/templates/assets/javascripts/components/source/index.ts b/src/templates/assets/javascripts/components/source/index.ts deleted file mode 100644 index 7fac4813..00000000 --- a/src/templates/assets/javascripts/components/source/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./facts" diff --git a/src/templates/assets/javascripts/components/tabs/index.ts b/src/templates/assets/javascripts/components/tabs/index.ts deleted file mode 100644 index 1e69df28..00000000 --- a/src/templates/assets/javascripts/components/tabs/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - defer, - distinctUntilKeyChanged, - finalize, - map, - of, - switchMap, - tap -} from "rxjs" - -import { feature } from "~/_" -import { - Viewport, - watchElementSize, - watchViewportAt -} from "~/browser" - -import { Component } from "../_" -import { Header } from "../header" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Navigation tabs - */ -export interface Tabs { - hidden: boolean /* Navigation tabs are hidden */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch navigation tabs - * - * @param el - Navigation tabs element - * @param options - Options - * - * @returns Navigation tabs observable - */ -export function watchTabs( - el: HTMLElement, { viewport$, header$ }: WatchOptions -): Observable<Tabs> { - return watchElementSize(document.body) - .pipe( - switchMap(() => watchViewportAt(el, { header$, viewport$ })), - map(({ offset: { y } }) => { - return { - hidden: y >= 10 - } - }), - distinctUntilKeyChanged("hidden") - ) -} - -/** - * Mount navigation tabs - * - * This function hides the navigation tabs when scrolling past the threshold - * and makes them reappear in a nice CSS animation when scrolling back up. - * - * @param el - Navigation tabs element - * @param options - Options - * - * @returns Navigation tabs component observable - */ -export function mountTabs( - el: HTMLElement, options: MountOptions -): Observable<Component<Tabs>> { - return defer(() => { - const push$ = new Subject<Tabs>() - push$.subscribe({ - - /* Handle emission */ - next({ hidden }) { - el.hidden = hidden - }, - - /* Handle complete */ - complete() { - el.hidden = false - } - }) - - /* Create and return component */ - return ( - feature("navigation.tabs.sticky") - ? of({ hidden: false }) - : watchTabs(el, options) - ) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/toc/index.ts b/src/templates/assets/javascripts/components/toc/index.ts deleted file mode 100644 index 04b8d85f..00000000 --- a/src/templates/assets/javascripts/components/toc/index.ts +++ /dev/null @@ -1,379 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - asyncScheduler, - bufferCount, - combineLatestWith, - debounceTime, - defer, - distinctUntilChanged, - distinctUntilKeyChanged, - endWith, - filter, - finalize, - ignoreElements, - map, - merge, - observeOn, - of, - repeat, - scan, - share, - skip, - startWith, - switchMap, - takeUntil, - tap, - withLatestFrom -} from "rxjs" - -import { feature } from "~/_" -import { - Viewport, - getElement, - getElementContainer, - getElementSize, - getElements, - getLocation, - getOptionalElement, - watchElementSize -} from "~/browser" - -import { - Component, - getComponentElement -} from "../_" -import { Header } from "../header" -import { Main } from "../main" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Table of contents - */ -export interface TableOfContents { - prev: HTMLAnchorElement[][] /* Anchors (previous) */ - next: HTMLAnchorElement[][] /* Anchors (next) */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ - main$: Observable<Main> /* Main area observable */ - target$: Observable<HTMLElement> /* Location target observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch table of contents - * - * This is effectively a scroll spy implementation which will account for the - * fixed header and automatically re-calculate anchor offsets when the viewport - * is resized. The returned observable will only emit if the table of contents - * needs to be repainted. - * - * This implementation tracks an anchor element's entire path starting from its - * level up to the top-most anchor element, e.g. `[h3, h2, h1]`. Although the - * Material theme currently doesn't make use of this information, it enables - * the styling of the entire hierarchy through customization. - * - * Note that the current anchor is the last item of the `prev` anchor list. - * - * @param el - Table of contents element - * @param options - Options - * - * @returns Table of contents observable - */ -export function watchTableOfContents( - el: HTMLElement, { viewport$, header$ }: WatchOptions -): Observable<TableOfContents> { - const table = new Map<HTMLAnchorElement, HTMLElement>() - - /* Compute anchor-to-target mapping */ - const anchors = getElements<HTMLAnchorElement>("[href^=\\#]", el) - for (const anchor of anchors) { - const id = decodeURIComponent(anchor.hash.substring(1)) - const target = getOptionalElement(`[id="${id}"]`) - if (typeof target !== "undefined") - table.set(anchor, target) - } - - /* Compute necessary adjustment for header */ - const adjust$ = header$ - .pipe( - distinctUntilKeyChanged("height"), - map(({ height }) => { - const main = getComponentElement("main") - const grid = getElement(":scope > :first-child", main) - return height + 0.8 * ( - grid.offsetTop - - main.offsetTop - ) - }), - share() - ) - - /* Compute partition of previous and next anchors */ - const partition$ = watchElementSize(document.body) - .pipe( - distinctUntilKeyChanged("height"), - - /* Build index to map anchor paths to vertical offsets */ - switchMap(body => defer(() => { - let path: HTMLAnchorElement[] = [] - return of([...table].reduce((index, [anchor, target]) => { - while (path.length) { - const last = table.get(path[path.length - 1])! - if (last.tagName >= target.tagName) { - path.pop() - } else { - break - } - } - - /* If the current anchor is hidden, continue with its parent */ - let offset = target.offsetTop - while (!offset && target.parentElement) { - target = target.parentElement - offset = target.offsetTop - } - - /* Fix anchor offsets in tables - see https://bit.ly/3CUFOcn */ - let parent = target.offsetParent as HTMLElement - for (; parent; parent = parent.offsetParent as HTMLElement) - offset += parent.offsetTop - - /* Map reversed anchor path to vertical offset */ - return index.set( - [...path = [...path, anchor]].reverse(), - offset - ) - }, new Map<HTMLAnchorElement[], number>())) - }) - .pipe( - - /* Sort index by vertical offset (see https://bit.ly/30z6QSO) */ - map(index => new Map([...index].sort(([, a], [, b]) => a - b))), - combineLatestWith(adjust$), - - /* Re-compute partition when viewport offset changes */ - switchMap(([index, adjust]) => viewport$ - .pipe( - scan(([prev, next], { offset: { y }, size }) => { - const last = y + size.height >= Math.floor(body.height) - - /* Look forward */ - while (next.length) { - const [, offset] = next[0] - if (offset - adjust < y || last) { - prev = [...prev, next.shift()!] - } else { - break - } - } - - /* Look backward */ - while (prev.length) { - const [, offset] = prev[prev.length - 1] - if (offset - adjust >= y && !last) { - next = [prev.pop()!, ...next] - } else { - break - } - } - - /* Return partition */ - return [prev, next] - }, [[], [...index]]), - distinctUntilChanged((a, b) => ( - a[0] === b[0] && - a[1] === b[1] - )) - ) - ) - ) - ) - ) - - /* Compute and return anchor list migrations */ - return partition$ - .pipe( - map(([prev, next]) => ({ - prev: prev.map(([path]) => path), - next: next.map(([path]) => path) - })), - - /* Extract anchor list migrations */ - startWith({ prev: [], next: [] }), - bufferCount(2, 1), - map(([a, b]) => { - - /* Moving down */ - if (a.prev.length < b.prev.length) { - return { - prev: b.prev.slice(Math.max(0, a.prev.length - 1), b.prev.length), - next: [] - } - - /* Moving up */ - } else { - return { - prev: b.prev.slice(-1), - next: b.next.slice(0, b.next.length - a.next.length) - } - } - }) - ) -} - -/* ------------------------------------------------------------------------- */ - -/** - * Mount table of contents - * - * @param el - Table of contents element - * @param options - Options - * - * @returns Table of contents component observable - */ -export function mountTableOfContents( - el: HTMLElement, { viewport$, header$, main$, target$ }: MountOptions -): Observable<Component<TableOfContents>> { - return defer(() => { - const push$ = new Subject<TableOfContents>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$.subscribe(({ prev, next }) => { - - /* Look forward */ - for (const [anchor] of next) { - anchor.classList.remove("md-nav__link--passed") - anchor.classList.remove("md-nav__link--active") - } - - /* Look backward */ - for (const [index, [anchor]] of prev.entries()) { - anchor.classList.add("md-nav__link--passed") - anchor.classList.toggle( - "md-nav__link--active", - index === prev.length - 1 - ) - } - }) - - /* Set up following, if enabled */ - if (feature("toc.follow")) { - - /* Toggle smooth scrolling only for anchor clicks */ - const smooth$ = merge( - viewport$.pipe(debounceTime(1), map(() => undefined)), - viewport$.pipe(debounceTime(250), map(() => "smooth" as const)) - ) - - /* Bring active anchor into view */ // @todo: refactor - push$ - .pipe( - filter(({ prev }) => prev.length > 0), - combineLatestWith(main$.pipe(observeOn(asyncScheduler))), - withLatestFrom(smooth$) - ) - .subscribe(([[{ prev }], behavior]) => { - const [anchor] = prev[prev.length - 1] - if (anchor.offsetHeight) { - - /* Retrieve overflowing container and scroll */ - const container = getElementContainer(anchor) - if (typeof container !== "undefined") { - const offset = anchor.offsetTop - container.offsetTop - const { height } = getElementSize(container) - container.scrollTo({ - top: offset - height / 2, - behavior - }) - } - } - }) - } - - /* Set up anchor tracking, if enabled */ - if (feature("navigation.tracking")) - viewport$ - .pipe( - takeUntil(done$), - distinctUntilKeyChanged("offset"), - debounceTime(250), - skip(1), - takeUntil(target$.pipe(skip(1))), - repeat({ delay: 250 }), - withLatestFrom(push$) - ) - .subscribe(([, { prev }]) => { - const url = getLocation() - - /* Set hash fragment to active anchor */ - const anchor = prev[prev.length - 1] - if (anchor && anchor.length) { - const [active] = anchor - const { hash } = new URL(active.href) - if (url.hash !== hash) { - url.hash = hash - history.replaceState({}, "", `${url}`) - } - - /* Reset anchor when at the top */ - } else { - url.hash = "" - history.replaceState({}, "", `${url}`) - } - }) - - /* Create and return component */ - return watchTableOfContents(el, { viewport$, header$ }) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/top/index.ts b/src/templates/assets/javascripts/components/top/index.ts deleted file mode 100644 index 82e88b61..00000000 --- a/src/templates/assets/javascripts/components/top/index.ts +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - Subject, - bufferCount, - combineLatest, - distinctUntilChanged, - distinctUntilKeyChanged, - endWith, - finalize, - fromEvent, - ignoreElements, - map, - repeat, - skip, - takeUntil, - tap -} from "rxjs" - -import { Viewport } from "~/browser" - -import { Component } from "../_" -import { Header } from "../header" -import { Main } from "../main" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Back-to-top button - */ -export interface BackToTop { - hidden: boolean /* Back-to-top button is hidden */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - main$: Observable<Main> /* Main area observable */ - target$: Observable<HTMLElement> /* Location target observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - header$: Observable<Header> /* Header observable */ - main$: Observable<Main> /* Main area observable */ - target$: Observable<HTMLElement> /* Location target observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch back-to-top - * - * @param _el - Back-to-top element - * @param options - Options - * - * @returns Back-to-top observable - */ -export function watchBackToTop( - _el: HTMLElement, { viewport$, main$, target$ }: WatchOptions -): Observable<BackToTop> { - - /* Compute direction */ - const direction$ = viewport$ - .pipe( - map(({ offset: { y } }) => y), - bufferCount(2, 1), - map(([a, b]) => a > b && b > 0), - distinctUntilChanged() - ) - - /* Compute whether main area is active */ - const active$ = main$ - .pipe( - map(({ active }) => active) - ) - - /* Compute threshold for hiding */ - return combineLatest([active$, direction$]) - .pipe( - map(([active, direction]) => !(active && direction)), - distinctUntilChanged(), - takeUntil(target$.pipe(skip(1))), - endWith(true), - repeat({ delay: 250 }), - map(hidden => ({ hidden })) - ) -} - -/* ------------------------------------------------------------------------- */ - -/** - * Mount back-to-top - * - * @param el - Back-to-top element - * @param options - Options - * - * @returns Back-to-top component observable - */ -export function mountBackToTop( - el: HTMLElement, { viewport$, header$, main$, target$ }: MountOptions -): Observable<Component<BackToTop>> { - const push$ = new Subject<BackToTop>() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$.subscribe({ - - /* Handle emission */ - next({ hidden }) { - el.hidden = hidden - if (hidden) { - el.setAttribute("tabindex", "-1") - el.blur() - } else { - el.removeAttribute("tabindex") - } - }, - - /* Handle complete */ - complete() { - el.style.top = "" - el.hidden = true - el.removeAttribute("tabindex") - } - }) - - /* Watch header height */ - header$ - .pipe( - takeUntil(done$), - distinctUntilKeyChanged("height") - ) - .subscribe(({ height }) => { - el.style.top = `${height + 16}px` - }) - - /* Go back to top */ - fromEvent(el, "click") - .subscribe(ev => { - ev.preventDefault() - window.scrollTo({ top: 0 }) - }) - - /* Create and return component */ - return watchBackToTop(el, { viewport$, main$, target$ }) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) -} diff --git a/src/templates/assets/javascripts/integrations/clipboard/index.ts b/src/templates/assets/javascripts/integrations/clipboard/index.ts deleted file mode 100644 index cf46f601..00000000 --- a/src/templates/assets/javascripts/integrations/clipboard/index.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 ClipboardJS from "clipboard" -import { - Observable, - Subject, - map, - tap -} from "rxjs" - -import { translation } from "~/_" -import { getElement } from "~/browser" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Setup options - */ -interface SetupOptions { - alert$: Subject<string> /* Alert subject */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Extract text to copy - * - * @param el - HTML element - * - * @returns Extracted text - */ -function extract(el: HTMLElement): string { - el.setAttribute("data-md-copying", "") - const copy = el.closest("[data-copy]") - const text = copy - ? copy.getAttribute("data-copy")! - : el.innerText - el.removeAttribute("data-md-copying") - return text -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Set up Clipboard.js integration - * - * @param options - Options - */ -export function setupClipboardJS( - { alert$ }: SetupOptions -): void { - if (ClipboardJS.isSupported()) { - new Observable<ClipboardJS.Event>(subscriber => { - new ClipboardJS("[data-clipboard-target], [data-clipboard-text]", { - text: el => ( - el.getAttribute("data-clipboard-text")! || - extract(getElement( - el.getAttribute("data-clipboard-target")! - )) - ) - }) - .on("success", ev => subscriber.next(ev)) - }) - .pipe( - tap(ev => { - const trigger = ev.trigger as HTMLElement - trigger.focus() - }), - map(() => translation("clipboard.copied")) - ) - .subscribe(alert$) - } -} diff --git a/src/templates/assets/javascripts/integrations/index.ts b/src/templates/assets/javascripts/integrations/index.ts deleted file mode 100644 index 5d91a9d5..00000000 --- a/src/templates/assets/javascripts/integrations/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./clipboard" -export * from "./instant" -export * from "./search" -export * from "./sitemap" -export * from "./version" diff --git a/src/templates/assets/javascripts/integrations/instant/.eslintrc b/src/templates/assets/javascripts/integrations/instant/.eslintrc deleted file mode 100644 index 5adf108a..00000000 --- a/src/templates/assets/javascripts/integrations/instant/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "no-self-assign": "off", - "no-null/no-null": "off" - } -} diff --git a/src/templates/assets/javascripts/integrations/instant/index.ts b/src/templates/assets/javascripts/integrations/instant/index.ts deleted file mode 100644 index d321b751..00000000 --- a/src/templates/assets/javascripts/integrations/instant/index.ts +++ /dev/null @@ -1,446 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - EMPTY, - Observable, - Subject, - bufferCount, - catchError, - concat, - debounceTime, - distinctUntilKeyChanged, - endWith, - filter, - fromEvent, - ignoreElements, - map, - of, - sample, - share, - skip, - startWith, - switchMap, - take, - withLatestFrom -} from "rxjs" - -import { configuration, feature } from "~/_" -import { - Viewport, - getElement, - getElements, - getLocation, - getOptionalElement, - request, - setLocation, - setLocationHash -} from "~/browser" -import { getComponentElement } from "~/components" - -import { fetchSitemap } from "../sitemap" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Setup options - */ -interface SetupOptions { - location$: Subject<URL> // Location subject - viewport$: Observable<Viewport> // Viewport observable - progress$: Subject<number> // Progress suject -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Create a map of head elements for lookup and replacement - * - * @param head - Document head - * - * @returns Element map - */ -function lookup(head: HTMLHeadElement): Map<string, HTMLElement> { - - // @todo When resolving URLs, we must make sure to use the correct base for - // resolution. The next time we refactor instant loading, we should use the - // location subject as a source, which is also used for anchor links tracking, - // but for now we just rely on canonical. - const canonical = getElement<HTMLLinkElement>("[rel=canonical]", head) - canonical.href = canonical.href.replace("//localhost:", "//127.0.0.1") - - // Create tag map and index elements in head - const tags = new Map<string, HTMLElement>() - for (const el of getElements(":scope > *", head)) { - let html = el.outerHTML - - // If the current element is a style sheet or script, we must resolve the - // URL relative to the current location and make it absolute, so it's easy - // to deduplicate it later on by comparing the outer HTML of tags. We must - // keep identical style sheets and scripts without replacing them. - for (const key of ["href", "src"]) { - const value = el.getAttribute(key)! - if (value === null) - continue - - // Resolve URL relative to current location - const url = new URL(value, canonical.href) - const ref = el.cloneNode() as HTMLElement - - // Set resolved URL and retrieve HTML for deduplication - ref.setAttribute(key, `${url}`) - html = ref.outerHTML - break - } - - // Index element in tag map - tags.set(html, el) - } - - // Return tag map - return tags -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Set up instant navigation - * - * This is a heavily orchestrated operation - see inline comments to learn how - * this works with Material for MkDocs, and how you can hook into it. - * - * @param options - Options - * - * @returns Document observable - */ -export function setupInstantNavigation( - { location$, viewport$, progress$ }: SetupOptions -): Observable<Document> { - const config = configuration() - if (location.protocol === "file:") - return EMPTY - - // Load sitemap immediately, so we have it available when the user initiates - // the first instant navigation request, and canonicalize URLs to the current - // base URL. The base URL will remain stable in between loads, as it's only - // read at the first initialization of the application. - const sitemap$ = fetchSitemap() - .pipe( - map(paths => paths.map(path => `${new URL(path, config.base)}`)) - ) - - // Intercept inter-site navigation - to keep the number of event listeners - // low we use the fact that uncaptured events bubble up to the body. This also - // has the nice property that we don't need to detach and then again attach - // event listeners when instant navigation occurs. - const instant$ = fromEvent<MouseEvent>(document.body, "click") - .pipe( - withLatestFrom(sitemap$), - switchMap(([ev, sitemap]) => { - if (!(ev.target instanceof Element)) - return EMPTY - - // Skip, as target is not within a link - clicks on non-link elements - // are also captured, which we need to exclude from processing - const el = ev.target.closest("a") - if (el === null) - return EMPTY - - // Skip, as link opens in new window - we now know we have captured a - // click on a link, but the link either has a `target` property defined, - // or the user pressed the `meta` or `ctrl` key to open it in a new - // window. Thus, we need to filter those events, too. - if (el.target || ev.metaKey || ev.ctrlKey) - return EMPTY - - // Next, we must check if the URL is relevant for us, i.e., if it's an - // internal link to a page that is managed by MkDocs. Only then we can - // be sure that the structure of the page to be loaded adheres to the - // current document structure and can subsequently be injected into it - // without doing a full reload. For this reason, we must canonicalize - // the URL by removing all search parameters and hash fragments. - const url = new URL(el.href) - url.search = url.hash = "" - - // Skip, if URL is not included in the sitemap - this could be the case - // when linking between versions or languages, or to another page that - // the author included as part of the build, but that is not managed by - // MkDocs. In that case we must not continue with instant navigation. - if (!sitemap.includes(`${url}`)) - return EMPTY - - // We now know that we have a link to an internal page, so we prevent - // the browser from navigation and emit the URL for instant navigation. - // Note that this also includes anchor links, which means we need to - // implement anchor positioning ourselves. The reason for this is that - // if we wouldn't manage anchor links as well, scroll restoration will - // not work correctly (e.g. following an anchor link and scrolling). - ev.preventDefault() - return of(new URL(el.href)) - }), - share() - ) - - // Before fetching for the first time, resolve the absolute favicon position, - // as the browser will try to fetch the icon immediately - instant$.pipe(take(1)) - .subscribe(() => { - const favicon = getOptionalElement<HTMLLinkElement>("link[rel=icon]") - if (typeof favicon !== "undefined") - favicon.href = favicon.href - }) - - // Enable scroll restoration before window unloads - this is essential to - // ensure that full reloads (F5) restore the viewport offset correctly. If - // only popstate events wouldn't reset the scroll position prior to their - // emission, we could just reset this in popstate. Meh. - fromEvent(window, "beforeunload") - .subscribe(() => { - history.scrollRestoration = "auto" - }) - - // When an instant navigation event occurs, disable scroll restoration, since - // we must normalize and synchronize the behavior across all browsers. For - // instance, when the user clicks the back or forward button, the browser - // would immediately jump to the position of the previous document. - instant$.pipe(withLatestFrom(viewport$)) - .subscribe(([url, { offset }]) => { - history.scrollRestoration = "manual" - - // While it would be better UX to defer the history state change until the - // document was fully fetched and parsed, we must schedule it here, since - // popstate events are emitted when history state changes happen. Moreover - // we need to back up the current viewport offset, so we can restore it - // when popstate events occur, e.g., when the browser's back and forward - // buttons are used for navigation. - history.replaceState(offset, "") - history.pushState(null, "", url) - }) - - // Emit URL that should be fetched via instant navigation on location subject, - // which was passed into this function. Instant navigation can be intercepted - // by other parts of the application, which can synchronously back up or - // restore state before instant navigation happens. - instant$.subscribe(location$) - - // Fetch document - when fetching, we could use `responseType: document`, but - // since all MkDocs links are relative, we need to make sure that the current - // location matches the document we just loaded. Otherwise any relative links - // in the document might use the old location. If the request fails for some - // reason, we fall back to regular navigation and set the location explicitly, - // which will force-load the page. Furthermore, we must pre-warm the buffer - // for the duplicate check, or the first click on an anchor link will also - // trigger an instant navigation event, which doesn't make sense. - const response$ = location$ - .pipe( - startWith(getLocation()), - distinctUntilKeyChanged("pathname"), - skip(1), - switchMap(url => request(url, { progress$ }) - .pipe( - catchError(() => { - setLocation(url, true) - return EMPTY - }) - ) - ) - ) - - // Initialize the DOM parser, parse the returned HTML, and replace selected - // components before handing control down to the application - const dom = new DOMParser() - const document$ = response$ - .pipe( - switchMap(res => res.text()), - switchMap(res => { - const next = dom.parseFromString(res, "text/html") - for (const selector of [ - "[data-md-component=announce]", - "[data-md-component=container]", - "[data-md-component=header-topic]", - "[data-md-component=outdated]", - "[data-md-component=logo]", - "[data-md-component=skip]", - ...feature("navigation.tabs.sticky") - ? ["[data-md-component=tabs]"] - : [] - ]) { - const source = getOptionalElement(selector) - const target = getOptionalElement(selector, next) - if ( - typeof source !== "undefined" && - typeof target !== "undefined" - ) { - source.replaceWith(target) - } - } - - // Update meta tags - const source = lookup(document.head) - const target = lookup(next.head) - for (const [html, el] of target) { - - // Hack: skip stylesheets and scripts until we manage to replace them - // entirely in order to omit flashes of white content @todo refactor - if ( - el.getAttribute("rel") === "stylesheet" || - el.hasAttribute("src") - ) - continue - - if (source.has(html)) { - source.delete(html) - } else { - document.head.appendChild(el) - } - } - - // Remove meta tags that are not present in the new document - for (const el of source.values()) - - // Hack: skip stylesheets and scripts until we manage to replace them - // entirely in order to omit flashes of white content @todo refactor - if ( - el.getAttribute("rel") === "stylesheet" || - el.hasAttribute("src") - ) - continue - else - el.remove() - - // After components and meta tags were replaced, re-evaluate scripts - // that were provided by the author as part of Markdown files - const container = getComponentElement("container") - return concat(getElements("script", container)) - .pipe( - switchMap(el => { - const script = next.createElement("script") - if (el.src) { - for (const name of el.getAttributeNames()) - script.setAttribute(name, el.getAttribute(name)!) - el.replaceWith(script) - - // Complete when script is loaded - return new Observable(observer => { - script.onload = () => observer.complete() - }) - - // Complete immediately - } else { - script.textContent = el.textContent - el.replaceWith(script) - return EMPTY - } - }), - ignoreElements(), - endWith(next) - ) - }), - share() - ) - - // Intercept popstate events, e.g. when using the browser's back and forward - // buttons, and emit new location for fetching and parsing - const popstate$ = fromEvent<PopStateEvent>(window, "popstate") - popstate$.pipe(map(getLocation)) - .subscribe(location$) - - // Intercept clicks on anchor links, and scroll document into position - as - // we disabled scroll restoration, we need to do this manually here - location$ - .pipe( - startWith(getLocation()), - bufferCount(2, 1), - filter(([prev, next]) => ( - prev.pathname === next.pathname && - prev.hash !== next.hash - )), - map(([, next]) => next) - ) - .subscribe(url => { - if (history.state !== null || !url.hash) { - window.scrollTo(0, history.state?.y ?? 0) - } else { - history.scrollRestoration = "auto" - setLocationHash(url.hash) - history.scrollRestoration = "manual" - } - }) - - // Intercept clicks on the same anchor link - we must use a distinct pipeline - // for this, or we'd end up in a loop, setting the hash again and again - location$ - .pipe( - sample(instant$), - startWith(getLocation()), - bufferCount(2, 1), - filter(([prev, next]) => ( - prev.pathname === next.pathname && - prev.hash === next.hash - )), - map(([, next]) => next) - ) - .subscribe(url => { - history.scrollRestoration = "auto" - setLocationHash(url.hash) - history.scrollRestoration = "manual" - - // Hack: we need to make sure that we don't end up with multiple history - // entries for the same anchor link, so we just remove the last entry - history.back() - }) - - // After parsing the document, check if the current history entry has a state. - // This may happen when users press the back or forward button to visit a page - // that was already seen. If there's no state, it means a new page was visited - // and we should scroll to the top, unless an anchor is given. - document$.pipe(withLatestFrom(location$)) - .subscribe(([, url]) => { - if (history.state !== null || !url.hash) { - window.scrollTo(0, history.state?.y ?? 0) - } else { - setLocationHash(url.hash) - } - }) - - // If the current history is not empty, register an event listener updating - // the current history state whenever the scroll position changes. This must - // be debounced and cannot be done in popstate, as popstate has already - // removed the entry from the history. - viewport$ - .pipe( - distinctUntilKeyChanged("offset"), - debounceTime(100) - ) - .subscribe(({ offset }) => { - history.replaceState(offset, "") - }) - - // Return document - return document$ -} diff --git a/src/templates/assets/javascripts/integrations/search/_/index.ts b/src/templates/assets/javascripts/integrations/search/_/index.ts deleted file mode 100644 index 0e217fa4..00000000 --- a/src/templates/assets/javascripts/integrations/search/_/index.ts +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - SearchDocument, - SearchIndex, - SearchOptions, - setupSearchDocumentMap -} from "../config" -import { - Position, - PositionTable, - highlight, - highlightAll, - tokenize -} from "../internal" -import { - SearchQueryTerms, - getSearchQueryTerms, - parseSearchQuery, - segment, - transformSearchQuery -} from "../query" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search item - */ -export interface SearchItem - extends SearchDocument -{ - score: number /* Score (relevance) */ - terms: SearchQueryTerms /* Search query terms */ -} - -/** - * Search result - */ -export interface SearchResult { - items: SearchItem[][] /* Search items */ - suggest?: string[] /* Search suggestions */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Create field extractor factory - * - * @param table - Position table map - * - * @returns Extractor factory - */ -function extractor(table: Map<string, PositionTable>) { - return (name: keyof SearchDocument) => { - return (doc: SearchDocument) => { - if (typeof doc[name] === "undefined") - return undefined - - /* Compute identifier and initialize table */ - const id = [doc.location, name].join(":") - table.set(id, lunr.tokenizer.table = []) - - /* Return field value */ - return doc[name] - } - } -} - -/** - * Compute the difference of two lists of strings - * - * @param a - 1st list of strings - * @param b - 2nd list of strings - * - * @returns Difference - */ -function difference(a: string[], b: string[]): string[] { - const [x, y] = [new Set(a), new Set(b)] - return [ - ...new Set([...x].filter(value => !y.has(value))) - ] -} - -/* ---------------------------------------------------------------------------- - * Class - * ------------------------------------------------------------------------- */ - -/** - * Search index - */ -export class Search { - - /** - * Search document map - */ - protected map: Map<string, SearchDocument> - - /** - * Search options - */ - protected options: SearchOptions - - /** - * The underlying Lunr.js search index - */ - protected index: lunr.Index - - /** - * Internal position table map - */ - protected table: Map<string, PositionTable> - - /** - * Create the search integration - * - * @param data - Search index - */ - public constructor({ config, docs, options }: SearchIndex) { - const field = extractor(this.table = new Map()) - - /* Set up document map and options */ - this.map = setupSearchDocumentMap(docs) - this.options = options - - /* Set up document index */ - this.index = lunr(function () { - this.metadataWhitelist = ["position"] - this.b(0) - - /* Set up (multi-)language support */ - if (config.lang.length === 1 && config.lang[0] !== "en") { - // @ts-expect-error - namespace indexing not supported - this.use(lunr[config.lang[0]]) - } else if (config.lang.length > 1) { - this.use(lunr.multiLanguage(...config.lang)) - } - - /* Set up custom tokenizer (must be after language setup) */ - this.tokenizer = tokenize as typeof lunr.tokenizer - lunr.tokenizer.separator = new RegExp(config.separator) - - /* Set up custom segmenter, if loaded */ - lunr.segmenter = "TinySegmenter" in lunr - ? new lunr.TinySegmenter() - : undefined - - /* Compute functions to be removed from the pipeline */ - const fns = difference([ - "trimmer", "stopWordFilter", "stemmer" - ], config.pipeline) - - /* Remove functions from the pipeline for registered languages */ - for (const lang of config.lang.map(language => ( - // @ts-expect-error - namespace indexing not supported - language === "en" ? lunr : lunr[language] - ))) - for (const fn of fns) { - this.pipeline.remove(lang[fn]) - this.searchPipeline.remove(lang[fn]) - } - - /* Set up index reference */ - this.ref("location") - - /* Set up index fields */ - this.field("title", { boost: 1e3, extractor: field("title") }) - this.field("text", { boost: 1e0, extractor: field("text") }) - this.field("tags", { boost: 1e6, extractor: field("tags") }) - - /* Add documents to index */ - for (const doc of docs) - this.add(doc, { boost: doc.boost }) - }) - } - - /** - * Search for matching documents - * - * @param query - Search query - * - * @returns Search result - */ - public search(query: string): SearchResult { - - // Experimental Chinese segmentation - query = query.replace(/\p{sc=Han}+/gu, value => { - return [...segment(value, this.index.invertedIndex)] - .join("* ") - }) - - // @todo: move segmenter (above) into transformSearchQuery - query = transformSearchQuery(query) - if (!query) - return { items: [] } - - /* Parse query to extract clauses for analysis */ - const clauses = parseSearchQuery(query) - .filter(clause => ( - clause.presence !== lunr.Query.presence.PROHIBITED - )) - - /* Perform search and post-process results */ - const groups = this.index.search(query) - - /* Apply post-query boosts based on title and search query terms */ - .reduce<SearchItem[]>((item, { ref, score, matchData }) => { - let doc = this.map.get(ref) - if (typeof doc !== "undefined") { - - /* Shallow copy document */ - doc = { ...doc } - if (doc.tags) - doc.tags = [...doc.tags] - - /* Compute and analyze search query terms */ - const terms = getSearchQueryTerms( - clauses, - Object.keys(matchData.metadata) - ) - - /* Highlight matches in fields */ - for (const field of this.index.fields) { - if (typeof doc[field] === "undefined") - continue - - /* Collect positions from matches */ - const positions: Position[] = [] - for (const match of Object.values(matchData.metadata)) - if (typeof match[field] !== "undefined") - positions.push(...match[field].position) - - /* Skip highlighting, if no positions were collected */ - if (!positions.length) - continue - - /* Load table and determine highlighting method */ - const table = this.table.get([doc.location, field].join(":"))! - const fn = Array.isArray(doc[field]) - ? highlightAll - : highlight - - // @ts-expect-error - stop moaning, TypeScript! - doc[field] = fn(doc[field], table, positions, field !== "text") - } - - /* Highlight title and text and apply post-query boosts */ - const boost = +!doc.parent + - Object.values(terms) - .filter(t => t).length / - Object.keys(terms).length - - /* Append item */ - item.push({ - ...doc, - score: score * (1 + boost ** 2), - terms - }) - } - return item - }, []) - - /* Sort search results again after applying boosts */ - .sort((a, b) => b.score - a.score) - - /* Group search results by article */ - .reduce((items, result) => { - const doc = this.map.get(result.location) - if (typeof doc !== "undefined") { - const ref = doc.parent - ? doc.parent.location - : doc.location - items.set(ref, [...items.get(ref) || [], result]) - } - return items - }, new Map<string, SearchItem[]>()) - - /* Ensure that every item set has an article */ - for (const [ref, items] of groups) - if (!items.find(item => item.location === ref)) { - const doc = this.map.get(ref)! - items.push({ ...doc, score: 0, terms: {} }) - } - - /* Generate search suggestions, if desired */ - let suggest: string[] | undefined - if (this.options.suggest) { - const titles = this.index.query(builder => { - for (const clause of clauses) - builder.term(clause.term, { - fields: ["title"], - presence: lunr.Query.presence.REQUIRED, - wildcard: lunr.Query.wildcard.TRAILING - }) - }) - - /* Retrieve suggestions for best match */ - suggest = titles.length - ? Object.keys(titles[0].matchData.metadata) - : [] - } - - /* Return search result */ - return { - items: [...groups.values()], - ...typeof suggest !== "undefined" && { suggest } - } - } -} diff --git a/src/templates/assets/javascripts/integrations/search/config/index.ts b/src/templates/assets/javascripts/integrations/search/config/index.ts deleted file mode 100644 index 3d88d1c6..00000000 --- a/src/templates/assets/javascripts/integrations/search/config/index.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 configuration - */ -export interface SearchConfig { - lang: string[] /* Search languages */ - separator: string /* Search separator */ - pipeline: SearchPipelineFn[] /* Search pipeline */ -} - -/** - * Search document - */ -export interface SearchDocument { - location: string /* Document location */ - title: string /* Document title */ - text: string /* Document text */ - tags?: string[] /* Document tags */ - boost?: number /* Document boost */ - parent?: SearchDocument /* Document parent */ -} - -/** - * Search options - */ -export interface SearchOptions { - suggest: boolean /* Search suggestions */ -} - -/* ------------------------------------------------------------------------- */ - -/** - * Search index - */ -export interface SearchIndex { - config: SearchConfig /* Search configuration */ - docs: SearchDocument[] /* Search documents */ - options: SearchOptions /* Search options */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Search pipeline function - */ -type SearchPipelineFn = - | "trimmer" /* Trimmer */ - | "stopWordFilter" /* Stop word filter */ - | "stemmer" /* Stemmer */ - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Create a search document map - * - * This function creates a mapping of URLs (including anchors) to the actual - * articles and sections. It relies on the invariant that the search index is - * ordered with the main article appearing before all sections with anchors. - * If this is not the case, the logic music be changed. - * - * @param docs - Search documents - * - * @returns Search document map - */ -export function setupSearchDocumentMap( - docs: SearchDocument[] -): Map<string, SearchDocument> { - const map = new Map<string, SearchDocument>() - for (const doc of docs) { - const [path] = doc.location.split("#") - - /* Add document article */ - const article = map.get(path) - if (typeof article === "undefined") { - map.set(path, doc) - - /* Add document section */ - } else { - map.set(doc.location, doc) - doc.parent = article - } - } - - /* Return search document map */ - return map -} diff --git a/src/templates/assets/javascripts/integrations/search/highlighter/index.ts b/src/templates/assets/javascripts/integrations/search/highlighter/index.ts deleted file mode 100644 index 0fcbb19e..00000000 --- a/src/templates/assets/javascripts/integrations/search/highlighter/index.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 escapeHTML from "escape-html" - -import { SearchConfig } from "../config" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search highlight function - * - * @param value - Value - * - * @returns Highlighted value - */ -export type SearchHighlightFn = (value: string) => string - -/** - * Search highlight factory function - * - * @param query - Query value - * - * @returns Search highlight function - */ -export type SearchHighlightFactoryFn = (query: string) => SearchHighlightFn - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Create a search highlighter - * - * @param config - Search configuration - * - * @returns Search highlight factory function - */ -export function setupSearchHighlighter( - config: SearchConfig -): SearchHighlightFactoryFn { - // Hack: temporarily remove pure lookaheads and lookbehinds - const regex = config.separator.split("|").map(term => { - const temp = term.replace(/(\(\?[!=<][^)]+\))/g, "") - return temp.length === 0 ? "�" : term - }) - .join("|") - - const separator = new RegExp(regex, "img") - const highlight = (_: unknown, data: string, term: string) => { - return `${data}<mark data-md-highlight>${term}</mark>` - } - - /* Return factory function */ - return (query: string) => { - query = query - .replace(/[\s*+\-:~^]+/g, " ") - .trim() - - /* Create search term match expression */ - const match = new RegExp(`(^|${config.separator}|)(${ - query - .replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&") - .replace(separator, "|") - })`, "img") - - /* Highlight string value */ - return value => escapeHTML(value) - .replace(match, highlight) - .replace(/<\/mark>(\s+)<mark[^>]*>/img, "$1") - } -} diff --git a/src/templates/assets/javascripts/integrations/search/index.ts b/src/templates/assets/javascripts/integrations/search/index.ts deleted file mode 100644 index 94c010bb..00000000 --- a/src/templates/assets/javascripts/integrations/search/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./config" -export * from "./highlighter" -export * from "./query" -export * from "./worker" diff --git a/src/templates/assets/javascripts/integrations/search/internal/.eslintrc b/src/templates/assets/javascripts/integrations/search/internal/.eslintrc deleted file mode 100644 index 9368ceb6..00000000 --- a/src/templates/assets/javascripts/integrations/search/internal/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "no-fallthrough": "off", - "no-underscore-dangle": "off" - } -} diff --git a/src/templates/assets/javascripts/integrations/search/internal/_/index.ts b/src/templates/assets/javascripts/integrations/search/internal/_/index.ts deleted file mode 100644 index ae8f6104..00000000 --- a/src/templates/assets/javascripts/integrations/search/internal/_/index.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2016-2023 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. - */ - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Visitor function - * - * @param start - Start offset - * @param end - End offset - */ -type VisitorFn = ( - start: number, end: number -) => void - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Split a string using the given separator - * - * @param input - Input value - * @param separator - Separator - * @param fn - Visitor function - */ -export function split( - input: string, separator: RegExp, fn: VisitorFn -): void { - separator = new RegExp(separator, "g") - - /* Split string using separator */ - let match: RegExpExecArray | null - let index = 0 - do { - match = separator.exec(input) - - /* Emit non-empty range */ - const until = match?.index ?? input.length - if (index < until) - fn(index, until) - - /* Update last index */ - if (match) { - const [term] = match - index = match.index + term.length - - /* Support zero-length lookaheads */ - if (term.length === 0) - separator.lastIndex = match.index + 1 - } - } while (match) -} diff --git a/src/templates/assets/javascripts/integrations/search/internal/extract/index.ts b/src/templates/assets/javascripts/integrations/search/internal/extract/index.ts deleted file mode 100644 index 2a98b9e1..00000000 --- a/src/templates/assets/javascripts/integrations/search/internal/extract/index.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 - * ------------------------------------------------------------------------- */ - -/** - * Extraction type - * - * This type defines the possible values that are encoded into the first two - * bits of a section that is part of the blocks of a tokenization table. There - * are three types of interest: HTML opening and closing tags, as well as the - * actual text content we need to extract for indexing. - */ -export const enum Extract { - TAG_OPEN = 0, /* HTML opening tag */ - TEXT = 1, /* Text content */ - TAG_CLOSE = 2 /* HTML closing tag */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Visitor function - * - * @param block - Block index - * @param type - Extraction type - * @param start - Start offset - * @param end - End offset - */ -type VisitorFn = ( - block: number, type: Extract, start: number, end: number -) => void - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Split a string into markup and text sections - * - * This function scans a string and divides it up into sections of markup and - * text. For each section, it invokes the given visitor function with the block - * index, extraction type, as well as start and end offsets. Using a visitor - * function (= streaming data) is ideal for minimizing pressure on the GC. - * - * @param input - Input value - * @param fn - Visitor function - */ -export function extract( - input: string, fn: VisitorFn -): void { - - let block = 0 /* Current block */ - let start = 0 /* Current start offset */ - let end = 0 /* Current end offset */ - - /* Split string into sections */ - for (let stack = 0; end < input.length; end++) { - - /* Opening tag after non-empty section */ - if (input.charAt(end) === "<" && end > start) { - fn(block, Extract.TEXT, start, start = end) - - /* Closing tag */ - } else if (input.charAt(end) === ">") { - if (input.charAt(start + 1) === "/") { - if (--stack === 0) - fn(block++, Extract.TAG_CLOSE, start, end + 1) - - /* Tag is not self-closing */ - } else if (input.charAt(end - 1) !== "/") { - if (stack++ === 0) - fn(block, Extract.TAG_OPEN, start, end + 1) - } - - /* New section */ - start = end + 1 - } - } - - /* Add trailing section */ - if (end > start) - fn(block, Extract.TEXT, start, end) -} diff --git a/src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts b/src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts deleted file mode 100644 index 7cc3bf1a..00000000 --- a/src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 - * ------------------------------------------------------------------------- */ - -/** - * Position table - */ -export type PositionTable = number[][] - -/** - * Position - */ -export type Position = number - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Highlight all occurrences in a string - * - * This function receives a field's value (e.g. like `title` or `text`), it's - * position table that was generated during indexing, and the positions found - * when executing the query. It then highlights all occurrences, and returns - * their concatenation. In case of multiple blocks, two are returned. - * - * @param input - Input value - * @param table - Table for indexing - * @param positions - Occurrences - * @param full - Full results - * - * @returns Highlighted string value - */ -export function highlight( - input: string, table: PositionTable, positions: Position[], full = false -): string { - return highlightAll([input], table, positions, full).pop()! -} - -/** - * Highlight all occurrences in a set of strings - * - * @param inputs - Input values - * @param table - Table for indexing - * @param positions - Occurrences - * @param full - Full results - * - * @returns Highlighted string values - */ -export function highlightAll( - inputs: string[], table: PositionTable, positions: Position[], full = false -): string[] { - - /* Map blocks to input values */ - const mapping = [0] - for (let t = 1; t < table.length; t++) { - const prev = table[t - 1] - const next = table[t] - - /* Check if table points to new block */ - const p = prev[prev.length - 1] >>> 2 & 0x3FF - const q = next[0] >>> 12 - - /* Add block to mapping */ - mapping.push(+(p > q) + mapping[mapping.length - 1]) - } - - /* Highlight strings one after another */ - return inputs.map((input, i) => { - let cursor = 0 - - /* Map occurrences to blocks */ - const blocks = new Map<number, number[]>() - for (const p of positions.sort((a, b) => a - b)) { - const index = p & 0xFFFFF - const block = p >>> 20 - if (mapping[block] !== i) - continue - - /* Ensure presence of block group */ - let group = blocks.get(block) - if (typeof group === "undefined") - blocks.set(block, group = []) - - /* Add index to group */ - group.push(index) - } - - /* Just return string, if no occurrences */ - if (blocks.size === 0) - return input - - /* Compute slices */ - const slices: string[] = [] - for (const [block, indexes] of blocks) { - const t = table[block] - - /* Extract positions and length */ - const start = t[0] >>> 12 - const end = t[t.length - 1] >>> 12 - const length = t[t.length - 1] >>> 2 & 0x3FF - - /* Add prefix, if full results are desired */ - if (full && start > cursor) - slices.push(input.slice(cursor, start)) - - /* Extract and highlight slice */ - let slice = input.slice(start, end + length) - for (const j of indexes.sort((a, b) => b - a)) { - - /* Retrieve offset and length of match */ - const p = (t[j] >>> 12) - start - const q = (t[j] >>> 2 & 0x3FF) + p - - /* Wrap occurrence */ - slice = [ - slice.slice(0, p), - "<mark>", - slice.slice(p, q), - "</mark>", - slice.slice(q) - ].join("") - } - - /* Update cursor */ - cursor = end + length - - /* Append slice and abort if we have two */ - if (slices.push(slice) === 2) - break - } - - /* Add suffix, if full results are desired */ - if (full && cursor < input.length) - slices.push(input.slice(cursor)) - - /* Return highlighted slices */ - return slices.join("") - }) -} diff --git a/src/templates/assets/javascripts/integrations/search/internal/index.ts b/src/templates/assets/javascripts/integrations/search/internal/index.ts deleted file mode 100644 index c752329e..00000000 --- a/src/templates/assets/javascripts/integrations/search/internal/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./extract" -export * from "./highlight" -export * from "./tokenize" diff --git a/src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts b/src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts deleted file mode 100644 index f5089bc9..00000000 --- a/src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { split } from "../_" -import { - Extract, - extract -} from "../extract" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Split a string or set of strings into tokens - * - * This tokenizer supersedes the default tokenizer that is provided by Lunr.js, - * as it is aware of HTML tags and allows for multi-character splitting. - * - * It takes the given inputs, splits each of them into markup and text sections, - * tokenizes and segments (if necessary) each of them, and then indexes them in - * a table by using a compact bit representation. Bitwise techniques are used - * to write and read from the table during indexing and querying. - * - * @see https://bit.ly/3W3Xw4J - Search: better, faster, smaller - * - * @param input - Input value(s) - * - * @returns Tokens - */ -export function tokenize( - input?: string | string[] -): lunr.Token[] { - const tokens: lunr.Token[] = [] - if (typeof input === "undefined") - return tokens - - /* Tokenize strings one after another */ - const inputs = Array.isArray(input) ? input : [input] - for (let i = 0; i < inputs.length; i++) { - const table = lunr.tokenizer.table - const total = table.length - - /* Split string into sections and tokenize content blocks */ - extract(inputs[i], (block, type, start, end) => { - table[block += total] ||= [] - switch (type) { - - /* Handle markup */ - case Extract.TAG_OPEN: - case Extract.TAG_CLOSE: - table[block].push( - start << 12 | - end - start << 2 | - type - ) - break - - /* Handle text content */ - case Extract.TEXT: - const section = inputs[i].slice(start, end) - split(section, lunr.tokenizer.separator, (index, until) => { - - /** - * Apply segmenter after tokenization. Note that the segmenter will - * also split words at word boundaries, which is not what we want, - * so we need to check if we can somehow mitigate this behavior. - */ - if (typeof lunr.segmenter !== "undefined") { - const subsection = section.slice(index, until) - if (/^[MHIK]$/.test(lunr.segmenter.ctype_(subsection))) { - const segments = lunr.segmenter.segment(subsection) - for (let s = 0, l = 0; s < segments.length; s++) { - - /* Add block to section */ - table[block] ||= [] - table[block].push( - start + index + l << 12 | - segments[s].length << 2 | - type - ) - - /* Add token with position */ - tokens.push(new lunr.Token( - segments[s].toLowerCase(), { - position: block << 20 | table[block].length - 1 - } - )) - - /* Keep track of length */ - l += segments[s].length - } - return - } - } - - /* Add block to section */ - table[block].push( - start + index << 12 | - until - index << 2 | - type - ) - - /* Add token with position */ - tokens.push(new lunr.Token( - section.slice(index, until).toLowerCase(), { - position: block << 20 | table[block].length - 1 - } - )) - }) - } - }) - } - - /* Return tokens */ - return tokens -} diff --git a/src/templates/assets/javascripts/integrations/search/query/.eslintrc b/src/templates/assets/javascripts/integrations/search/query/.eslintrc deleted file mode 100644 index 3031c7e3..00000000 --- a/src/templates/assets/javascripts/integrations/search/query/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "no-control-regex": "off", - "@typescript-eslint/no-explicit-any": "off" - } -} diff --git a/src/templates/assets/javascripts/integrations/search/query/_/index.ts b/src/templates/assets/javascripts/integrations/search/query/_/index.ts deleted file mode 100644 index 14482e43..00000000 --- a/src/templates/assets/javascripts/integrations/search/query/_/index.ts +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { split } from "../../internal" -import { transform } from "../transform" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search query clause - */ -export interface SearchQueryClause { - presence: lunr.Query.presence /* Clause presence */ - term: string /* Clause term */ -} - -/* ------------------------------------------------------------------------- */ - -/** - * Search query terms - */ -export type SearchQueryTerms = Record<string, boolean> - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Transform search query - * - * This function lexes the given search query and applies the transformation - * function to each term, preserving markup like `+` and `-` modifiers. - * - * @param query - Search query - * - * @returns Search query - */ -export function transformSearchQuery( - query: string -): string { - - /* Split query terms with tokenizer */ - return transform(query, part => { - const terms: string[] = [] - - /* Initialize lexer and analyze part */ - const lexer = new lunr.QueryLexer(part) - lexer.run() - - /* Extract and tokenize term from lexeme */ - for (const { type, str: term, start, end } of lexer.lexemes) - switch (type) { - - /* Hack: remove colon - see https://bit.ly/3wD3T3I */ - case "FIELD": - if (!["title", "text", "tags"].includes(term)) - part = [ - part.slice(0, end), - " ", - part.slice(end + 1) - ].join("") - break - - /* Tokenize term */ - case "TERM": - split(term, lunr.tokenizer.separator, (...range) => { - terms.push([ - part.slice(0, start), - term.slice(...range), - part.slice(end) - ].join("")) - }) - } - - /* Return terms */ - return terms - }) -} - -/* ------------------------------------------------------------------------- */ - -/** - * Parse a search query for analysis - * - * Lunr.js itself has a bug where it doesn't detect or remove wildcards for - * query clauses, so we must do this here. - * - * @see https://bit.ly/3DpTGtz - GitHub issue - * - * @param value - Query value - * - * @returns Search query clauses - */ -export function parseSearchQuery( - value: string -): SearchQueryClause[] { - const query = new lunr.Query(["title", "text", "tags"]) - const parser = new lunr.QueryParser(value, query) - - /* Parse Search query */ - parser.parse() - for (const clause of query.clauses) { - clause.usePipeline = true - - /* Handle leading wildcard */ - if (clause.term.startsWith("*")) { - clause.wildcard = lunr.Query.wildcard.LEADING - clause.term = clause.term.slice(1) - } - - /* Handle trailing wildcard */ - if (clause.term.endsWith("*")) { - clause.wildcard = lunr.Query.wildcard.TRAILING - clause.term = clause.term.slice(0, -1) - } - } - - /* Return query clauses */ - return query.clauses -} - -/** - * Analyze the search query clauses in regard to the search terms found - * - * @param query - Search query clauses - * @param terms - Search terms - * - * @returns Search query terms - */ -export function getSearchQueryTerms( - query: SearchQueryClause[], terms: string[] -): SearchQueryTerms { - const clauses = new Set<SearchQueryClause>(query) - - /* Match query clauses against terms */ - const result: SearchQueryTerms = {} - for (let t = 0; t < terms.length; t++) - for (const clause of clauses) - if (terms[t].startsWith(clause.term)) { - result[clause.term] = true - clauses.delete(clause) - } - - /* Annotate unmatched non-stopword query clauses */ - for (const clause of clauses) - if (lunr.stopWordFilter?.(clause.term)) - result[clause.term] = false - - /* Return query terms */ - return result -} diff --git a/src/templates/assets/javascripts/integrations/search/query/index.ts b/src/templates/assets/javascripts/integrations/search/query/index.ts deleted file mode 100644 index 763e2fd4..00000000 --- a/src/templates/assets/javascripts/integrations/search/query/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./segment" -export * from "./transform" diff --git a/src/templates/assets/javascripts/integrations/search/query/segment/index.ts b/src/templates/assets/javascripts/integrations/search/query/segment/index.ts deleted file mode 100644 index b96796f4..00000000 --- a/src/templates/assets/javascripts/integrations/search/query/segment/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2016-2023 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. - */ - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Segment a search query using the inverted index - * - * This function implements a clever approach to text segmentation for Asian - * languages, as it used the information already available in the search index. - * The idea is to greedily segment the search query based on the tokens that are - * already part of the index, as described in the linked issue. - * - * @see https://bit.ly/3lwjrk7 - GitHub issue - * - * @param query - Query value - * @param index - Inverted index - * - * @returns Segmented query value - */ -export function segment( - query: string, index: object -): Iterable<string> { - const segments = new Set<string>() - - /* Segment search query */ - const wordcuts = new Uint16Array(query.length) - for (let i = 0; i < query.length; i++) - for (let j = i + 1; j < query.length; j++) { - const value = query.slice(i, j) - if (value in index) - wordcuts[i] = j - i - } - - /* Compute longest matches with minimum overlap */ - const stack = [0] - for (let s = stack.length; s > 0;) { - const p = stack[--s] - for (let q = 1; q < wordcuts[p]; q++) - if (wordcuts[p + q] > wordcuts[p] - q) { - segments.add(query.slice(p, p + q)) - stack[s++] = p + q - } - - /* Continue at end of query string */ - const q = p + wordcuts[p] - if (wordcuts[q] && q < query.length - 1) - stack[s++] = q - - /* Add current segment */ - segments.add(query.slice(p, q)) - } - - // @todo fix this case in the code block above, this is a hotfix - if (segments.has("")) - return new Set([query]) - - /* Return segmented query value */ - return segments -} diff --git a/src/templates/assets/javascripts/integrations/search/query/transform/index.ts b/src/templates/assets/javascripts/integrations/search/query/transform/index.ts deleted file mode 100644 index 41497786..00000000 --- a/src/templates/assets/javascripts/integrations/search/query/transform/index.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2016-2023 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. - */ - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Visitor function - * - * @param value - String value - * - * @returns String term(s) - */ -type VisitorFn = ( - value: string -) => string | string[] - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Default transformation function - * - * 1. Trim excess whitespace from left and right. - * - * 2. Search for parts in quotation marks and prepend a `+` modifier to denote - * that the resulting document must contain all parts, converting the query - * to an `AND` query (as opposed to the default `OR` behavior). While users - * may expect parts enclosed in quotation marks to map to span queries, i.e. - * for which order is important, Lunr.js doesn't support them, so the best - * we can do is to convert the parts to an `AND` query. - * - * 3. Replace control characters which are not located at the beginning of the - * query or preceded by white space, or are not followed by a non-whitespace - * character or are at the end of the query string. Furthermore, filter - * unmatched quotation marks. - * - * 4. Split the query string at whitespace, then pass each part to the visitor - * function for tokenization, and append a wildcard to every resulting term - * that is not explicitly marked with a `+`, `-`, `~` or `^` modifier, since - * it ensures consistent and stable ranking when multiple terms are entered. - * Also, if a fuzzy or boost modifier are given, but no numeric value has - * been entered, default to 1 to not induce a query error. - * - * @param query - Query value - * @param fn - Visitor function - * - * @returns Transformed query value - */ -export function transform( - query: string, fn: VisitorFn = term => term -): string { - return query - - /* => 1 */ - .trim() - - /* => 2 */ - .split(/"([^"]+)"/g) - .map((parts, index) => index & 1 - ? parts.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g, " +") - : parts - ) - .join("") - - /* => 3 */ - .replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g, "") - - /* => 4 */ - .split(/\s+/g) - .reduce((prev, term) => { - const next = fn(term) - return [...prev, ...Array.isArray(next) ? next : [next]] - }, [] as string[]) - .map(term => /([~^]$)/.test(term) ? `${term}1` : term) - .map(term => /(^[+-]|[~^]\d+$)/.test(term) ? term : `${term}*`) - .join(" ") -} diff --git a/src/templates/assets/javascripts/integrations/search/worker/_/index.ts b/src/templates/assets/javascripts/integrations/search/worker/_/index.ts deleted file mode 100644 index 26713573..00000000 --- a/src/templates/assets/javascripts/integrations/search/worker/_/index.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 RTICULAR 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 { - ObservableInput, - Subject, - first, - merge, - of, - switchMap -} from "rxjs" - -import { feature } from "~/_" -import { watchToggle, watchWorker } from "~/browser" - -import { SearchIndex } from "../../config" -import { - SearchMessage, - SearchMessageType -} from "../message" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Set up search worker - * - * This function creates and initializes a web worker that is used for search, - * so that the user interface doesn't freeze. In general, the application does - * not care how search is implemented, as long as the web worker conforms to - * the format expected by the application as defined in `SearchMessage`. This - * allows the author to implement custom search functionality, by providing a - * custom web worker via configuration. - * - * Material for MkDocs' built-in search implementation makes use of Lunr.js, an - * efficient and fast implementation for client-side search. Leveraging a tiny - * iframe-based web worker shim, search is even supported for the `file://` - * protocol, enabling search for local non-hosted builds. - * - * If the protocol is `file://`, search initialization is deferred to mitigate - * freezing, as it's now synchronous by design - see https://bit.ly/3C521EO - * - * @see https://bit.ly/3igvtQv - How to implement custom search - * - * @param url - Worker URL - * @param index$ - Search index observable input - * - * @returns Search worker - */ -export function setupSearchWorker( - url: string, index$: ObservableInput<SearchIndex> -): Subject<SearchMessage> { - const worker$ = watchWorker<SearchMessage>(url) - merge( - of(location.protocol !== "file:"), - watchToggle("search") - ) - .pipe( - first(active => active), - switchMap(() => index$) - ) - .subscribe(({ config, docs }) => worker$.next({ - type: SearchMessageType.SETUP, - data: { - config, - docs, - options: { - suggest: feature("search.suggest") - } - } - })) - - /* Return search worker */ - return worker$ -} diff --git a/src/templates/assets/javascripts/integrations/search/worker/index.ts b/src/templates/assets/javascripts/integrations/search/worker/index.ts deleted file mode 100644 index 7120ad6e..00000000 --- a/src/templates/assets/javascripts/integrations/search/worker/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./message" diff --git a/src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc b/src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc deleted file mode 100644 index 3df9d551..00000000 --- a/src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "no-console": "off", - "@typescript-eslint/no-misused-promises": "off" - } -} diff --git a/src/templates/assets/javascripts/integrations/search/worker/main/index.ts b/src/templates/assets/javascripts/integrations/search/worker/main/index.ts deleted file mode 100644 index 2df38080..00000000 --- a/src/templates/assets/javascripts/integrations/search/worker/main/index.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 RTICULAR 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 lunr from "lunr" - -import { getElement } from "~/browser/element/_" -import "~/polyfills" - -import { Search } from "../../_" -import { SearchConfig } from "../../config" -import { - SearchMessage, - SearchMessageType -} from "../message" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Add support for `iframe-worker` shim - * - * While `importScripts` is synchronous when executed inside of a web worker, - * it's not possible to provide a synchronous shim implementation. The cool - * thing is that awaiting a non-Promise will convert it into a Promise, so - * extending the type definition to return a `Promise` shouldn't break anything. - * - * @see https://bit.ly/2PjDnXi - GitHub comment - * - * @param urls - Scripts to load - * - * @returns Promise resolving with no result - */ -declare global { - function importScripts(...urls: string[]): Promise<void> | void -} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Search index - */ -let index: Search - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch (= import) multi-language support through `lunr-languages` - * - * This function automatically imports the stemmers necessary to process the - * languages which are defined as part of the search 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 - * searching for the first `script` element with a `src` attribute, which will - * contain the contents of this script. - * - * @param config - Search configuration - * - * @returns Promise resolving with no result - */ -async function setupSearchLanguages( - config: SearchConfig -): Promise<void> { - let base = "../lunr" - - /* Detect `iframe-worker` and fix base URL */ - if (typeof parent !== "undefined" && "IFrameWorker" in parent) { - const worker = getElement<HTMLScriptElement>("script[src]")! - const [path] = worker.src.split("/worker") - - /* Prefix base with path */ - base = base.replace("..", path) - } - - /* Add scripts for languages */ - const scripts = [] - for (const lang of config.lang) { - switch (lang) { - - /* Add segmenter for Japanese */ - case "ja": - scripts.push(`${base}/tinyseg.js`) - break - - /* Add segmenter for Hindi and Thai */ - case "hi": - case "th": - scripts.push(`${base}/wordcut.js`) - break - } - - /* Add language support */ - if (lang !== "en") - scripts.push(`${base}/min/lunr.${lang}.min.js`) - } - - /* Add multi-language support */ - if (config.lang.length > 1) - scripts.push(`${base}/min/lunr.multi.min.js`) - - /* Load scripts synchronously */ - if (scripts.length) - await importScripts( - `${base}/min/lunr.stemmer.support.min.js`, - ...scripts - ) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Message handler - * - * @param message - Source message - * - * @returns Target message - */ -export async function handler( - message: SearchMessage -): Promise<SearchMessage> { - switch (message.type) { - - /* Search setup message */ - case SearchMessageType.SETUP: - await setupSearchLanguages(message.data.config) - index = new Search(message.data) - return { - type: SearchMessageType.READY - } - - /* Search query message */ - case SearchMessageType.QUERY: - const query = message.data - try { - return { - type: SearchMessageType.RESULT, - data: index.search(query) - } - - /* Return empty result in case of error */ - } catch (err) { - console.warn(`Invalid query: ${query} – see https://bit.ly/2s3ChXG`) - console.warn(err) - return { - type: SearchMessageType.RESULT, - data: { items: [] } - } - } - - /* All other messages */ - default: - throw new TypeError("Invalid message type") - } -} - -/* ---------------------------------------------------------------------------- - * Worker - * ------------------------------------------------------------------------- */ - -/* Expose Lunr.js in global scope, or stemmers won't work */ -self.lunr = lunr - -/* Handle messages */ -addEventListener("message", async ev => { - postMessage(await handler(ev.data)) -}) diff --git a/src/templates/assets/javascripts/integrations/search/worker/message/index.ts b/src/templates/assets/javascripts/integrations/search/worker/message/index.ts deleted file mode 100644 index 54d5001e..00000000 --- a/src/templates/assets/javascripts/integrations/search/worker/message/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 RTICULAR 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 { SearchResult } from "../../_" -import { SearchIndex } from "../../config" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search message type - */ -export const enum SearchMessageType { - SETUP, /* Search index setup */ - READY, /* Search index ready */ - QUERY, /* Search query */ - RESULT /* Search results */ -} - -/* ------------------------------------------------------------------------- */ - -/** - * Message containing the data necessary to setup the search index - */ -export interface SearchSetupMessage { - type: SearchMessageType.SETUP /* Message type */ - data: SearchIndex /* Message data */ -} - -/** - * Message indicating the search index is ready - */ -export interface SearchReadyMessage { - type: SearchMessageType.READY /* Message type */ -} - -/** - * Message containing a search query - */ -export interface SearchQueryMessage { - type: SearchMessageType.QUERY /* Message type */ - data: string /* Message data */ -} - -/** - * Message containing results for a search query - */ -export interface SearchResultMessage { - type: SearchMessageType.RESULT /* Message type */ - data: SearchResult /* Message data */ -} - -/* ------------------------------------------------------------------------- */ - -/** - * Message exchanged with the search worker - */ -export type SearchMessage = - | SearchSetupMessage - | SearchReadyMessage - | SearchQueryMessage - | SearchResultMessage - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Type guard for search ready messages - * - * @param message - Search worker message - * - * @returns Test result - */ -export function isSearchReadyMessage( - message: SearchMessage -): message is SearchReadyMessage { - return message.type === SearchMessageType.READY -} - -/** - * Type guard for search result messages - * - * @param message - Search worker message - * - * @returns Test result - */ -export function isSearchResultMessage( - message: SearchMessage -): message is SearchResultMessage { - return message.type === SearchMessageType.RESULT -} diff --git a/src/templates/assets/javascripts/integrations/sitemap/index.ts b/src/templates/assets/javascripts/integrations/sitemap/index.ts deleted file mode 100644 index 08695bad..00000000 --- a/src/templates/assets/javascripts/integrations/sitemap/index.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - EMPTY, - Observable, - catchError, - defaultIfEmpty, - map, - of, - tap -} from "rxjs" - -import { configuration } from "~/_" -import { getElements, requestXML } from "~/browser" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Sitemap, i.e. a list of URLs - */ -export type Sitemap = string[] - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Preprocess a list of URLs - * - * This function replaces the `site_url` in the sitemap with the actual base - * URL, to allow instant navigation to work in occasions like Netlify previews. - * - * @param urls - URLs - * - * @returns URL path parts - */ -function preprocess(urls: Sitemap): Sitemap { - if (urls.length < 2) - return [""] - - /* Take the first two URLs and remove everything after the last slash */ - const [root, next] = [...urls] - .sort((a, b) => a.length - b.length) - .map(url => url.replace(/[^/]+$/, "")) - - /* Compute common prefix */ - let index = 0 - if (root === next) - index = root.length - else - while (root.charCodeAt(index) === next.charCodeAt(index)) - index++ - - /* Remove common prefix and return in original order */ - return urls.map(url => url.replace(root.slice(0, index), "")) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch the sitemap for the given base URL - * - * @param base - Base URL - * - * @returns Sitemap observable - */ -export function fetchSitemap(base?: URL): Observable<Sitemap> { - const cached = __md_get<Sitemap>("__sitemap", sessionStorage, base) - if (cached) { - return of(cached) - } else { - const config = configuration() - return requestXML(new URL("sitemap.xml", base || config.base)) - .pipe( - map(sitemap => preprocess(getElements("loc", sitemap) - .map(node => node.textContent!) - )), - catchError(() => EMPTY), // @todo refactor instant loading - defaultIfEmpty([]), - tap(sitemap => __md_set("__sitemap", sitemap, sessionStorage, base)) - ) - } -} diff --git a/src/templates/assets/javascripts/integrations/version/.eslintrc b/src/templates/assets/javascripts/integrations/version/.eslintrc deleted file mode 100644 index 38a5714d..00000000 --- a/src/templates/assets/javascripts/integrations/version/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "no-null/no-null": "off" - } -} diff --git a/src/templates/assets/javascripts/integrations/version/index.ts b/src/templates/assets/javascripts/integrations/version/index.ts deleted file mode 100644 index 38d78f17..00000000 --- a/src/templates/assets/javascripts/integrations/version/index.ts +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { - EMPTY, - Subject, - catchError, - combineLatest, - filter, - fromEvent, - map, - of, - switchMap, - withLatestFrom -} from "rxjs" - -import { configuration } from "~/_" -import { - getElement, - getLocation, - requestJSON, - setLocation -} from "~/browser" -import { getComponentElements } from "~/components" -import { - Version, - renderVersionSelector -} from "~/templates" - -import { fetchSitemap } from "../sitemap" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Setup options - */ -interface SetupOptions { - document$: Subject<Document> /* Document subject */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Set up version selector - * - * @param options - Options - */ -export function setupVersionSelector( - { document$ }: SetupOptions -): void { - const config = configuration() - const versions$ = requestJSON<Version[]>( - new URL("../versions.json", config.base) - ) - .pipe( - catchError(() => EMPTY) // @todo refactor instant loading - ) - - /* Determine current version */ - const current$ = versions$ - .pipe( - map(versions => { - const [, current] = config.base.match(/([^/]+)\/?$/)! - return versions.find(({ version, aliases }) => ( - version === current || aliases.includes(current) - )) || versions[0] - }) - ) - - /* Intercept inter-version navigation */ - versions$ - .pipe( - map(versions => new Map(versions.map(version => [ - `${new URL(`../${version.version}/`, config.base)}`, - version - ]))), - switchMap(urls => fromEvent<MouseEvent>(document.body, "click") - .pipe( - filter(ev => !ev.metaKey && !ev.ctrlKey), - withLatestFrom(current$), - switchMap(([ev, current]) => { - if (ev.target instanceof Element) { - const el = ev.target.closest("a") - if (el && !el.target && urls.has(el.href)) { - const url = el.href - // This is a temporary hack to detect if a version inside the - // version selector or on another part of the site was clicked. - // If we're inside the version selector, we definitely want to - // find the same page, as we might have different deployments - // due to aliases. However, if we're outside the version - // selector, we must abort here, because we might otherwise - // interfere with instant navigation. We need to refactor this - // at some point together with instant navigation. - // - // See https://github.com/squidfunk/mkdocs-material/issues/4012 - if (!ev.target.closest(".md-version")) { - const version = urls.get(url)! - if (version === current) - return EMPTY - } - ev.preventDefault() - return of(url) - } - } - return EMPTY - }), - switchMap(url => { - const { version } = urls.get(url)! - return fetchSitemap(new URL(url)) - .pipe( - map(sitemap => { - const location = getLocation() - const path = location.href.replace(config.base, "") - return sitemap.includes(path.split("#")[0]) - ? new URL(`../${version}/${path}`, config.base) - : new URL(url) - }) - ) - }) - ) - ) - ) - .subscribe(url => setLocation(url, true)) - - /* Render version selector and warning */ - combineLatest([versions$, current$]) - .subscribe(([versions, current]) => { - const topic = getElement(".md-header__topic") - topic.appendChild(renderVersionSelector(versions, current)) - }) - - /* Integrate outdated version banner with instant navigation */ - document$.pipe(switchMap(() => current$)) - .subscribe(current => { - - /* Check if version state was already determined */ - let outdated = __md_get("__outdated", sessionStorage) - if (outdated === null) { - outdated = true - - /* Obtain and normalize default versions */ - let ignored = config.version?.default || "latest" - if (!Array.isArray(ignored)) - ignored = [ignored] - - /* Check if version is considered a default */ - main: for (const ignore of ignored) - for (const alias of current.aliases) - if (new RegExp(ignore, "i").test(alias)) { - outdated = false - break main - } - - /* Persist version state in session storage */ - __md_set("__outdated", outdated, sessionStorage) - } - - /* Unhide outdated version banner */ - if (outdated) - for (const warning of getComponentElements("outdated")) - warning.hidden = false - }) -} diff --git a/src/templates/assets/javascripts/patches/indeterminate/index.ts b/src/templates/assets/javascripts/patches/indeterminate/index.ts deleted file mode 100644 index 9b7b0d5a..00000000 --- a/src/templates/assets/javascripts/patches/indeterminate/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - fromEvent, - map, - mergeMap, - switchMap, - takeWhile, - tap, - withLatestFrom -} from "rxjs" - -import { getElements } from "~/browser" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Patch options - */ -interface PatchOptions { - document$: Observable<Document> /* Document observable */ - tablet$: Observable<boolean> /* Media tablet observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Patch indeterminate checkboxes - * - * This function replaces the indeterminate "pseudo state" with the actual - * indeterminate state, which is used to keep navigation always expanded. - * - * @param options - Options - */ -export function patchIndeterminate( - { document$, tablet$ }: PatchOptions -): void { - document$ - .pipe( - switchMap(() => getElements<HTMLInputElement>( - ".md-toggle--indeterminate" - )), - tap(el => { - el.indeterminate = true - el.checked = false - }), - mergeMap(el => fromEvent(el, "change") - .pipe( - takeWhile(() => el.classList.contains("md-toggle--indeterminate")), - map(() => el) - ) - ), - withLatestFrom(tablet$) - ) - .subscribe(([el, tablet]) => { - el.classList.remove("md-toggle--indeterminate") - if (tablet) - el.checked = false - }) -} diff --git a/src/templates/assets/javascripts/patches/index.ts b/src/templates/assets/javascripts/patches/index.ts deleted file mode 100644 index b6e65fc0..00000000 --- a/src/templates/assets/javascripts/patches/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./indeterminate" -export * from "./scrollfix" -export * from "./scrolllock" diff --git a/src/templates/assets/javascripts/patches/scrollfix/index.ts b/src/templates/assets/javascripts/patches/scrollfix/index.ts deleted file mode 100644 index 607c46a0..00000000 --- a/src/templates/assets/javascripts/patches/scrollfix/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - filter, - fromEvent, - map, - mergeMap, - switchMap, - tap -} from "rxjs" - -import { getElements } from "~/browser" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Patch options - */ -interface PatchOptions { - document$: Observable<Document> /* Document observable */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Check whether the given device is an Apple device - * - * @returns Test result - */ -function isAppleDevice(): boolean { - return /(iPad|iPhone|iPod)/.test(navigator.userAgent) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Patch all elements with `data-md-scrollfix` attributes - * - * This is a year-old patch which ensures that overflow scrolling works at the - * top and bottom of containers on iOS by ensuring a `1px` scroll offset upon - * the start of a touch event. - * - * @see https://bit.ly/2SCtAOO - Original source - * - * @param options - Options - */ -export function patchScrollfix( - { document$ }: PatchOptions -): void { - document$ - .pipe( - switchMap(() => getElements("[data-md-scrollfix]")), - tap(el => el.removeAttribute("data-md-scrollfix")), - filter(isAppleDevice), - mergeMap(el => fromEvent(el, "touchstart") - .pipe( - map(() => el) - ) - ) - ) - .subscribe(el => { - const top = el.scrollTop - - /* We're at the top of the container */ - if (top === 0) { - el.scrollTop = 1 - - /* We're at the bottom of the container */ - } else if (top + el.offsetHeight === el.scrollHeight) { - el.scrollTop = top - 1 - } - }) -} diff --git a/src/templates/assets/javascripts/patches/scrolllock/index.ts b/src/templates/assets/javascripts/patches/scrolllock/index.ts deleted file mode 100644 index 4ec3e103..00000000 --- a/src/templates/assets/javascripts/patches/scrolllock/index.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2016-2023 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, - combineLatest, - delay, - map, - of, - switchMap, - withLatestFrom -} from "rxjs" - -import { - Viewport, - watchToggle -} from "~/browser" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Patch options - */ -interface PatchOptions { - viewport$: Observable<Viewport> /* Viewport observable */ - tablet$: Observable<boolean> /* Media tablet observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Patch the document body to lock when search is open - * - * For mobile and tablet viewports, the search is rendered full screen, which - * leads to scroll leaking when at the top or bottom of the search result. This - * function locks the body when the search is in full screen mode, and restores - * the scroll position when leaving. - * - * @param options - Options - */ -export function patchScrolllock( - { viewport$, tablet$ }: PatchOptions -): void { - combineLatest([watchToggle("search"), tablet$]) - .pipe( - map(([active, tablet]) => active && !tablet), - switchMap(active => of(active) - .pipe( - delay(active ? 400 : 100) - ) - ), - withLatestFrom(viewport$) - ) - .subscribe(([active, { offset: { y }}]) => { - if (active) { - document.body.setAttribute("data-md-scrolllock", "") - document.body.style.top = `-${y}px` - } else { - const value = -1 * parseInt(document.body.style.top, 10) - document.body.removeAttribute("data-md-scrolllock") - document.body.style.top = "" - if (value) - window.scrollTo(0, value) - } - }) -} diff --git a/src/templates/assets/javascripts/polyfills/index.ts b/src/templates/assets/javascripts/polyfills/index.ts deleted file mode 100644 index 2aec8290..00000000 --- a/src/templates/assets/javascripts/polyfills/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2016-2023 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. - */ - -/* ---------------------------------------------------------------------------- - * Polyfills - * ------------------------------------------------------------------------- */ - -/* Polyfill `Object.entries` */ -if (!Object.entries) - Object.entries = function (obj: object) { - const data: [string, string][] = [] - for (const key of Object.keys(obj)) - // @ts-expect-error - ignore property access warning - data.push([key, obj[key]]) - - /* Return entries */ - return data - } - -/* Polyfill `Object.values` */ -if (!Object.values) - Object.values = function (obj: object) { - const data: string[] = [] - for (const key of Object.keys(obj)) - // @ts-expect-error - ignore property access warning - data.push(obj[key]) - - /* Return values */ - return data - } - -/* ------------------------------------------------------------------------- */ - -/* Polyfills for `Element` */ -if (typeof Element !== "undefined") { - - /* Polyfill `Element.scrollTo` */ - if (!Element.prototype.scrollTo) - Element.prototype.scrollTo = function ( - x?: ScrollToOptions | number, y?: number - ): void { - if (typeof x === "object") { - this.scrollLeft = x.left! - this.scrollTop = x.top! - } else { - this.scrollLeft = x! - this.scrollTop = y! - } - } - - /* Polyfill `Element.replaceWith` */ - if (!Element.prototype.replaceWith) - Element.prototype.replaceWith = function ( - ...nodes: Array<string | Node> - ): void { - const parent = this.parentNode - if (parent) { - if (nodes.length === 0) - parent.removeChild(this) - - /* Replace children and create text nodes */ - for (let i = nodes.length - 1; i >= 0; i--) { - let node = nodes[i] - if (typeof node === "string") - node = document.createTextNode(node) - else if (node.parentNode) - node.parentNode.removeChild(node) - - /* Replace child or insert before previous sibling */ - if (!i) - parent.replaceChild(node, this) - else - parent.insertBefore(this.previousSibling!, node) - } - } - } -} diff --git a/src/templates/assets/javascripts/templates/annotation/index.tsx b/src/templates/assets/javascripts/templates/annotation/index.tsx deleted file mode 100644 index 9b8f85f5..00000000 --- a/src/templates/assets/javascripts/templates/annotation/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { h } from "~/utilities" - -import { renderTooltip } from "../tooltip" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Render an annotation - * - * @param id - Annotation identifier - * @param prefix - Tooltip identifier prefix - * - * @returns Element - */ -export function renderAnnotation( - id: string | number, prefix?: string -): HTMLElement { - prefix = prefix ? `${prefix}_annotation_${id}` : undefined - - /* Render tooltip with anchor, if given */ - if (prefix) { - const anchor = prefix ? `#${prefix}` : undefined - return ( - <aside class="md-annotation" tabIndex={0}> - {renderTooltip(prefix)} - <a href={anchor} class="md-annotation__index" tabIndex={-1}> - <span data-md-annotation-id={id}></span> - </a> - </aside> - ) - } else { - return ( - <aside class="md-annotation" tabIndex={0}> - {renderTooltip(prefix)} - <span class="md-annotation__index" tabIndex={-1}> - <span data-md-annotation-id={id}></span> - </span> - </aside> - ) - } -} diff --git a/src/templates/assets/javascripts/templates/clipboard/index.tsx b/src/templates/assets/javascripts/templates/clipboard/index.tsx deleted file mode 100644 index 95dbf12a..00000000 --- a/src/templates/assets/javascripts/templates/clipboard/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { translation } from "~/_" -import { h } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Render a 'copy-to-clipboard' button - * - * @param id - Unique identifier - * - * @returns Element - */ -export function renderClipboardButton(id: string): HTMLElement { - return ( - <button - class="md-clipboard md-icon" - title={translation("clipboard.copy")} - data-clipboard-target={`#${id} > code`} - ></button> - ) -} diff --git a/src/templates/assets/javascripts/templates/index.ts b/src/templates/assets/javascripts/templates/index.ts deleted file mode 100644 index b50b93b8..00000000 --- a/src/templates/assets/javascripts/templates/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./annotation" -export * from "./clipboard" -export * from "./search" -export * from "./source" -export * from "./tabbed" -export * from "./table" -export * from "./version" diff --git a/src/templates/assets/javascripts/templates/search/index.tsx b/src/templates/assets/javascripts/templates/search/index.tsx deleted file mode 100644 index 350c0505..00000000 --- a/src/templates/assets/javascripts/templates/search/index.tsx +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { ComponentChild } from "preact" - -import { configuration, feature, translation } from "~/_" -import { SearchItem } from "~/integrations/search" -import { h } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Render flag - */ -const enum Flag { - TEASER = 1, /* Render teaser */ - PARENT = 2 /* Render as parent */ -} - -/* ---------------------------------------------------------------------------- - * Helper function - * ------------------------------------------------------------------------- */ - -/** - * Render a search document - * - * @param document - Search document - * @param flag - Render flags - * - * @returns Element - */ -function renderSearchDocument( - document: SearchItem, flag: Flag -): HTMLElement { - const parent = flag & Flag.PARENT - const teaser = flag & Flag.TEASER - - /* Render missing query terms */ - const missing = Object.keys(document.terms) - .filter(key => !document.terms[key]) - .reduce<ComponentChild[]>((list, key) => [ - ...list, <del>{key}</del>, " " - ], []) - .slice(0, -1) - - /* Assemble query string for highlighting */ - const config = configuration() - const url = new URL(document.location, config.base) - if (feature("search.highlight")) - url.searchParams.set("h", Object.entries(document.terms) - .filter(([, match]) => match) - .reduce((highlight, [value]) => `${highlight} ${value}`.trim(), "") - ) - - /* Render article or section, depending on flags */ - const { tags } = configuration() - return ( - <a href={`${url}`} class="md-search-result__link" tabIndex={-1}> - <article - class="md-search-result__article md-typeset" - data-md-score={document.score.toFixed(2)} - > - {parent > 0 && <div class="md-search-result__icon md-icon"></div>} - {parent > 0 && <h1>{document.title}</h1>} - {parent <= 0 && <h2>{document.title}</h2>} - {teaser > 0 && document.text.length > 0 && - document.text - } - {document.tags && document.tags.map(tag => { - const type = tags - ? tag in tags - ? `md-tag-icon md-tag--${tags[tag]}` - : "md-tag-icon" - : "" - return ( - <span class={`md-tag ${type}`}>{tag}</span> - ) - })} - {teaser > 0 && missing.length > 0 && - <p class="md-search-result__terms"> - {translation("search.result.term.missing")}: {...missing} - </p> - } - </article> - </a> - ) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Render a search result - * - * @param result - Search result - * - * @returns Element - */ -export function renderSearchResultItem( - result: SearchItem[] -): HTMLElement { - const threshold = result[0].score - const docs = [...result] - - const config = configuration() - - /* Find and extract parent article */ - const parent = docs.findIndex(doc => { - const l = `${new URL(doc.location, config.base)}` // @todo hacky - return !l.includes("#") - }) - const [article] = docs.splice(parent, 1) - - /* Determine last index above threshold */ - let index = docs.findIndex(doc => doc.score < threshold) - if (index === -1) - index = docs.length - - /* Partition sections */ - const best = docs.slice(0, index) - const more = docs.slice(index) - - /* Render children */ - const children = [ - renderSearchDocument(article, Flag.PARENT | +(!parent && index === 0)), - ...best.map(section => renderSearchDocument(section, Flag.TEASER)), - ...more.length ? [ - <details class="md-search-result__more"> - <summary tabIndex={-1}> - <div> - {more.length > 0 && more.length === 1 - ? translation("search.result.more.one") - : translation("search.result.more.other", more.length) - } - </div> - </summary> - {...more.map(section => renderSearchDocument(section, Flag.TEASER))} - </details> - ] : [] - ] - - /* Render search result */ - return ( - <li class="md-search-result__item"> - {children} - </li> - ) -} diff --git a/src/templates/assets/javascripts/templates/source/index.tsx b/src/templates/assets/javascripts/templates/source/index.tsx deleted file mode 100644 index b59a8f67..00000000 --- a/src/templates/assets/javascripts/templates/source/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { SourceFacts } from "~/components" -import { h, round } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Render repository facts - * - * @param facts - Repository facts - * - * @returns Element - */ -export function renderSourceFacts(facts: SourceFacts): HTMLElement { - return ( - <ul class="md-source__facts"> - {Object.entries(facts).map(([key, value]) => ( - <li class={`md-source__fact md-source__fact--${key}`}> - {typeof value === "number" ? round(value) : value} - </li> - ))} - </ul> - ) -} diff --git a/src/templates/assets/javascripts/templates/tabbed/index.tsx b/src/templates/assets/javascripts/templates/tabbed/index.tsx deleted file mode 100644 index b283ac66..00000000 --- a/src/templates/assets/javascripts/templates/tabbed/index.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { h } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Tabbed control type - */ -type TabbedControlType = - | "prev" - | "next" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Render control for content tabs - * - * @param type - Control type - * - * @returns Element - */ -export function renderTabbedControl( - type: TabbedControlType -): HTMLElement { - const classes = `tabbed-control tabbed-control--${type}` - return ( - <div class={classes} hidden> - <button class="tabbed-button" tabIndex={-1} aria-hidden="true"></button> - </div> - ) -} diff --git a/src/templates/assets/javascripts/templates/table/index.tsx b/src/templates/assets/javascripts/templates/table/index.tsx deleted file mode 100644 index 1fcba152..00000000 --- a/src/templates/assets/javascripts/templates/table/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { h } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Render a table inside a wrapper to improve scrolling on mobile - * - * @param table - Table element - * - * @returns Element - */ -export function renderTable(table: HTMLElement): HTMLElement { - return ( - <div class="md-typeset__scrollwrap"> - <div class="md-typeset__table"> - {table} - </div> - </div> - ) -} diff --git a/src/templates/assets/javascripts/templates/tooltip/index.tsx b/src/templates/assets/javascripts/templates/tooltip/index.tsx deleted file mode 100644 index ec583490..00000000 --- a/src/templates/assets/javascripts/templates/tooltip/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { h } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Render a tooltip - * - * @param id - Tooltip identifier - * - * @returns Element - */ -export function renderTooltip(id?: string): HTMLElement { - return ( - <div class="md-tooltip" id={id}> - <div class="md-tooltip__inner md-typeset"></div> - </div> - ) -} diff --git a/src/templates/assets/javascripts/templates/version/index.tsx b/src/templates/assets/javascripts/templates/version/index.tsx deleted file mode 100644 index 4aff7aa7..00000000 --- a/src/templates/assets/javascripts/templates/version/index.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { configuration, translation } from "~/_" -import { h } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Version - */ -export interface Version { - version: string /* Version identifier */ - title: string /* Version title */ - aliases: string[] /* Version aliases */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Render a version - * - * @param version - Version - * - * @returns Element - */ -function renderVersion(version: Version): HTMLElement { - const config = configuration() - - /* Ensure trailing slash - see https://bit.ly/3rL5u3f */ - const url = new URL(`../${version.version}/`, config.base) - return ( - <li class="md-version__item"> - <a href={`${url}`} class="md-version__link"> - {version.title} - </a> - </li> - ) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Render a version selector - * - * @param versions - Versions - * @param active - Active version - * - * @returns Element - */ -export function renderVersionSelector( - versions: Version[], active: Version -): HTMLElement { - return ( - <div class="md-version"> - <button - class="md-version__current" - aria-label={translation("select.version")} - > - {active.title} - </button> - <ul class="md-version__list"> - {versions.map(renderVersion)} - </ul> - </div> - ) -} diff --git a/src/templates/assets/javascripts/utilities/h/.eslintrc b/src/templates/assets/javascripts/utilities/h/.eslintrc deleted file mode 100644 index d79b45b0..00000000 --- a/src/templates/assets/javascripts/utilities/h/.eslintrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "rules": { - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-namespace": "off", - "jsdoc/require-jsdoc": "off" - } -} diff --git a/src/templates/assets/javascripts/utilities/h/index.ts b/src/templates/assets/javascripts/utilities/h/index.ts deleted file mode 100644 index 08d809f1..00000000 --- a/src/templates/assets/javascripts/utilities/h/index.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 { JSX as JSXInternal } from "preact" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * HTML attributes - */ -type Attributes = - & JSXInternal.HTMLAttributes - & JSXInternal.SVGAttributes - & Record<string, any> - -/** - * Child element - */ -type Child = - | ChildNode - | HTMLElement - | Text - | string - | number - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Append a child node to an element - * - * @param el - Element - * @param child - Child node(s) - */ -function appendChild(el: HTMLElement, child: Child | Child[]): void { - - /* Handle primitive types (including raw HTML) */ - if (typeof child === "string" || typeof child === "number") { - el.innerHTML += child.toString() - - /* Handle nodes */ - } else if (child instanceof Node) { - el.appendChild(child) - - /* Handle nested children */ - } else if (Array.isArray(child)) { - for (const node of child) - appendChild(el, node) - } -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * JSX factory - * - * @template T - Element type - * - * @param tag - HTML tag - * @param attributes - HTML attributes - * @param children - Child elements - * - * @returns Element - */ -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 */ - if (attributes) - for (const attr of Object.keys(attributes)) { - if (typeof attributes[attr] === "undefined") - continue - - /* Set default attribute or boolean */ - if (typeof attributes[attr] !== "boolean") - el.setAttribute(attr, attributes[attr]) - else - el.setAttribute(attr, "") - } - - /* Append child nodes */ - for (const child of children) - appendChild(el, child) - - /* Return element */ - return el as T -} - -/* ---------------------------------------------------------------------------- - * Namespace - * ------------------------------------------------------------------------- */ - -export declare namespace h { - namespace JSX { - type Element = HTMLElement - type IntrinsicElements = JSXInternal.IntrinsicElements - } -} diff --git a/src/templates/assets/javascripts/utilities/index.ts b/src/templates/assets/javascripts/utilities/index.ts deleted file mode 100644 index 42886e0b..00000000 --- a/src/templates/assets/javascripts/utilities/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./h" -export * from "./round" diff --git a/src/templates/assets/javascripts/utilities/round/index.ts b/src/templates/assets/javascripts/utilities/round/index.ts deleted file mode 100644 index 3e6bf91a..00000000 --- a/src/templates/assets/javascripts/utilities/round/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2016-2023 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. - */ - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Round a number for display with repository facts - * - * This is a reverse-engineered version of GitHub's weird rounding algorithm - * for stars, forks and all other numbers. While all numbers below `1,000` are - * returned as-is, bigger numbers are converted to fixed numbers: - * - * - `1,049` => `1k` - * - `1,050` => `1.1k` - * - `1,949` => `1.9k` - * - `1,950` => `2k` - * - * @param value - Original value - * - * @returns Rounded value - */ -export function round(value: number): string { - if (value > 999) { - const digits = +((value - 950) % 1000 > 99) - return `${((value + 0.000001) / 1000).toFixed(digits)}k` - } else { - return value.toString() - } -} diff --git a/src/templates/assets/javascripts/workers/search.ts b/src/templates/assets/javascripts/workers/search.ts deleted file mode 100644 index e995b1ff..00000000 --- a/src/templates/assets/javascripts/workers/search.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2016-2023 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 RTICULAR 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 "~/integrations/search/worker/main" |
