diff options
Diffstat (limited to 'src/templates')
280 files changed, 28097 insertions, 0 deletions
diff --git a/src/templates/.icons/logo.afdesign b/src/templates/.icons/logo.afdesign Binary files differnew file mode 100644 index 00000000..07f57d0a --- /dev/null +++ b/src/templates/.icons/logo.afdesign diff --git a/src/templates/.icons/logo.svg b/src/templates/.icons/logo.svg new file mode 100644 index 00000000..763eb2c2 --- /dev/null +++ b/src/templates/.icons/logo.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 89 89"> + <path d="M3.136,17.387l0,42.932l42.932,21.467l-42.932,-64.399Z" /> + <path d="M21.91,8l42.933,64.398l-18.775,9.388l-42.932,-64.399l18.774,-9.387Z" style="fill-opacity: 0.5" /> + <path d="M67.535,17.387l-27.262,18.156l21.878,32.818l5.384,2.691l0,-53.665Z" /> + <path d="M67.535,17.387l0,53.666l18.774,-9.388l0,-53.665l-18.774,9.387Z" style="fill-opacity: 0.25" /> +</svg> diff --git a/src/templates/404.html b/src/templates/404.html new file mode 100644 index 00000000..e87e7783 --- /dev/null +++ b/src/templates/404.html @@ -0,0 +1,28 @@ +<!-- + 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. +--> + +{% extends "main.html" %} + +<!-- Content --> +{% block content %} + <h1>404 - Not found</h1> +{% endblock %} diff --git a/src/templates/__init__.py b/src/templates/__init__.py new file mode 100644 index 00000000..d1899378 --- /dev/null +++ b/src/templates/__init__.py @@ -0,0 +1,19 @@ +# 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. diff --git a/src/templates/assets/images/favicon.png b/src/templates/assets/images/favicon.png Binary files differnew file mode 100644 index 00000000..1cf13b9f --- /dev/null +++ b/src/templates/assets/images/favicon.png diff --git a/src/templates/assets/javascripts/_/index.ts b/src/templates/assets/javascripts/_/index.ts new file mode 100644 index 00000000..be0f4a42 --- /dev/null +++ b/src/templates/assets/javascripts/_/index.ts @@ -0,0 +1,148 @@ +/* + * 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 new file mode 100644 index 00000000..354c9b5c --- /dev/null +++ b/src/templates/assets/javascripts/browser/document/index.ts @@ -0,0 +1,48 @@ +/* + * 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 new file mode 100644 index 00000000..16973760 --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/_/.eslintrc @@ -0,0 +1,6 @@ +{ + "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 new file mode 100644 index 00000000..b7beb462 --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/_/index.ts @@ -0,0 +1,120 @@ +/* + * 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 new file mode 100644 index 00000000..f31fe276 --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/focus/index.ts @@ -0,0 +1,81 @@ +/* + * 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 new file mode 100644 index 00000000..50ce84b2 --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/index.ts @@ -0,0 +1,27 @@ +/* + * 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 new file mode 100644 index 00000000..6dd229d5 --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/offset/_/index.ts @@ -0,0 +1,86 @@ +/* + * 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 new file mode 100644 index 00000000..557301a6 --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/offset/content/index.ts @@ -0,0 +1,76 @@ +/* + * 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 new file mode 100644 index 00000000..602ff2cf --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/offset/index.ts @@ -0,0 +1,24 @@ +/* + * 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 new file mode 100644 index 00000000..35a5e68b --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/size/_/index.ts @@ -0,0 +1,151 @@ +/* + * 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 new file mode 100644 index 00000000..5ed388cf --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/size/content/index.ts @@ -0,0 +1,67 @@ +/* + * 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 new file mode 100644 index 00000000..602ff2cf --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/size/index.ts @@ -0,0 +1,24 @@ +/* + * 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 new file mode 100644 index 00000000..1ffe0b8d --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/visibility/index.ts @@ -0,0 +1,131 @@ +/* + * 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 new file mode 100644 index 00000000..f1ee2bae --- /dev/null +++ b/src/templates/assets/javascripts/browser/index.ts @@ -0,0 +1,32 @@ +/* + * 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 new file mode 100644 index 00000000..783f2cda --- /dev/null +++ b/src/templates/assets/javascripts/browser/keyboard/index.ts @@ -0,0 +1,148 @@ +/* + * 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 new file mode 100644 index 00000000..2672fa74 --- /dev/null +++ b/src/templates/assets/javascripts/browser/location/_/index.ts @@ -0,0 +1,85 @@ +/* + * 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 new file mode 100644 index 00000000..5d3a134a --- /dev/null +++ b/src/templates/assets/javascripts/browser/location/hash/index.ts @@ -0,0 +1,104 @@ +/* + * 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 new file mode 100644 index 00000000..d77a5444 --- /dev/null +++ b/src/templates/assets/javascripts/browser/location/index.ts @@ -0,0 +1,24 @@ +/* + * 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 new file mode 100644 index 00000000..dd7400d4 --- /dev/null +++ b/src/templates/assets/javascripts/browser/media/index.ts @@ -0,0 +1,95 @@ +/* + * 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 new file mode 100644 index 00000000..74a56a64 --- /dev/null +++ b/src/templates/assets/javascripts/browser/request/index.ts @@ -0,0 +1,141 @@ +/* + * 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 new file mode 100644 index 00000000..ef5c89e6 --- /dev/null +++ b/src/templates/assets/javascripts/browser/script/index.ts @@ -0,0 +1,70 @@ +/* + * 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 new file mode 100644 index 00000000..0be4b29d --- /dev/null +++ b/src/templates/assets/javascripts/browser/toggle/index.ts @@ -0,0 +1,102 @@ +/* + * 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 new file mode 100644 index 00000000..09c45f32 --- /dev/null +++ b/src/templates/assets/javascripts/browser/viewport/_/index.ts @@ -0,0 +1,69 @@ +/* + * 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 new file mode 100644 index 00000000..8769cf3b --- /dev/null +++ b/src/templates/assets/javascripts/browser/viewport/at/index.ts @@ -0,0 +1,84 @@ +/* + * 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 new file mode 100644 index 00000000..b3d135e9 --- /dev/null +++ b/src/templates/assets/javascripts/browser/viewport/index.ts @@ -0,0 +1,26 @@ +/* + * 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 new file mode 100644 index 00000000..63d37dd2 --- /dev/null +++ b/src/templates/assets/javascripts/browser/viewport/offset/index.ts @@ -0,0 +1,78 @@ +/* + * 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 new file mode 100644 index 00000000..06694888 --- /dev/null +++ b/src/templates/assets/javascripts/browser/viewport/size/index.ts @@ -0,0 +1,71 @@ +/* + * 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 new file mode 100644 index 00000000..12e4e63b --- /dev/null +++ b/src/templates/assets/javascripts/browser/worker/index.ts @@ -0,0 +1,112 @@ +/* + * 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 new file mode 100644 index 00000000..141789c9 --- /dev/null +++ b/src/templates/assets/javascripts/bundle.ts @@ -0,0 +1,316 @@ +/* + * 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 new file mode 100644 index 00000000..61c471d9 --- /dev/null +++ b/src/templates/assets/javascripts/components/_/index.ts @@ -0,0 +1,138 @@ +/* + * 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 new file mode 100644 index 00000000..dd04b4ff --- /dev/null +++ b/src/templates/assets/javascripts/components/announce/index.ts @@ -0,0 +1,110 @@ +/* + * 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 new file mode 100644 index 00000000..bc99db58 --- /dev/null +++ b/src/templates/assets/javascripts/components/consent/index.ts @@ -0,0 +1,116 @@ +/* + * 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 new file mode 100644 index 00000000..899a695c --- /dev/null +++ b/src/templates/assets/javascripts/components/content/_/index.ts @@ -0,0 +1,125 @@ +/* + * 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 new file mode 100644 index 00000000..c5138fa4 --- /dev/null +++ b/src/templates/assets/javascripts/components/content/annotation/_/index.ts @@ -0,0 +1,272 @@ +/* + * 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 new file mode 100644 index 00000000..c73b01fa --- /dev/null +++ b/src/templates/assets/javascripts/components/content/annotation/block/index.ts @@ -0,0 +1,88 @@ +/* + * 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 new file mode 100644 index 00000000..c593b723 --- /dev/null +++ b/src/templates/assets/javascripts/components/content/annotation/index.ts @@ -0,0 +1,25 @@ +/* + * 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 new file mode 100644 index 00000000..725dd583 --- /dev/null +++ b/src/templates/assets/javascripts/components/content/annotation/list/index.ts @@ -0,0 +1,209 @@ +/* + * 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 new file mode 100644 index 00000000..ccc09339 --- /dev/null +++ b/src/templates/assets/javascripts/components/content/code/_/index.ts @@ -0,0 +1,238 @@ +/* + * 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 new file mode 100644 index 00000000..3f86e2b4 --- /dev/null +++ b/src/templates/assets/javascripts/components/content/code/index.ts @@ -0,0 +1,23 @@ +/* + * 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 new file mode 100644 index 00000000..17bfae45 --- /dev/null +++ b/src/templates/assets/javascripts/components/content/details/index.ts @@ -0,0 +1,138 @@ +/* + * 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 new file mode 100644 index 00000000..a29d8b41 --- /dev/null +++ b/src/templates/assets/javascripts/components/content/index.ts @@ -0,0 +1,28 @@ +/* + * 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 new file mode 100644 index 00000000..3092b8ec --- /dev/null +++ b/src/templates/assets/javascripts/components/content/mermaid/index.css @@ -0,0 +1,430 @@ +/* + * 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 new file mode 100644 index 00000000..3f6480fd --- /dev/null +++ b/src/templates/assets/javascripts/components/content/mermaid/index.ts @@ -0,0 +1,133 @@ +/* + * 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 new file mode 100644 index 00000000..c318e7a6 --- /dev/null +++ b/src/templates/assets/javascripts/components/content/table/index.ts @@ -0,0 +1,70 @@ +/* + * 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 new file mode 100644 index 00000000..f57447e2 --- /dev/null +++ b/src/templates/assets/javascripts/components/content/tabs/index.ts @@ -0,0 +1,265 @@ +/* + * 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 new file mode 100644 index 00000000..6ff1bd44 --- /dev/null +++ b/src/templates/assets/javascripts/components/dialog/index.ts @@ -0,0 +1,128 @@ +/* + * 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 new file mode 100644 index 00000000..0f33eb48 --- /dev/null +++ b/src/templates/assets/javascripts/components/header/_/index.ts @@ -0,0 +1,200 @@ +/* + * 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 new file mode 100644 index 00000000..cf23ec1a --- /dev/null +++ b/src/templates/assets/javascripts/components/header/index.ts @@ -0,0 +1,24 @@ +/* + * 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 new file mode 100644 index 00000000..f3bc0d08 --- /dev/null +++ b/src/templates/assets/javascripts/components/header/title/index.ts @@ -0,0 +1,144 @@ +/* + * 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 new file mode 100644 index 00000000..3d4391d1 --- /dev/null +++ b/src/templates/assets/javascripts/components/index.ts @@ -0,0 +1,37 @@ +/* + * 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 new file mode 100644 index 00000000..2509f9b9 --- /dev/null +++ b/src/templates/assets/javascripts/components/main/index.ts @@ -0,0 +1,125 @@ +/* + * 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 new file mode 100644 index 00000000..cf578f60 --- /dev/null +++ b/src/templates/assets/javascripts/components/palette/index.ts @@ -0,0 +1,180 @@ +/* + * 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 new file mode 100644 index 00000000..30c722b8 --- /dev/null +++ b/src/templates/assets/javascripts/components/progress/index.ts @@ -0,0 +1,87 @@ +/* + * 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 new file mode 100644 index 00000000..aa963b47 --- /dev/null +++ b/src/templates/assets/javascripts/components/search/_/index.ts @@ -0,0 +1,239 @@ +/* + * 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 new file mode 100644 index 00000000..38a5714d --- /dev/null +++ b/src/templates/assets/javascripts/components/search/highlight/.eslintrc @@ -0,0 +1,5 @@ +{ + "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 new file mode 100644 index 00000000..bc3f94c9 --- /dev/null +++ b/src/templates/assets/javascripts/components/search/highlight/index.ts @@ -0,0 +1,115 @@ +/* + * 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 new file mode 100644 index 00000000..846d8685 --- /dev/null +++ b/src/templates/assets/javascripts/components/search/index.ts @@ -0,0 +1,28 @@ +/* + * 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 new file mode 100644 index 00000000..4ce21279 --- /dev/null +++ b/src/templates/assets/javascripts/components/search/query/index.ts @@ -0,0 +1,206 @@ +/* + * 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 new file mode 100644 index 00000000..c3c9ef20 --- /dev/null +++ b/src/templates/assets/javascripts/components/search/result/index.ts @@ -0,0 +1,197 @@ +/* + * 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 new file mode 100644 index 00000000..3db382c8 --- /dev/null +++ b/src/templates/assets/javascripts/components/search/share/index.ts @@ -0,0 +1,135 @@ +/* + * 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 new file mode 100644 index 00000000..e7881475 --- /dev/null +++ b/src/templates/assets/javascripts/components/search/suggest/index.ts @@ -0,0 +1,154 @@ +/* + * 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 new file mode 100644 index 00000000..82f3d03e --- /dev/null +++ b/src/templates/assets/javascripts/components/sidebar/index.ts @@ -0,0 +1,227 @@ +/* + * 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 new file mode 100644 index 00000000..5f6c4d11 --- /dev/null +++ b/src/templates/assets/javascripts/components/source/_/index.ts @@ -0,0 +1,142 @@ +/* + * 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 new file mode 100644 index 00000000..154f229f --- /dev/null +++ b/src/templates/assets/javascripts/components/source/facts/_/index.ts @@ -0,0 +1,88 @@ +/* + * 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 new file mode 100644 index 00000000..12cc55e0 --- /dev/null +++ b/src/templates/assets/javascripts/components/source/facts/github/index.ts @@ -0,0 +1,103 @@ +/* + * 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 new file mode 100644 index 00000000..d85d4afd --- /dev/null +++ b/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts @@ -0,0 +1,61 @@ +/* + * 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 new file mode 100644 index 00000000..f9bda64d --- /dev/null +++ b/src/templates/assets/javascripts/components/source/facts/index.ts @@ -0,0 +1,25 @@ +/* + * 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 new file mode 100644 index 00000000..7fac4813 --- /dev/null +++ b/src/templates/assets/javascripts/components/source/index.ts @@ -0,0 +1,24 @@ +/* + * 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 new file mode 100644 index 00000000..1e69df28 --- /dev/null +++ b/src/templates/assets/javascripts/components/tabs/index.ts @@ -0,0 +1,144 @@ +/* + * 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 new file mode 100644 index 00000000..04b8d85f --- /dev/null +++ b/src/templates/assets/javascripts/components/toc/index.ts @@ -0,0 +1,379 @@ +/* + * 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 new file mode 100644 index 00000000..82e88b61 --- /dev/null +++ b/src/templates/assets/javascripts/components/top/index.ts @@ -0,0 +1,184 @@ +/* + * 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 new file mode 100644 index 00000000..cf46f601 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/clipboard/index.ts @@ -0,0 +1,99 @@ +/* + * 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 new file mode 100644 index 00000000..5d91a9d5 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/index.ts @@ -0,0 +1,27 @@ +/* + * 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 new file mode 100644 index 00000000..5adf108a --- /dev/null +++ b/src/templates/assets/javascripts/integrations/instant/.eslintrc @@ -0,0 +1,6 @@ +{ + "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 new file mode 100644 index 00000000..d321b751 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/instant/index.ts @@ -0,0 +1,446 @@ +/* + * 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 new file mode 100644 index 00000000..0e217fa4 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/_/index.ts @@ -0,0 +1,332 @@ +/* + * 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 new file mode 100644 index 00000000..3d88d1c6 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/config/index.ts @@ -0,0 +1,115 @@ +/* + * 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 new file mode 100644 index 00000000..0fcbb19e --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/highlighter/index.ts @@ -0,0 +1,93 @@ +/* + * 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 new file mode 100644 index 00000000..94c010bb --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/index.ts @@ -0,0 +1,27 @@ +/* + * 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 new file mode 100644 index 00000000..9368ceb6 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/internal/.eslintrc @@ -0,0 +1,6 @@ +{ + "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 new file mode 100644 index 00000000..ae8f6104 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/internal/_/index.ts @@ -0,0 +1,74 @@ +/* + * 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 new file mode 100644 index 00000000..2a98b9e1 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/internal/extract/index.ts @@ -0,0 +1,107 @@ +/* + * 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 new file mode 100644 index 00000000..7cc3bf1a --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts @@ -0,0 +1,162 @@ +/* + * 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 new file mode 100644 index 00000000..c752329e --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/internal/index.ts @@ -0,0 +1,26 @@ +/* + * 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 new file mode 100644 index 00000000..f5089bc9 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts @@ -0,0 +1,136 @@ +/* + * 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 new file mode 100644 index 00000000..3031c7e3 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/query/.eslintrc @@ -0,0 +1,6 @@ +{ + "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 new file mode 100644 index 00000000..14482e43 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/query/_/index.ts @@ -0,0 +1,172 @@ +/* + * 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 new file mode 100644 index 00000000..763e2fd4 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/query/index.ts @@ -0,0 +1,25 @@ +/* + * 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 new file mode 100644 index 00000000..b96796f4 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/query/segment/index.ts @@ -0,0 +1,81 @@ +/* + * 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 new file mode 100644 index 00000000..41497786 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/query/transform/index.ts @@ -0,0 +1,99 @@ +/* + * 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 new file mode 100644 index 00000000..26713573 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/worker/_/index.ts @@ -0,0 +1,95 @@ +/* + * 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 new file mode 100644 index 00000000..7120ad6e --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/worker/index.ts @@ -0,0 +1,24 @@ +/* + * 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 new file mode 100644 index 00000000..3df9d551 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc @@ -0,0 +1,6 @@ +{ + "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 new file mode 100644 index 00000000..2df38080 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/worker/main/index.ts @@ -0,0 +1,192 @@ +/* + * 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 new file mode 100644 index 00000000..54d5001e --- /dev/null +++ b/src/templates/assets/javascripts/integrations/search/worker/message/index.ts @@ -0,0 +1,112 @@ +/* + * 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 new file mode 100644 index 00000000..08695bad --- /dev/null +++ b/src/templates/assets/javascripts/integrations/sitemap/index.ts @@ -0,0 +1,107 @@ +/* + * 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 new file mode 100644 index 00000000..38a5714d --- /dev/null +++ b/src/templates/assets/javascripts/integrations/version/.eslintrc @@ -0,0 +1,5 @@ +{ + "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 new file mode 100644 index 00000000..38d78f17 --- /dev/null +++ b/src/templates/assets/javascripts/integrations/version/index.ts @@ -0,0 +1,186 @@ +/* + * 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 new file mode 100644 index 00000000..9b7b0d5a --- /dev/null +++ b/src/templates/assets/javascripts/patches/indeterminate/index.ts @@ -0,0 +1,85 @@ +/* + * 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 new file mode 100644 index 00000000..b6e65fc0 --- /dev/null +++ b/src/templates/assets/javascripts/patches/index.ts @@ -0,0 +1,25 @@ +/* + * 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 new file mode 100644 index 00000000..607c46a0 --- /dev/null +++ b/src/templates/assets/javascripts/patches/scrollfix/index.ts @@ -0,0 +1,100 @@ +/* + * 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 new file mode 100644 index 00000000..4ec3e103 --- /dev/null +++ b/src/templates/assets/javascripts/patches/scrolllock/index.ts @@ -0,0 +1,89 @@ +/* + * 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 new file mode 100644 index 00000000..2aec8290 --- /dev/null +++ b/src/templates/assets/javascripts/polyfills/index.ts @@ -0,0 +1,96 @@ +/* + * 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 new file mode 100644 index 00000000..9b8f85f5 --- /dev/null +++ b/src/templates/assets/javascripts/templates/annotation/index.tsx @@ -0,0 +1,65 @@ +/* + * 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 new file mode 100644 index 00000000..95dbf12a --- /dev/null +++ b/src/templates/assets/javascripts/templates/clipboard/index.tsx @@ -0,0 +1,45 @@ +/* + * 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 new file mode 100644 index 00000000..b50b93b8 --- /dev/null +++ b/src/templates/assets/javascripts/templates/index.ts @@ -0,0 +1,29 @@ +/* + * 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 new file mode 100644 index 00000000..350c0505 --- /dev/null +++ b/src/templates/assets/javascripts/templates/search/index.tsx @@ -0,0 +1,170 @@ +/* + * 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 new file mode 100644 index 00000000..b59a8f67 --- /dev/null +++ b/src/templates/assets/javascripts/templates/source/index.tsx @@ -0,0 +1,47 @@ +/* + * 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 new file mode 100644 index 00000000..b283ac66 --- /dev/null +++ b/src/templates/assets/javascripts/templates/tabbed/index.tsx @@ -0,0 +1,56 @@ +/* + * 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 new file mode 100644 index 00000000..1fcba152 --- /dev/null +++ b/src/templates/assets/javascripts/templates/table/index.tsx @@ -0,0 +1,44 @@ +/* + * 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 new file mode 100644 index 00000000..ec583490 --- /dev/null +++ b/src/templates/assets/javascripts/templates/tooltip/index.tsx @@ -0,0 +1,42 @@ +/* + * 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 new file mode 100644 index 00000000..4aff7aa7 --- /dev/null +++ b/src/templates/assets/javascripts/templates/version/index.tsx @@ -0,0 +1,92 @@ +/* + * 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 new file mode 100644 index 00000000..d79b45b0 --- /dev/null +++ b/src/templates/assets/javascripts/utilities/h/.eslintrc @@ -0,0 +1,7 @@ +{ + "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 new file mode 100644 index 00000000..08d809f1 --- /dev/null +++ b/src/templates/assets/javascripts/utilities/h/index.ts @@ -0,0 +1,132 @@ +/* + * 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 new file mode 100644 index 00000000..42886e0b --- /dev/null +++ b/src/templates/assets/javascripts/utilities/index.ts @@ -0,0 +1,24 @@ +/* + * 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 new file mode 100644 index 00000000..3e6bf91a --- /dev/null +++ b/src/templates/assets/javascripts/utilities/round/index.ts @@ -0,0 +1,50 @@ +/* + * 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 new file mode 100644 index 00000000..e995b1ff --- /dev/null +++ b/src/templates/assets/javascripts/workers/search.ts @@ -0,0 +1,23 @@ +/* + * 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" diff --git a/src/templates/assets/stylesheets/_config.scss b/src/templates/assets/stylesheets/_config.scss new file mode 100644 index 00000000..e64b8e29 --- /dev/null +++ b/src/templates/assets/stylesheets/_config.scss @@ -0,0 +1,42 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Variables: breakpoints +// ---------------------------------------------------------------------------- + +// Device-specific breakpoints +$break-devices: ( + mobile: ( + portrait: px2em(220px) px2em(479.75px), + landscape: px2em(480px) px2em(719.75px) + ), + tablet: ( + portrait: px2em(720px) px2em(959.75px), + landscape: px2em(960px) px2em(1219.75px) + ), + screen: ( + small: px2em(1220px) px2em(1599.75px), + medium: px2em(1600px) px2em(1999.75px), + large: px2em(2000px) + ) +); diff --git a/src/templates/assets/stylesheets/main.scss b/src/templates/assets/stylesheets/main.scss new file mode 100644 index 00000000..2b203d3d --- /dev/null +++ b/src/templates/assets/stylesheets/main.scss @@ -0,0 +1,86 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Dependencies +// ---------------------------------------------------------------------------- + +@import "material-color"; +@import "material-shadows"; + +// ---------------------------------------------------------------------------- +// Local imports +// ---------------------------------------------------------------------------- + +@import "utilities/break"; +@import "utilities/convert"; + +@import "config"; + +@import "main/resets"; +@import "main/colors"; +@import "main/icons"; +@import "main/typeset"; + +@import "main/components/author"; +@import "main/components/banner"; +@import "main/components/base"; +@import "main/components/clipboard"; +@import "main/components/consent"; +@import "main/components/content"; +@import "main/components/dialog"; +@import "main/components/feedback"; +@import "main/components/footer"; +@import "main/components/form"; +@import "main/components/header"; +@import "main/components/meta"; +@import "main/components/nav"; +@import "main/components/pagination"; +@import "main/components/post"; +@import "main/components/progress"; +@import "main/components/search"; +@import "main/components/select"; +@import "main/components/sidebar"; +@import "main/components/source"; +@import "main/components/status"; +@import "main/components/tabs"; +@import "main/components/tag"; +@import "main/components/tooltip"; +@import "main/components/top"; +@import "main/components/version"; + +@import "main/extensions/markdown/admonition"; +@import "main/extensions/markdown/footnotes"; +@import "main/extensions/markdown/toc"; + +@import "main/extensions/pymdownx/arithmatex"; +@import "main/extensions/pymdownx/critic"; +@import "main/extensions/pymdownx/details"; +@import "main/extensions/pymdownx/emoji"; +@import "main/extensions/pymdownx/highlight"; +@import "main/extensions/pymdownx/keys"; +@import "main/extensions/pymdownx/tabbed"; +@import "main/extensions/pymdownx/tasklist"; + +@import "main/integrations/mermaid"; + +@import "main/modifiers"; diff --git a/src/templates/assets/stylesheets/main/_colors.scss b/src/templates/assets/stylesheets/main/_colors.scss new file mode 100644 index 00000000..68969fe9 --- /dev/null +++ b/src/templates/assets/stylesheets/main/_colors.scss @@ -0,0 +1,153 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Color variables +:root { + @extend %root; + + // Primary color shades + --md-primary-fg-color: hsla(#{hex2hsl($clr-indigo-500)}, 1); + --md-primary-fg-color--light: hsla(#{hex2hsl($clr-indigo-400)}, 1); + --md-primary-fg-color--dark: hsla(#{hex2hsl($clr-indigo-700)}, 1); + --md-primary-bg-color: hsla(0, 0%, 100%, 1); + --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7); + + // Accent color shades + --md-accent-fg-color: hsla(#{hex2hsl($clr-indigo-a200)}, 1); + --md-accent-fg-color--transparent: hsla(#{hex2hsl($clr-indigo-a200)}, 0.1); + --md-accent-bg-color: hsla(0, 0%, 100%, 1); + --md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7); +} + +// ---------------------------------------------------------------------------- + +// Allow to explicitly use color schemes in nested content +[data-md-color-scheme="default"] { + @extend %root; + + // Indicate that the site is rendered with a light color scheme + color-scheme: light; + + // Hide images for dark mode + img[src$="#only-dark"], + img[src$="#gh-dark-mode-only"] { + display: none; + } +} + +// ---------------------------------------------------------------------------- +// Placeholders +// ---------------------------------------------------------------------------- + +// Default theme, i.e. light mode +%root { + + // Color hue in the range [0,360] - change this variable to alter the tone + // of the theme, e.g. to make it more redish or greenish + --md-hue: 225deg; + + // Default color shades + --md-default-fg-color: hsla(0, 0%, 0%, 0.87); + --md-default-fg-color--light: hsla(0, 0%, 0%, 0.54); + --md-default-fg-color--lighter: hsla(0, 0%, 0%, 0.32); + --md-default-fg-color--lightest: hsla(0, 0%, 0%, 0.07); + --md-default-bg-color: hsla(0, 0%, 100%, 1); + --md-default-bg-color--light: hsla(0, 0%, 100%, 0.7); + --md-default-bg-color--lighter: hsla(0, 0%, 100%, 0.3); + --md-default-bg-color--lightest: hsla(0, 0%, 100%, 0.12); + + // Code color shades + --md-code-fg-color: hsla(200, 18%, 26%, 1); + --md-code-bg-color: hsla(200, 0%, 96%, 1); + + // Code highlighting color shades + --md-code-hl-color: hsla(#{hex2hsl($clr-blue-a200)}, 1); + --md-code-hl-color--light: hsla(#{hex2hsl($clr-blue-a200)}, 0.1); + --md-code-hl-number-color: hsla(0, 67%, 50%, 1); + --md-code-hl-special-color: hsla(340, 83%, 47%, 1); + --md-code-hl-function-color: hsla(291, 45%, 50%, 1); + --md-code-hl-constant-color: hsla(250, 63%, 60%, 1); + --md-code-hl-keyword-color: hsla(219, 54%, 51%, 1); + --md-code-hl-string-color: hsla(150, 63%, 30%, 1); + --md-code-hl-name-color: var(--md-code-fg-color); + --md-code-hl-operator-color: var(--md-default-fg-color--light); + --md-code-hl-punctuation-color: var(--md-default-fg-color--light); + --md-code-hl-comment-color: var(--md-default-fg-color--light); + --md-code-hl-generic-color: var(--md-default-fg-color--light); + --md-code-hl-variable-color: var(--md-default-fg-color--light); + + // Typeset color shades + --md-typeset-color: var(--md-default-fg-color); + + // Typeset `a` color shades + --md-typeset-a-color: var(--md-primary-fg-color); + + // Typeset `del` and `ins` color shades + --md-typeset-del-color: hsla(6, 90%, 60%, 0.15); + --md-typeset-ins-color: hsla(150, 90%, 44%, 0.15); + + // Typeset `kbd` color shades + --md-typeset-kbd-color: hsla(0, 0%, 98%, 1); + --md-typeset-kbd-accent-color: hsla(0, 100%, 100%, 1); + --md-typeset-kbd-border-color: hsla(0, 0%, 72%, 1); + + // Typeset `mark` color shades + --md-typeset-mark-color: hsla(#{hex2hsl($clr-yellow-a200)}, 0.5); + + // Typeset `table` color shades + --md-typeset-table-color: hsla(0, 0%, 0%, 0.12); + --md-typeset-table-color--light: hsla(0, 0%, 0%, 0.035); + + // Admonition color shades + --md-admonition-fg-color: var(--md-default-fg-color); + --md-admonition-bg-color: var(--md-default-bg-color); + + // Warning color shades + --md-warning-fg-color: hsla(0, 0%, 0%, 0.87); + --md-warning-bg-color: hsla(60, 100%, 80%, 1); + + // Footer color shades + --md-footer-fg-color: hsla(0, 0%, 100%, 1); + --md-footer-fg-color--light: hsla(0, 0%, 100%, 0.7); + --md-footer-fg-color--lighter: hsla(0, 0%, 100%, 0.45); + --md-footer-bg-color: hsla(0, 0%, 0%, 0.87); + --md-footer-bg-color--dark: hsla(0, 0%, 0%, 0.32); + + // Shadow depth 1 + --md-shadow-z1: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.05), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.1); + + // Shadow depth 2 + --md-shadow-z2: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.1), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.25); + + // Shadow depth 3 + --md-shadow-z3: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.2), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.35); +} diff --git a/src/templates/assets/stylesheets/main/_icons.scss b/src/templates/assets/stylesheets/main/_icons.scss new file mode 100644 index 00000000..9853e93d --- /dev/null +++ b/src/templates/assets/stylesheets/main/_icons.scss @@ -0,0 +1,37 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Icon +.md-icon { + + // SVG defaults + svg { + display: block; + width: px2rem(24px); + height: px2rem(24px); + fill: currentcolor; + } +} diff --git a/src/templates/assets/stylesheets/main/_modifiers.scss b/src/templates/assets/stylesheets/main/_modifiers.scss new file mode 100644 index 00000000..4b2b046a --- /dev/null +++ b/src/templates/assets/stylesheets/main/_modifiers.scss @@ -0,0 +1,48 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // [tablet +]: Allow for rendering content as sidebars + @include break-from-device(tablet) { + + // Modifier to float block elements + .inline { + float: inline-start; + width: px2rem(234px); + margin-inline-end: px2rem(16px); + margin-top: 0; + margin-bottom: px2rem(16px); + + // Modifier to move to end (ltr: right, rtl: left) + &.end { + float: inline-end; + margin-inline: px2rem(16px) 0; + } + } + } +} diff --git a/src/templates/assets/stylesheets/main/_resets.scss b/src/templates/assets/stylesheets/main/_resets.scss new file mode 100644 index 00000000..c6fc4b28 --- /dev/null +++ b/src/templates/assets/stylesheets/main/_resets.scss @@ -0,0 +1,118 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Enforce correct box model and prevent adjustments of font size after +// orientation changes in IE and iOS +html { + box-sizing: border-box; + text-size-adjust: none; +} + +// All elements shall inherit the document default +*, +*::before, +*::after { + box-sizing: inherit; + + // [reduced motion]: Disable all transitions + @media (prefers-reduced-motion) { + transition: none !important; // stylelint-disable-line + } +} + +// Remove margin in all browsers +body { + margin: 0; +} + +// Reset tap outlines on iOS and Android +a, +button, +label, +input { + -webkit-tap-highlight-color: transparent; +} + +// Reset link styles +a { + color: inherit; + text-decoration: none; +} + +// Normalize horizontal separator styles +hr { + box-sizing: content-box; + display: block; + height: px2rem(1px); + padding: 0; + overflow: visible; + border: 0; +} + +// Normalize font-size in all browsers +small { + font-size: 80%; +} + +// Prevent subscript and superscript from affecting line-height +sub, +sup { + line-height: 1em; +} + +// Remove border on image +img { + border-style: none; +} + +// Reset table styles +table { + border-spacing: 0; + border-collapse: separate; +} + +// Reset table cell styles +td, +th { + font-weight: 400; + vertical-align: top; +} + +// Reset button styles +button { + padding: 0; + margin: 0; + font-family: inherit; + font-size: inherit; + background: transparent; + border: 0; +} + +// Reset input styles +input { + border: 0; + outline: none; +} diff --git a/src/templates/assets/stylesheets/main/_typeset.scss b/src/templates/assets/stylesheets/main/_typeset.scss new file mode 100644 index 00000000..1c322859 --- /dev/null +++ b/src/templates/assets/stylesheets/main/_typeset.scss @@ -0,0 +1,603 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules: font definitions +// ---------------------------------------------------------------------------- + +// Enable font-smoothing in Webkit and FF +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + // Font with fallback for body copy + --md-text-font-family: + var(--md-text-font, _), + -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif; + + // Font with fallback for code + --md-code-font-family: + var(--md-code-font, _), + SFMono-Regular, Consolas, Menlo, monospace; +} + +// Define default fonts +body, +input, +aside { + font-family: var(--md-text-font-family); + font-feature-settings: "kern", "liga"; + color: var(--md-typeset-color); +} + +// Define monospaced fonts +code, +pre, +kbd { + font-family: var(--md-code-font-family); + font-feature-settings: "kern"; +} + +// ---------------------------------------------------------------------------- +// Rules: typesetted content +// ---------------------------------------------------------------------------- + +// General variables +:root { + --md-typeset-table-sort-icon: svg-load("material/sort.svg"); + --md-typeset-table-sort-icon--asc: svg-load("material/sort-ascending.svg"); + --md-typeset-table-sort-icon--desc: svg-load("material/sort-descending.svg"); +} + +// ---------------------------------------------------------------------------- + +// Content that is typeset - if possible, all margins, paddings and font sizes +// should be set in ems, so nested blocks (e.g. admonitions) render correctly. +.md-typeset { + font-size: px2rem(16px); + line-height: 1.6; + color-adjust: exact; + + // [print]: We'll use a smaller `font-size` for printing, so code examples + // don't break too early, and `16px` looks too big anyway. + @media print { + font-size: px2rem(13.6px); + } + + // Default spacing + ul, + ol, + dl, + figure, + blockquote, + pre { + margin-block: 1em; + } + + // Headline on level 1 + h1 { + margin: 0 0 px2em(40px, 32px); + font-size: px2em(32px); + font-weight: 300; + line-height: 1.3; + color: var(--md-default-fg-color--light); + letter-spacing: -0.01em; + } + + // Headline on level 2 + h2 { + margin: px2em(40px, 25px) 0 px2em(16px, 25px); + font-size: px2em(25px); + font-weight: 300; + line-height: 1.4; + letter-spacing: -0.01em; + } + + // Headline on level 3 + h3 { + margin: px2em(32px, 20px) 0 px2em(16px, 20px); + font-size: px2em(20px); + font-weight: 400; + line-height: 1.5; + letter-spacing: -0.01em; + } + + // Headline on level 3 following level 2 + h2 + h3 { + margin-top: px2em(16px, 20px); + } + + // Headline on level 4 + h4 { + margin: px2em(16px) 0; + font-weight: 700; + letter-spacing: -0.01em; + } + + // Headline on level 5-6 + h5, + h6 { + margin: px2em(16px, 12.8px) 0; + font-size: px2em(12.8px); + font-weight: 700; + color: var(--md-default-fg-color--light); + letter-spacing: -0.01em; + } + + // Headline on level 5 + h5 { + text-transform: uppercase; + } + + // Horizontal separator + hr { + display: flow-root; + margin: 1.5em 0; + border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest); + } + + // Text link + a { + color: var(--md-typeset-a-color); + word-break: break-word; + + // Also enable color transition on pseudo elements + &, + &::before { + transition: color 125ms; + } + + // Text link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + + // Inline code block + code { + background-color: var(--md-accent-fg-color--transparent); + } + } + + // Inline code block + code { + color: currentcolor; + transition: background-color 125ms; + } + + // Show outline for keyboard devices + &.focus-visible { + outline-color: var(--md-accent-fg-color); + outline-offset: px2rem(4px); + } + } + + // Code block + code, + pre, + kbd { + font-variant-ligatures: none; + color: var(--md-code-fg-color); + direction: ltr; + + // [print]: Wrap text and hide scollbars + @media print { + white-space: pre-wrap; + } + } + + // Inline code block + code { + padding: 0 px2em(4px, 13.6px); + font-size: px2em(13.6px); + word-break: break-word; + background-color: var(--md-code-bg-color); + border-radius: px2rem(2px); + box-decoration-break: clone; + + // Hide outline for pointer devices + &:not(.focus-visible) { + outline: none; + -webkit-tap-highlight-color: transparent; + } + } + + // Unformatted content + pre { + position: relative; + display: flow-root; + line-height: 1.4; + + // Code block + > code { + display: block; + padding: px2em(10.5px, 13.6px) px2em(16px, 13.6px); + margin: 0; + overflow: auto; + word-break: normal; + touch-action: auto; + outline-color: var(--md-accent-fg-color); + box-shadow: none; + box-decoration-break: slice; + scrollbar-width: thin; + scrollbar-color: var(--md-default-fg-color--lighter) transparent; + + // Code block on hover + &:hover { + scrollbar-color: var(--md-accent-fg-color) transparent; + } + + // Webkit scrollbar + &::-webkit-scrollbar { + width: px2rem(4px); + height: px2rem(4px); + } + + // Webkit scrollbar thumb + &::-webkit-scrollbar-thumb { + background-color: var(--md-default-fg-color--lighter); + + // Webkit scrollbar thumb on hover + &:hover { + background-color: var(--md-accent-fg-color); + } + } + } + } + + // Keyboard key + kbd { + display: inline-block; + padding: 0 px2em(8px, 12px); + font-size: px2em(12px); + color: var(--md-default-fg-color); + word-break: break-word; + vertical-align: text-top; + background-color: var(--md-typeset-kbd-color); + border-radius: px2rem(2px); + box-shadow: + 0 px2rem(2px) 0 px2rem(1px) var(--md-typeset-kbd-border-color), + 0 px2rem(2px) 0 var(--md-typeset-kbd-border-color), + 0 px2rem(-2px) px2rem(4px) var(--md-typeset-kbd-accent-color) inset; + } + + // Text highlighting marker + mark { + color: inherit; + word-break: break-word; + background-color: var(--md-typeset-mark-color); + box-decoration-break: clone; + } + + // Abbreviation + abbr { + text-decoration: none; + cursor: help; + border-bottom: px2rem(1px) dotted var(--md-default-fg-color--light); + + // Show tooltip for touch devices + @media (hover: none) { + + // Tooltip + &[title]:is(:focus, :hover)::after { + position: absolute; + inset-inline: px2rem(16px); + padding: px2rem(4px) px2rem(6px); + margin-top: 2em; + font-size: px2rem(14px); + color: var(--md-default-bg-color); + content: attr(title); + background-color: var(--md-default-fg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z3); + } + } + } + + // Small text + small { + opacity: 0.75; + } + + // Superscript and subscript + sup, + sub { + margin-inline-start: px2em(1px, 12.8px); + } + + // Blockquotes, possibly nested + blockquote { + padding-inline-start: px2rem(12px); + margin-inline: 0; + color: var(--md-default-fg-color--light); + border-inline-start: px2rem(4px) solid var(--md-default-fg-color--lighter); + } + + // Unordered list + ul { + list-style-type: disc; + } + + // Unordered and ordered list + ul, + ol { + padding: 0; + margin-inline-start: px2em(10px); + + // Adjust display mode if not hidden + &:not([hidden]) { + display: flow-root; + } + + // Nested ordered list + ol { + list-style-type: lower-alpha; + + // Triply nested ordered list + ol { + list-style-type: lower-roman; + } + } + + // List element + li { + margin-inline-start: px2em(20px); + margin-bottom: 0.5em; + + // Adjust spacing + p, + blockquote { + margin: 0.5em 0; + } + + // Adjust spacing on last child + &:last-child { + margin-bottom: 0; + } + + // Nested list + :is(ul, ol) { + margin-block: 0.5em; + margin-inline-start: px2em(10px); + } + } + } + + // Definition list + dd { + margin-block: 1em 1.5em; + margin-inline-start: px2em(30px); + } + + // Image or video + img, + svg, + video { + max-width: 100%; + height: auto; + } + + // Image + img { + + // Adjust spacing when left-aligned + &[align="left"] { + margin: 1em; + margin-left: 0; + } + + // Adjust spacing when right-aligned + &[align="right"] { + margin: 1em; + margin-right: 0; + } + + // Adjust spacing when sole children + &[align]:only-child { + margin-top: 0; + } + } + + // Figure + figure { + display: flow-root; + width: fit-content; + max-width: 100%; + margin: 1em auto; + text-align: center; + + // Figure images + img { + display: block; + } + } + + // Figure caption + figcaption { + max-width: px2rem(480px); + margin: 1em auto; + font-style: italic; + } + + // Limit width to container + iframe { + max-width: 100%; + } + + // Data table + table:not([class]) { + display: inline-block; + max-width: 100%; + overflow: auto; + font-size: px2rem(12.8px); + touch-action: auto; + background-color: var(--md-default-bg-color); + border: px2rem(1px) solid var(--md-typeset-table-color); + border-radius: px2rem(2px); + + // [print]: Reset display mode so table header wraps when printing + @media print { + display: table; + } + + // Due to margin collapse because of the necessary inline-block hack, we + // cannot increase the bottom margin on the table, so we just increase the + // top margin on the following element + + * { + margin-top: 1.5em; + } + + // Elements in table heading and cell + :is(th, td) > * { + + // Adjust spacing on first child + &:first-child { + margin-top: 0; + } + + // Adjust spacing on last child + &:last-child { + margin-bottom: 0; + } + } + + // Table heading and cell + :is(th, td):not([align]) { + text-align: left; + + // Adjust for right-to-left languages + [dir="rtl"] & { + text-align: right; + } + } + + // Table heading + th { + min-width: px2rem(100px); + padding: px2em(12px, 12.8px) px2em(16px, 12.8px); + font-weight: 700; + vertical-align: top; + } + + // Table cell + td { + padding: px2em(12px, 12.8px) px2em(16px, 12.8px); + vertical-align: top; + border-top: px2rem(1px) solid var(--md-typeset-table-color); + } + + // Table body row + tbody tr { + transition: background-color 125ms; + + // Table row on hover + &:hover { + background-color: var(--md-typeset-table-color--light); + box-shadow: 0 px2rem(1px) 0 var(--md-default-bg-color) inset; + } + } + + // Text link in table + a { + word-break: normal; + } + } + + // Sortable table + table th[role="columnheader"] { + cursor: pointer; + + // Sort icon + &::after { + display: inline-block; + width: 1.2em; + height: 1.2em; + margin-inline-start: 0.5em; + vertical-align: text-bottom; + content: ""; + transition: background-color 125ms; + mask-image: var(--md-typeset-table-sort-icon); + mask-repeat: no-repeat; + mask-size: contain; + } + + // Show sort icon on hover + &:hover::after { + background-color: var(--md-default-fg-color--lighter); + } + + // Sort ascending icon + &[aria-sort="ascending"]::after { + background-color: var(--md-default-fg-color--light); + mask-image: var(--md-typeset-table-sort-icon--asc); + } + + // Sort descending icon + &[aria-sort="descending"]::after { + background-color: var(--md-default-fg-color--light); + mask-image: var(--md-typeset-table-sort-icon--desc); + } + } + + // Data table scroll wrapper + &__scrollwrap { + margin: 1em px2rem(-16px); + overflow-x: auto; + touch-action: auto; + } + + // Data table wrapper + &__table { + display: inline-block; + padding: 0 px2rem(16px); + margin-bottom: 0.5em; + + // [print]: Reset display mode so table header wraps when printing + @media print { + display: block; + } + + // Data table + html & table { + display: table; + width: 100%; + margin: 0; + overflow: hidden; + } + } +} + +// ---------------------------------------------------------------------------- +// Rules: top-level +// ---------------------------------------------------------------------------- + +// [mobile -]: Align with body copy +@include break-to-device(mobile) { + + // Top-level unformatted content + .md-content__inner > pre { + margin: 1em px2rem(-16px); + + // Code block + code { + border-radius: 0; + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_author.scss b/src/templates/assets/stylesheets/main/components/_author.scss new file mode 100644 index 00000000..111baf40 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_author.scss @@ -0,0 +1,86 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Author, i.e., GitHub user + .md-author { + position: relative; + display: block; + flex-shrink: 0; + width: px2rem(32px); + height: px2rem(32px); + overflow: hidden; + transition: + color 125ms, + transform 125ms; + + // Author image + img { + display: block; + border-radius: 100%; + } + + // More authors + &--more { + font-size: px2rem(12px); + font-weight: 700; + line-height: px2rem(32px); + color: var(--md-default-fg-color--lighter); + text-align: center; + background: var(--md-default-fg-color--lightest); + } + + // Enlarge image + &--long { + width: px2rem(48px); + height: px2rem(48px); + } + } + + // Author link + a.md-author { + transform: scale(1); + + // Author image + img { + filter: grayscale(100%) opacity(75%); + transition: filter 125ms; + } + + // Author on focus/hover + &:is(:focus, :hover) { + z-index: 1; + transform: scale(1.1); + + // Author image + img { + filter: grayscale(0%); + } + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_banner.scss b/src/templates/assets/stylesheets/main/components/_banner.scss new file mode 100644 index 00000000..8fe08c0f --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_banner.scss @@ -0,0 +1,68 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Banner for announcements and warnings +.md-banner { + overflow: auto; + color: var(--md-footer-fg-color); + background-color: var(--md-footer-bg-color); + + // [print]: Hide banner + @media print { + display: none; + } + + // Banner with warning + &--warning { + color: var(--md-warning-fg-color); + background-color: var(--md-warning-bg-color); + } + + // Banner wrapper + &__inner { + padding: 0 px2rem(16px); + margin: px2rem(12px) auto; + font-size: px2rem(14px); + } + + // Banner button + &__button { + float: inline-end; + color: inherit; + cursor: pointer; + transition: opacity 250ms; + + // [no-js]: Hide button + .no-js & { + display: none; + } + + // Button on hover + &:hover { + opacity: 0.7; + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_base.scss b/src/templates/assets/stylesheets/main/components/_base.scss new file mode 100644 index 00000000..33f834ed --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_base.scss @@ -0,0 +1,182 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules: base grid and containers +// ---------------------------------------------------------------------------- + +// Stretch container to viewport and set base `font-size` +html { + height: 100%; + overflow-x: hidden; + // Hack: normally, we would set the base `font-size` to `62.5%`, so we can + // base all calculations on `10px`, but Chromium and Chrome define a minimal + // `font-size` of `12px` if the system language is set to Chinese. For this + // reason we just double the `font-size` and set it to `20px`. + // + // See https://github.com/squidfunk/mkdocs-material/issues/911 + font-size: 125%; + + // [screen medium +]: Set base `font-size` to `11px` + @include break-from-device(screen medium) { + font-size: 137.5%; + } + + // [screen large +]: Set base `font-size` to `12px` + @include break-from-device(screen large) { + font-size: 150%; + } +} + +// Stretch body to container - flexbox is used, so the footer will always be +// aligned to the bottom of the viewport +body { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + min-height: 100%; + // Hack: reset `font-size` to `10px`, so the spacing for all inline elements + // is correct again. Otherwise the spacing would be based on `20px`. + font-size: px2rem(10px); + background-color: var(--md-default-bg-color); + + // [print]: Omit flexbox layout due to a Firefox bug (https://mzl.la/39DgR3m) + @media print { + display: block; + } + + // Body in locked state + &[data-md-scrolllock] { + + // [tablet portrait -]: Omit scroll bubbling + @include break-to-device(tablet portrait) { + position: fixed; + } + } +} + +// ---------------------------------------------------------------------------- + +// Grid container - this class is applied to wrapper elements within the +// header, content area and footer, and makes sure that their width is limited +// to `1220px`, and they are rendered centered if the screen is larger. +.md-grid { + max-width: px2rem(1220px); + margin-inline: auto; +} + +// Main container +.md-container { + display: flex; + flex-direction: column; + flex-grow: 1; + + // [print]: Omit flexbox layout due to a Firefox bug (https://mzl.la/39DgR3m) + @media print { + display: block; + } +} + +// Main area - stretch to remaining space of container +.md-main { + flex-grow: 1; + + // Main area wrapper + &__inner { + display: flex; + height: 100%; + margin-top: px2rem(24px + 6px); + } +} + +// Add ellipsis in case of overflowing text +.md-ellipsis { + overflow: hidden; + text-overflow: ellipsis; +} + +// ---------------------------------------------------------------------------- +// Rules: navigational elements +// ---------------------------------------------------------------------------- + +// Toggle - this class is applied to checkbox elements, which are used to +// implement the CSS-only drawer and navigation, as well as the search +.md-toggle { + display: none; +} + +// Option - this class is applied to radio elements, which are used to +// implement the color palette toggle +.md-option { + position: absolute; + width: 0; + height: 0; + opacity: 0; + + // Option label for checked radio button + &:checked + label:not([hidden]) { + display: block; + } + + // Show outline for keyboard devices + &.focus-visible + label { + outline-style: auto; + outline-color: var(--md-accent-fg-color); + } +} + +// Skip link +.md-skip { + position: fixed; + // Hack: if we don't set the negative `z-index`, the skip link will force the + // creation of new layers when code blocks are near the header on scrolling + z-index: -1; + padding: px2rem(6px) px2rem(10px); + margin: px2rem(10px); + font-size: px2rem(12.8px); + color: var(--md-default-bg-color); + background-color: var(--md-default-fg-color); + border-radius: px2rem(2px); + outline-color: var(--md-accent-fg-color); + opacity: 0; + transform: translateY(px2rem(8px)); + + // Show skip link on focus + &:focus { + z-index: 10; + opacity: 1; + transition: + transform 250ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 175ms 75ms; + transform: translateY(0); + } +} + +// ---------------------------------------------------------------------------- +// Rules: print styles +// ---------------------------------------------------------------------------- + +// Add margins to page +@page { + margin: 25mm; +} diff --git a/src/templates/assets/stylesheets/main/components/_clipboard.scss b/src/templates/assets/stylesheets/main/components/_clipboard.scss new file mode 100644 index 00000000..c07c9c67 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_clipboard.scss @@ -0,0 +1,102 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Clipboard button variables +:root { + --md-clipboard-icon: svg-load("material/content-copy.svg"); +} + +// ---------------------------------------------------------------------------- + +// Clipboard button +.md-clipboard { + position: absolute; + top: px2em(8px); + right: px2em(8px); + z-index: 1; + width: px2em(24px); + height: px2em(24px); + color: var(--md-default-fg-color--lightest); + cursor: pointer; + border-radius: px2rem(2px); + outline-color: var(--md-accent-fg-color); + outline-offset: px2rem(2px); + transition: color 250ms; + + // [print]: Hide button + @media print { + display: none; + } + + // Hide outline for pointer devices + &:not(.focus-visible) { + outline: none; + -webkit-tap-highlight-color: transparent; + } + + // Darken color on code block hover + :hover > & { + color: var(--md-default-fg-color--light); + } + + // Button on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + } + + // Button icon - the width and height are defined in `em`, so the size is + // automatically adjusted for nested code blocks (e.g. in admonitions) + &::after { + display: block; + width: px2em(18px); + height: px2em(18px); + margin: 0 auto; + content: ""; + background-color: currentcolor; + mask-image: var(--md-clipboard-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + + // Inline clipboard button + &--inline { + cursor: pointer; + + // Code block + code { + transition: + color 250ms, + background-color 250ms; + } + + // Code block on focus/hover + &:is(:focus, :hover) code { + color: var(--md-accent-fg-color); + background-color: var(--md-accent-fg-color--transparent); + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_consent.scss b/src/templates/assets/stylesheets/main/components/_consent.scss new file mode 100644 index 00000000..5502460c --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_consent.scss @@ -0,0 +1,127 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Keyframes +// ---------------------------------------------------------------------------- + +// Show consent +@keyframes consent { + 0% { + opacity: 0; + transform: translateY(100%); + } + + 100% { + opacity: 1; + transform: translateY(0); + } +} + +// Show consent overlay +@keyframes overlay { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Consent +.md-consent { + + // Consent overlay + &__overlay { + position: fixed; + top: 0; + z-index: 5; + width: 100%; + height: 100%; + background-color: hsla(0, 0%, 0%, 0.54); + opacity: 1; + backdrop-filter: blur(px2rem(2px)); + animation: overlay 250ms both; + } + + // Consent wrapper + &__inner { + position: fixed; + bottom: 0; + z-index: 5; + width: 100%; + max-height: 100%; + padding: 0; + overflow: auto; + background-color: var(--md-default-bg-color); + border: 0; + border-radius: px2rem(2px); + box-shadow: + 0 0 px2rem(4px) rgba(0, 0, 0, 0.1), + 0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0.2); + animation: consent 500ms cubic-bezier(0.1, 0.7, 0.1, 1) both; + } + + // Consent form + &__form { + padding: px2rem(16px); + } + + // Consent settings + &__settings { + display: none; + margin: 1em 0; + + // Show settings + input:checked + & { + display: block; + } + } + + // Consent controls + &__controls { + margin-bottom: px2rem(16px); + + // Consent control button + .md-typeset & .md-button { + display: inline; + + // [tablet +]: Align buttons horizontally + @include break-to-device(mobile) { + display: block; + width: 100%; + margin-top: px2rem(8px); + text-align: center; + } + } + } + + // Ensure users realize that labels are clickaböe + label { + cursor: pointer; + } +} diff --git a/src/templates/assets/stylesheets/main/components/_content.scss b/src/templates/assets/stylesheets/main/components/_content.scss new file mode 100644 index 00000000..7c945749 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_content.scss @@ -0,0 +1,97 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Content area +.md-content { + flex-grow: 1; + // Hack: we must use `min-width: 0`, so the content area is capped by the + // dimensions of its parent. Otherwise, long code blocks might lead to a + // wider content area which will overflow. See https://bit.ly/3bP3f8k + min-width: 0; + + // Content wrapper + &__inner { + padding-top: px2rem(12px); + margin: 0 px2rem(16px) px2rem(24px); + + // [screen +]: Adjust spacing between content area and sidebars + @include break-from-device(screen) { + + // Sidebar with navigation is visible + .md-sidebar--primary:not([hidden]) ~ .md-content > & { + margin-inline-start: px2rem(24px); + } + + // Sidebar with table of contents is visible + .md-sidebar--secondary:not([hidden]) ~ .md-content > & { + margin-inline-end: px2rem(24px); + } + } + + // Hack: add pseudo element for spacing, as the overflow of the content + // container may not be hidden due to an imminent offset error on targets + &::before { + display: block; + height: px2rem(8px); + content: ""; + } + + // Adjust spacing on last child + > :last-child { + margin-bottom: 0; + } + } + + // Button inside of the content area - these buttons are meant for actions on + // a document-level, i.e. linking to related source code files, printing etc. + &__button { + float: inline-end; + padding: 0; + margin: px2rem(8px) 0; + margin-inline-start: px2rem(8px); + + // [print]: Hide buttons + @media print { + display: none; + } + + // Adjust default link color for icons + .md-typeset & { + color: var(--md-default-fg-color--lighter); + } + + // Align with body copy located next to icon + svg { + display: inline; + vertical-align: top; + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: scaleX(-1); + } + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_dialog.scss b/src/templates/assets/stylesheets/main/components/_dialog.scss new file mode 100644 index 00000000..16782ede --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_dialog.scss @@ -0,0 +1,65 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Dialog +.md-dialog { + position: fixed; + bottom: px2rem(16px); + z-index: 4; + min-width: px2rem(222px); + padding: px2rem(8px) px2rem(12px); + pointer-events: none; + background-color: var(--md-default-fg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z3); + opacity: 0; + transition: + transform 0ms 400ms, + opacity 400ms; + transform: translateY(100%); + inset-inline-end: px2rem(16px); + + // [print]: Hide dialog + @media print { + display: none; + } + + // Active dialog + &--active { + pointer-events: initial; + opacity: 1; + transition: + transform 400ms cubic-bezier(0.075, 0.85, 0.175, 1), + opacity 400ms; + transform: translateY(0); + } + + // Dialog wrapper + &__inner { + font-size: px2rem(14px); + color: var(--md-default-bg-color); + } +} diff --git a/src/templates/assets/stylesheets/main/components/_feedback.scss b/src/templates/assets/stylesheets/main/components/_feedback.scss new file mode 100644 index 00000000..bbcd00e9 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_feedback.scss @@ -0,0 +1,110 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Was this page helpful? +.md-feedback { + margin: 2em 0 1em; + text-align: center; + + // Feedback fieldset + fieldset { + padding: 0; + margin: 0; + border: none; + } + + // Feedback title + &__title { + margin: 1em auto; + font-weight: 700; + } + + // Feedback wrapper + &__inner { + position: relative; + } + + // Feedback list + &__list { + position: relative; + display: flex; + flex-wrap: wrap; + align-content: baseline; + justify-content: center; + + // Feedback icon on hover + &:hover .md-icon:not(:disabled) { + color: var(--md-default-fg-color--lighter); + } + + // Adjust height after submission + :disabled & { + min-height: px2rem(36px); + } + } + + // Feedback icon + &__icon { + flex-shrink: 0; + margin: 0 px2rem(2px); + color: var(--md-default-fg-color--light); + cursor: pointer; + transition: color 125ms; + + // Feedback icon on hover + &:not(:disabled).md-icon:hover { + color: var(--md-accent-fg-color); + } + + // Feedback icon after submit + &:disabled { + color: var(--md-default-fg-color--lightest); + pointer-events: none; + } + } + + // Feedback note + &__note { + position: relative; + opacity: 0; + transition: + transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 150ms; + transform: translateY(px2rem(8px)); + + // Feedback note value + > * { + max-width: px2rem(320px); + margin: 0 auto; + } + + // Show after submission + :disabled & { + opacity: 1; + transform: translateY(0); + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_footer.scss b/src/templates/assets/stylesheets/main/components/_footer.scss new file mode 100644 index 00000000..9fabc05b --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_footer.scss @@ -0,0 +1,201 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Footer +.md-footer { + color: var(--md-footer-fg-color); + background-color: var(--md-footer-bg-color); + + // [print]: Hide footer + @media print { + display: none; + } + + // Footer wrapper + &__inner { + justify-content: space-between; + padding: px2rem(4px); + overflow: auto; + + // Footer is visible + &:not([hidden]) { + display: flex; + } + } + + // Footer link to previous and next page + &__link { + display: flex; + // Hack: some browsers induce ellipsis on flex children that are set to + // `overflow: hidden` and `text-overflow: ellipsis`. Enforcing growth by + // a tiny factor seems to get rid of the ellipsis and renders the text as + // it should - see https://bit.ly/2ZUCXQ8 + flex-grow: 0.01; + align-items: end; + max-width: 100%; + margin-block: px2rem(20px) px2rem(8px); + overflow: hidden; + outline-color: var(--md-accent-fg-color); + transition: opacity 250ms; + + // Footer link on focus/hover + &:is(:focus, :hover) { + opacity: 0.7; + } + + // Adjust for right-to-left languages + [dir="rtl"] & svg { + transform: scaleX(-1); + } + + // [mobile -]: Adjust width to 25/75 and hide title + @include break-to-device(mobile) { + + // Footer link to previous page + &--prev { + flex-shrink: 0; + + // Hide footer title + .md-footer__title { + display: none; + } + } + } + + // Footer link to next page + &--next { + margin-inline-start: auto; + text-align: right; + + // Adjust for right-to-left languages + [dir="rtl"] & { + text-align: left; + } + } + } + + // Footer title + &__title { + flex-grow: 1; + max-width: calc(100% - #{px2rem(48px)}); + padding: 0 px2rem(20px); + margin-bottom: px2rem(14px); + font-size: px2rem(18px); + white-space: nowrap; + } + + // Footer link button + &__button { + padding: px2rem(8px); + margin: px2rem(4px); + } + + // Footer link direction (i.e. prev and next) + &__direction { + font-size: px2rem(12.8px); + opacity: 0.7; + } +} + +// Footer metadata +.md-footer-meta { + background-color: var(--md-footer-bg-color--dark); + + // Footer metadata wrapper + &__inner { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + padding: px2rem(4px); + } + + // Lighten color for non-hovered text links + html &.md-typeset a { + color: var(--md-footer-fg-color--light); + + // Text link on focus/hover + &:is(:focus, :hover) { + color: var(--md-footer-fg-color); + } + } +} + +// ---------------------------------------------------------------------------- + +// Copyright and theme information +.md-copyright { + width: 100%; + padding: px2rem(8px) 0; + margin: auto px2rem(12px); + font-size: px2rem(12.8px); + color: var(--md-footer-fg-color--lighter); + + // [tablet portrait +]: Show copyright and social links in one line + @include break-from-device(tablet portrait) { + width: auto; + } + + // Footer copyright highlight - this is the upper part of the copyright and + // theme information, which will include a darker color than the theme link + &__highlight { + color: var(--md-footer-fg-color--light); + } +} + +// ---------------------------------------------------------------------------- + +// Social links +.md-social { + display: inline-flex; + gap: px2rem(4px); + padding: px2rem(4px) 0 px2rem(12px); + margin: 0 px2rem(8px); + + // [tablet portrait +]: Show copyright and social links in one line + @include break-from-device(tablet portrait) { + padding: px2rem(12px) 0; + } + + // Footer social link + &__link { + display: inline-block; + width: px2rem(32px); + height: px2rem(32px); + text-align: center; + + // Adjust line-height to match height for correct alignment + &::before { + line-height: 1.9; + } + + // Fill icon with current color + svg { + max-height: px2rem(16px); + vertical-align: -25%; + fill: currentcolor; + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_form.scss b/src/templates/assets/stylesheets/main/components/_form.scss new file mode 100644 index 00000000..49b59e42 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_form.scss @@ -0,0 +1,83 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Form button + .md-button { + display: inline-block; + padding: px2em(10px) px2em(32px); + font-weight: 700; + color: var(--md-primary-fg-color); + cursor: pointer; + border: px2rem(2px) solid currentcolor; + border-radius: px2rem(2px); + transition: + color 125ms, + background-color 125ms, + border-color 125ms; + + // Primary button + &--primary { + color: var(--md-primary-bg-color); + background-color: var(--md-primary-fg-color); + border-color: var(--md-primary-fg-color); + } + + // Button on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-bg-color); + background-color: var(--md-accent-fg-color); + border-color: var(--md-accent-fg-color); + } + } + + // Form input + .md-input { + height: px2rem(36px); + padding: 0 px2rem(12px); + font-size: px2rem(16px); + border-bottom: px2rem(2px) solid var(--md-default-fg-color--lighter); + border-start-start-radius: px2rem(2px); + border-start-end-radius: px2rem(2px); + box-shadow: var(--md-shadow-z1); + transition: + border 250ms, + box-shadow 250ms; + + // Input on focus/hover + &:is(:focus, :hover) { + border-bottom-color: var(--md-accent-fg-color); + box-shadow: var(--md-shadow-z2); + } + + // Stretch to full width + &--stretch { + width: 100%; + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_header.scss b/src/templates/assets/stylesheets/main/components/_header.scss new file mode 100644 index 00000000..e51f3f99 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_header.scss @@ -0,0 +1,270 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Header - by default, the header will be sticky and stay always on top of the +// viewport. If this behavior is not desired, just set `position: static`. +.md-header { + position: sticky; + inset-inline: 0; + top: 0; + z-index: 4; + display: block; + color: var(--md-primary-bg-color); + background-color: var(--md-primary-fg-color); + // Hack: reduce jitter by adding a transparent box shadow of the same size + // so the size of the layer doesn't change during animation + box-shadow: + 0 0 px2rem(4px) rgba(0, 0, 0, 0), + 0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0); + + // [print]: Hide header + @media print { + display: none; + } + + // Header is hidden + &[hidden] { + transition: + transform 250ms cubic-bezier(0.8, 0, 0.6, 1), + box-shadow 250ms; + transform: translateY(-100%); + } + + // Header in shadow state, i.e. shadow is visible + &--shadow { + box-shadow: + 0 0 px2rem(4px) rgba(0, 0, 0, 0.1), + 0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0.2); + transition: + transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1), + box-shadow 250ms; + } + + // Header wrapper + &__inner { + display: flex; + align-items: center; + padding: 0 px2rem(4px); + } + + // Header button + &__button { + position: relative; + z-index: 1; + padding: px2rem(8px); + margin: px2rem(4px); + color: currentcolor; + vertical-align: middle; + cursor: pointer; + outline-color: var(--md-accent-fg-color); + transition: opacity 250ms; + + // Button on hover + &:hover { + opacity: 0.7; + } + + // Header button is visible + &:not([hidden]) { + display: inline-block; + } + + // Hide outline for pointer devices + &:not(.focus-visible) { + outline: none; + -webkit-tap-highlight-color: transparent; + } + + // Button with logo, pointing to `config.site_url` + &.md-logo { + padding: px2rem(8px); + margin: px2rem(4px); + + // [tablet -]: Hide button + @include break-to-device(tablet) { + display: none; + } + + // Image or icon + :is(img, svg) { + display: block; + width: auto; + height: px2rem(24px); + fill: currentcolor; + } + } + + // Button for search + &[for="__search"] { + + // [tablet landscape +]: Hide button + @include break-from-device(tablet landscape) { + display: none; + } + + // [no-js]: Hide button + .no-js & { + display: none; + } + + // Adjust for right-to-left languages + [dir="rtl"] & svg { + transform: scaleX(-1); + } + } + + // Button for drawer + &[for="__drawer"] { + + // [screen +]: Hide button + @include break-from-device(screen) { + display: none; + } + } + } + + // Header topic + &__topic { + position: absolute; + display: flex; + max-width: 100%; + white-space: nowrap; + transition: + transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 150ms; + + // Second header topic - title of the current page + & + & { + z-index: -1; + pointer-events: none; + opacity: 0; + transition: + transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1), + opacity 150ms; + transform: translateX(px2rem(25px)); + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translateX(px2rem(-25px)); + } + } + + // Adjust font weight of site title + &:first-child { + font-weight: 700; + } + } + + // Header title + &__title { + flex-grow: 1; + height: px2rem(48px); + margin-inline-start: px2rem(20px); + margin-inline-end: px2rem(8px); + font-size: px2rem(18px); + line-height: px2rem(48px); + + // Header title in active state, i.e. page title is visible + &--active .md-header__topic { + z-index: -1; + pointer-events: none; + opacity: 0; + transition: + transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1), + opacity 150ms; + transform: translateX(px2rem(-25px)); + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translateX(px2rem(25px)); + } + + // Second header topic - title of the current page + + .md-header__topic { + z-index: 0; + pointer-events: initial; + opacity: 1; + transition: + transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 150ms; + transform: translateX(0); + } + } + + // Add ellipsis in case of overflowing text + > .md-header__ellipsis { + position: relative; + width: 100%; + height: 100%; + } + } + + // Header option + &__option { + display: flex; + flex-shrink: 0; + max-width: 100%; + white-space: nowrap; + transition: + max-width 0ms 250ms, + opacity 250ms 250ms; + + // Hide toggle when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + max-width: 0; + opacity: 0; + transition: + max-width 0ms, + opacity 0ms; + } + + // Hack: Firefox 117 introduces a bug where the browser scrolls the page by + // a small amount to the top every time the header button is focused. After + // investigating, we're confident that it seems to be caused by the input + // field being too close to the border - see https://t.ly/APO8l + > input { + bottom: 0; + } + } + + // Repository information container + &__source { + display: none; + + // [tablet landscape +]: Show repository information + @include break-from-device(tablet landscape) { + display: block; + width: px2rem(234px); + max-width: px2rem(234px); + margin-inline-start: px2rem(20px); + } + + // [screen +]: Adjust spacing of search bar + @include break-from-device(screen) { + margin-inline-start: px2rem(28px); + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_meta.scss b/src/templates/assets/stylesheets/main/components/_meta.scss new file mode 100644 index 00000000..aaeae8df --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_meta.scss @@ -0,0 +1,67 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Metadata +.md-meta { + font-size: px2rem(14px); + line-height: 1.3; + color: var(--md-default-fg-color--light); + + // Metadata list + &__list { + display: inline-flex; + flex-wrap: wrap; + padding: 0; + margin: 0; + list-style: none; + } + + // Metadata item separator + &__item:not(:last-child)::after { + margin-inline: px2rem(4px); + content: "·"; + } + + // Metadata link + &__link { + color: var(--md-typeset-a-color); + + // Metadata link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + } + } +} + +// Draft +.md-draft { + display: inline-block; + padding-inline: px2em(8px, 14px); + font-weight: 700; + color: hsla(255, 100%, 100%); + background-color: $clr-red-a400; + border-radius: px2em(2px); +} diff --git a/src/templates/assets/stylesheets/main/components/_nav.scss b/src/templates/assets/stylesheets/main/components/_nav.scss new file mode 100644 index 00000000..673918af --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_nav.scss @@ -0,0 +1,761 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Navigation variables +:root { + --md-nav-icon--prev: svg-load("material/arrow-left.svg"); + --md-nav-icon--next: svg-load("material/chevron-right.svg"); + --md-toc-icon: svg-load("material/table-of-contents.svg"); +} + +// ---------------------------------------------------------------------------- + +// Navigation +.md-nav { + font-size: px2rem(14px); + line-height: 1.3; + + // Navigation title + &__title { + display: block; + padding: 0 px2rem(12px); + overflow: hidden; + font-weight: 700; + color: var(--md-default-fg-color--light); + text-overflow: ellipsis; + + // Navigaton button + .md-nav__button { + display: none; + + // Stretch images based on height, as it's the smaller dimension + img { + width: auto; + height: 100%; + } + + // Button with logo, pointing to `config.site_url` + &.md-logo { + + // Image or icon + :is(img, svg) { + display: block; + width: auto; + max-width: 100%; + height: px2rem(48px); + object-fit: contain; + fill: currentcolor; + } + } + } + } + + // Navigation list + &__list { + padding: 0; + margin: 0; + list-style: none; + } + + // Navigation link + &__link { + display: flex; + gap: px2rem(8px); + align-items: flex-start; + margin-top: 0.625em; + transition: color 125ms; + scroll-snap-align: start; + + // Navigation link that was passed + &--passed { + color: var(--md-default-fg-color--light); + } + + // Active link + .md-nav__item &--active { + + // Also enable color transitions on inline code blocks + &, + code { + color: var(--md-typeset-a-color); + } + } + + // Navigation link title + .md-ellipsis { + // Hack: Safari exhibits a bug where the text will sometimes disappear + // and the element will become unclickable. Setting `position: relative` + // seems to fix the issue - see https://bit.ly/3HljM1T + position: relative; + } + + // Always align navigation icons to the end + .md-icon:last-child { + margin-inline-start: auto; + } + + // Navigation link icon + svg { + flex-shrink: 0; + height: 1.3em; + fill: currentcolor; + } + + // Navigation link on focus/hover + &:is([href], [for]):is(:focus, :hover) { + color: var(--md-accent-fg-color); + cursor: pointer; + } + + // Show outline for keyboard devices + &.focus-visible { + outline-color: var(--md-accent-fg-color); + outline-offset: px2rem(4px); + } + + // Navigation link for table of contents + .md-nav--primary &[for="__toc"] { + display: none; + + // Table of contents icon + .md-icon::after { + display: block; + width: 100%; + height: 100%; + mask-image: var(--md-toc-icon); + background-color: currentcolor; + } + + // Hide table of contents + ~ .md-nav { + display: none; + } + } + } + + // Navigation container (for section index pages) + &__container > .md-nav__link { + margin-top: 0; + + // Stretch first child + &:first-child { + flex-grow: 1; + // Hack: if a very long word is used, it can push the arrow out of sight. + // Setting this property contains the text - see https://t.ly/E02vp + min-width: 0; + } + } + + // Navigation icon + &__icon { + flex-shrink: 0; + } + + // Repository information container + &__source { + display: none; + } + + // [tablet -]: Layered navigation + @include break-to-device(tablet) { + + // Primary and nested navigation + &--primary, + &--primary & { + position: absolute; + inset-inline: 0; + top: 0; + z-index: 1; + display: flex; + flex-direction: column; + height: 100%; + background-color: var(--md-default-bg-color); + } + + // Primary navigation + &--primary { + + // Navigation title and item + :is(.md-nav__title, .md-nav__item) { + font-size: px2rem(16px); + line-height: 1.5; + } + + // Navigation title + .md-nav__title { + position: relative; + height: px2rem(112px); + padding: px2rem(60px) px2rem(16px) px2rem(4px); + line-height: px2rem(48px); + color: var(--md-default-fg-color--light); + white-space: nowrap; + cursor: pointer; + background-color: var(--md-default-fg-color--lightest); + + // Navigation icon + .md-nav__icon { + position: absolute; + top: px2rem(8px); + inset-inline-start: px2rem(8px); + display: block; + width: px2rem(24px); + height: px2rem(24px); + margin: px2rem(4px); + + // Navigation icon in link to previous level + &::after { + display: block; + width: 100%; + height: 100%; + content: ""; + background-color: currentcolor; + mask-image: var(--md-nav-icon--prev); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + } + + // Navigation list + ~ .md-nav__list { + overflow-y: auto; + touch-action: pan-y; + background-color: var(--md-default-bg-color); + box-shadow: + 0 px2rem(1px) 0 var(--md-default-fg-color--lightest) inset; + scroll-snap-type: y mandatory; + + // Omit border on first child + > :first-child { + border-top: 0; + } + } + + // Top-level navigation title + &[for="__drawer"] { + font-weight: 700; + color: var(--md-primary-bg-color); + background-color: var(--md-primary-fg-color); + } + + // Button with logo, pointing to `config.site_url` + .md-logo { + position: absolute; + inset-inline: px2rem(4px); + top: px2rem(4px); + display: block; + padding: px2rem(8px); + margin: px2rem(4px); + } + } + + // Navigation list + .md-nav__list { + flex: 1; + } + + // Navigation item + .md-nav__item { + border-top: px2rem(1px) solid var(--md-default-fg-color--lightest); + + // Navigation link in active navigation + &--active > .md-nav__link { + color: var(--md-typeset-a-color); + + // Navigation link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + } + } + } + + // Navigation link + .md-nav__link { + padding: px2rem(12px) px2rem(16px); + margin-top: 0; + + // Navigation link icon + svg { + margin-top: 0.1em; + } + + // Adjust spacing on nested link + > .md-nav__link { + padding: 0; + } + + // Navigation icon + .md-nav__icon { + width: px2rem(24px); + height: px2rem(24px); + margin-inline-end: px2rem(-4px); + font-size: px2rem(24px); + + // Navigation icon in link to next level + &::after { + display: block; + width: 100%; + height: 100%; + content: ""; + background-color: currentcolor; + mask-image: var(--md-nav-icon--next); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + } + } + + // Flip icon vertically + .md-nav__icon { + + // Adjust for right-to-left languages + [dir="rtl"] &::after { + transform: scale(-1); + } + } + + // Table of contents contained in primary navigation + .md-nav--secondary { + + // Navigation on level 2-6 + .md-nav { + position: static; + background-color: transparent; + + // Navigation link on level 3 + .md-nav__link { + padding-inline-start: px2rem(28px); + } + + // Navigation link on level 4 + .md-nav .md-nav__link { + padding-inline-start: px2rem(40px); + } + + // Navigation link on level 5 + .md-nav .md-nav .md-nav__link { + padding-inline-start: px2rem(52px); + } + + // Navigation link on level 6 + .md-nav .md-nav .md-nav .md-nav__link { + padding-inline-start: px2rem(64px); + } + } + } + } + + // Table of contents + &--secondary { + background-color: transparent; + } + + // Hide nested navigation + &__toggle ~ & { + display: flex; + opacity: 0; + transition: + transform 250ms cubic-bezier(0.8, 0, 0.6, 1), + opacity 125ms 50ms; + transform: translateX(100%); + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translateX(-100%); + } + } + + // Show nested navigation when toggle is active + &__toggle:checked ~ & { + opacity: 1; + transition: + transform 250ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 125ms 125ms; + transform: translateX(0); + + // Navigation list + > .md-nav__list { + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + } + } + } + + // [tablet portrait -]: Layered navigation with table of contents + @include break-to-device(tablet portrait) { + + // Show link to table of contents + &--primary &__link[for="__toc"] { + display: flex; + + // Show table of contents icon + .md-icon::after { + content: ""; + } + + // Hide navigation link to current page + + .md-nav__link { + display: none; + } + + // Show table of contents + ~ .md-nav { + display: flex; + } + } + + // Repository information container + &__source { + display: block; + padding: 0 px2rem(4px); + color: var(--md-primary-bg-color); + background-color: var(--md-primary-fg-color--dark); + } + } + + // [tablet landscape]: Layered navigation with table of contents + @include break-at-device(tablet landscape) { + + // Show link to integrated table of contents + &--integrated &__link[for="__toc"] { + display: flex; + + // Show table of contents icon + .md-icon::after { + content: ""; + } + + // Hide navigation link to current page + + .md-nav__link { + display: none; + } + + // Show table of contents + ~ .md-nav { + display: flex; + } + } + } + + // [tablet landscape +]: Tree-like table of contents + @include break-from-device(tablet landscape) { + margin-bottom: px2rem(-8px); + + // Table of contents + &--secondary { + + // Navigation title + .md-nav__title { + position: sticky; + top: 0; + // Hack: because of the hack that we need to make .md-ellipsis work in + // Safari, we need to set `z-index` here as - see https://bit.ly/3s5M2jm + z-index: 1; + background: var(--md-default-bg-color); + box-shadow: 0 0 px2rem(8px) px2rem(8px) var(--md-default-bg-color); + + // Adjust snapping behavior + &[for="__toc"] { + scroll-snap-align: start; + } + + // Hide navigation icon + .md-nav__icon { + display: none; + } + } + + // Adjust spacing for navigation list - same reason as below + .md-nav__list { + padding-inline-start: px2rem(12px); + padding-bottom: px2rem(8px); + } + + // Adjust spacing for navigation link - before this change, we set spacing + // on the left and right of a navigation item, but this led to the problem + // of cropped focus outlines, because we must set `overflow: hidden` on + // the navigation list for smooth expand and collapse transitions. + .md-nav__item > .md-nav__link { + margin-inline-end: px2rem(8px); + } + } + } + + // [screen +]: Tree-like navigation + @include break-from-device(screen) { + margin-bottom: px2rem(-8px); + transition: max-height 250ms cubic-bezier(0.86, 0, 0.07, 1); + + // Primary navigation + &--primary { + + // Navigation title + .md-nav__title { + position: sticky; + top: 0; + // Hack: because of the hack that we need to make .md-ellipsis work in + // Safari, we need to set `z-index` here as - see https://bit.ly/3s5M2jm + z-index: 1; + background: var(--md-default-bg-color); + box-shadow: 0 0 px2rem(8px) px2rem(8px) var(--md-default-bg-color); + + // Adjust snapping behavior + &[for="__drawer"] { + scroll-snap-align: start; + } + + // Hide navigation icon + .md-nav__icon { + display: none; + } + } + + // Adjust spacing for navigation list - same reason as below + .md-nav__list { + padding-inline-start: px2rem(12px); + padding-bottom: px2rem(8px); + } + + // Adjust spacing for navigation link - before this change, we set spacing + // on the left and right of a navigation item, but this led to the problem + // of cropped focus outlines, because we must set `overflow: hidden` on + // the navigation list for smooth expand and collapse transitions. + .md-nav__item > .md-nav__link { + margin-inline-end: px2rem(8px); + } + } + + // Hide nested navigation + &__toggle ~ & { + display: grid; + grid-template-rows: 0fr; + visibility: collapse; + opacity: 0; + transition: + grid-template-rows 250ms cubic-bezier(0.86, 0, 0.07, 1), + opacity 250ms, + visibility 0ms 250ms; + + // Navigation list + > .md-nav__list { + overflow: hidden; + } + } + + // Show nested navigation when toggle is active or indeterminate + &__toggle:is(:checked, :indeterminate) ~ & { + grid-template-rows: 1fr; + visibility: visible; + opacity: 1; + transition: + grid-template-rows 250ms cubic-bezier(0.86, 0, 0.07, 1), + opacity 150ms 100ms, + visibility 0ms; + } + + // Hide navigation title in nested navigation + &__item--nested > & > &__title { + display: none; + } + + // Navigation section + &__item--section { + display: block; + margin: 1.25em 0; + + // Adjust spacing on last child + &:last-child { + margin-bottom: 0; + } + + // Show navigation link as title + > .md-nav__link { + font-weight: 700; + + // Make labels discernable from links + &[for] { + color: var(--md-default-fg-color--light); + } + + // Omit clicks if not a section index page + &:not(.md-nav__container) { + pointer-events: none; + } + + // Hide navigation icon + > [for], + .md-icon { + display: none; + } + } + + // Navigation + > .md-nav { + display: block; + margin-inline-start: px2rem(-12px); + visibility: visible; + opacity: 1; + + // Adjust spacing on next level item + > .md-nav__list > .md-nav__item { + padding: 0; + } + } + } + + // Navigation icon + &__icon { + width: px2rem(18px); + height: px2rem(18px); + border-radius: 100%; + transition: background-color 250ms; + + // Navigation icon on hover + &:hover { + background-color: var(--md-accent-fg-color--transparent); + } + + // Navigation icon content + &::after { + display: inline-block; + width: 100%; + height: 100%; + vertical-align: px2rem(-2px); + content: ""; + background-color: currentcolor; + border-radius: 100%; + transition: transform 250ms; + mask-image: var(--md-nav-icon--next); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: rotate(180deg); + } + + // Navigation icon - rotate icon when toggle is active or indeterminate + .md-nav__item--nested .md-nav__toggle:checked ~ .md-nav__link &, + .md-nav__item--nested .md-nav__toggle:indeterminate ~ .md-nav__link & { + transform: rotate(90deg); + } + } + } + + // Modifier for when navigation tabs are rendered + &--lifted { + + // Hide site title + > .md-nav__title { + display: none; + } + + // Hide level 0 navigation items + > .md-nav__list > .md-nav__item { + display: none; + + // Active parent navigation item + &--active { + display: block; + + // Show navigation link as title + > .md-nav__link { + position: sticky; + top: 0; + z-index: 1; + margin-top: 0; + background: var(--md-default-bg-color); + box-shadow: 0 0 px2rem(8px) px2rem(8px) var(--md-default-bg-color); + + // Omit clicks if not a section index page + &:not(.md-nav__container) { + pointer-events: none; + } + } + + // Adjust spacing for navigation section + &.md-nav__item--section { + margin: 0; + } + } + + // Adjust spacing for nested navigation + > .md-nav { + margin-inline-start: px2rem(-12px); + } + + // Make labels discernable from links + > [for] { + color: var(--md-default-fg-color--light); + } + } + + // Hack: Always show active navigation tab on breakpoint screen, despite + // of checkbox being checked or not - see https://t.ly/Qc311 + .md-nav[data-md-level="1"] { + grid-template-rows: 1fr; + visibility: visible; + opacity: 1; + } + } + + // Modifier for when table of contents is rendered in primary navigation + &--integrated > .md-nav__list > .md-nav__item--active { + + // Add spacing to container for non-nested navigation items + &:not(.md-nav__item--nested) { + padding: 0 px2rem(12px); + + // Remove padding as it's given by container + > .md-nav__link { + padding: 0; + } + } + + // Show integrated table of contents + .md-nav--secondary { + display: block; + margin-bottom: 1.25em; + visibility: visible; + border-inline-start: px2rem(1px) solid var(--md-primary-fg-color); + opacity: 1; + + // Navigation list + > .md-nav__list { + padding-bottom: 0; + overflow: visible; + } + + // Hide table of contents title + > .md-nav__title { + display: none; + } + } + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_pagination.scss b/src/templates/assets/stylesheets/main/components/_pagination.scss new file mode 100644 index 00000000..a010bf43 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_pagination.scss @@ -0,0 +1,85 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Pagination +.md-pagination { + display: flex; + gap: px2rem(8px); + align-items: center; + justify-content: center; + font-size: px2rem(16px); + font-weight: 700; + + // Pagination item + > * { + display: flex; + align-items: center; + justify-content: center; + min-width: px2rem(36px); + height: px2rem(36px); + text-align: center; + border-radius: px2rem(4px); + } + + // Active pagination item + &__current { + color: var(--md-default-fg-color--light); + background-color: var(--md-default-fg-color--lightest); + } + + // Pagination link + &__link { + transition: + color 125ms, + background-color 125ms; + + // Pagination link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + background-color: var(--md-accent-fg-color--transparent); + + // Pagination icon + svg { + color: var(--md-accent-fg-color); + } + } + + // Show outline for keyboard devices + &.focus-visible { + outline-color: var(--md-accent-fg-color); + outline-offset: px2rem(4px); + } + + // Pagination icon + svg { + display: block; + width: px2rem(24px); + max-height: 100%; + color: var(--md-default-fg-color--lighter); + fill: currentcolor; + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_post.scss b/src/templates/assets/stylesheets/main/components/_post.scss new file mode 100644 index 00000000..cf6ce019 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_post.scss @@ -0,0 +1,196 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Post +.md-post { + + // Post backlink + &__back { + padding-bottom: px2rem(24px); + margin-bottom: px2rem(24px); + border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest); + + // [tablet -]: Hide post backlink + @include break-to-device(tablet) { + display: none; + } + + // Adjust for right-to-left languages + [dir="rtl"] & { + + // Flip icon vertically + svg { + transform: scaleX(-1); + } + } + } + + // Post authors + &__authors { + display: flex; + flex-direction: column; + gap: px2rem(12px); + margin: 0 px2rem(12px) px2rem(24px); + } + + // Post metadata + .md-post__meta { + + // Navigation link + a { + transition: color 125ms; + + // Navigation link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + } + } + } + + // Post navigation title @todo - generalize + &__title { + font-weight: 700; + color: var(--md-default-fg-color--light); + } + + // Post excerpt + &--excerpt { + margin-bottom: px2rem(64px); + + // Post excerpt header + .md-post__header { + display: flex; + gap: px2rem(12px); + align-items: center; + min-height: px2rem(32px); + } + + // Post excerpt authors + .md-post__authors { + display: inline-flex; + flex-direction: row; + gap: px2rem(4px); + align-items: center; + min-height: px2rem(48px); + margin: 0; + } + + // Post excerpt metadata + .md-post__meta .md-meta__list { + margin-inline-end: px2rem(8px); + } + + // Post excerpt content + .md-post__content > :first-child { + --md-scroll-margin: #{px2rem(120px)}; + + margin-top: 0; + } + } + + // Add margin to table of contents + > .md-nav--secondary { + margin: 1em 0; + } +} + +// ---------------------------------------------------------------------------- + +// Post author profile +.md-profile { + display: flex; + gap: px2rem(12px); + align-items: center; + width: 100%; + font-size: px2rem(14px); + line-height: 1.4; + + // Post author description + &__description { + flex-grow: 1; + } +} + +// ---------------------------------------------------------------------------- + +// Content area for post +.md-content--post { + display: flex; + + // [tablet -]: Switch to inverted column layout + @include break-to-device(tablet) { + flex-flow: column-reverse; + } + + // Content wrapper + > .md-content__inner { + min-width: 0; + + // [screen +]: Adjust spacing between content area and sidebars + @include break-from-device(screen) { + margin-inline-start: px2rem(24px); + } + } +} + +// Sidebar for post +.md-sidebar.md-sidebar--post { + + // [tablet -]: Adjust spacing + @include break-to-device(tablet) { + position: initial; + width: 100%; + padding: 0; + + .md-sidebar__inner { + padding: 0; + } + + .md-post__meta { + margin-inline: px2rem(12px); + } + + .md-nav__item { + display: inline; + border: none; + } + + .md-nav__list { + display: inline-flex; + flex-wrap: wrap; + gap: px2rem(12px); + padding-block: px2rem(12px); + } + + .md-nav__link { + padding: 0; + } + + .md-nav { + position: initial; + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_progress.scss b/src/templates/assets/stylesheets/main/components/_progress.scss new file mode 100644 index 00000000..7386ae33 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_progress.scss @@ -0,0 +1,53 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Progress variables +:root { + --md-progress-value: 0; + --md-progress-delay: 400ms; +} + +// ---------------------------------------------------------------------------- + +// Progress indicator +.md-progress { + position: fixed; + top: 0; + z-index: 4; + width: 100%; + height: px2rem(1.5px); + background: var(--md-primary-bg-color); + opacity: + min( + clamp(0, var(--md-progress-value), 1), + clamp(0, 100 - var(--md-progress-value), 1) + ); + transition: + transform 500ms cubic-bezier(0.19, 1, 0.22, 1), + opacity 250ms var(--md-progress-delay); + transform: scaleX(calc(var(--md-progress-value) * 1%)); + transform-origin: left; +} diff --git a/src/templates/assets/stylesheets/main/components/_search.scss b/src/templates/assets/stylesheets/main/components/_search.scss new file mode 100644 index 00000000..e0f36b0c --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_search.scss @@ -0,0 +1,707 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Search variables +:root { + --md-search-result-icon: svg-load("material/file-search-outline.svg"); +} + +// ---------------------------------------------------------------------------- + +// Search +.md-search { + position: relative; + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + padding: px2rem(4px) 0; + } + + // [no-js]: Hide search + .no-js & { + display: none; + } + + // Search overlay + &__overlay { + z-index: 1; + opacity: 0; + + // [tablet portrait -]: Search modal + @include break-to-device(tablet portrait) { + position: absolute; + top: px2rem(-20px); + width: px2rem(40px); + height: px2rem(40px); + overflow: hidden; + pointer-events: none; + background-color: var(--md-default-bg-color); + border-radius: px2rem(20px); + transition: + transform 300ms 100ms, + opacity 200ms 200ms; + transform-origin: center; + inset-inline-start: px2rem(-44px); + + // Show overlay when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + opacity: 1; + transition: + transform 400ms, + opacity 100ms; + } + } + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + position: fixed; + top: 0; + width: 0; + height: 0; + cursor: pointer; + background-color: hsla(0, 0%, 0%, 0.54); + transition: + width 0ms 250ms, + height 0ms 250ms, + opacity 250ms; + inset-inline-start: 0; + + // Show overlay when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + width: 100%; + // Hack: when the header is translated upon scrolling, a new layer is + // induced, which means that the height will now refer to the height of + // the header, albeit positioning is fixed. This should be mitigated + // in all cases when setting the height to 2x the viewport. + height: 200vh; + opacity: 1; + transition: + width 0ms, + height 0ms, + opacity 250ms; + } + } + + // Adjust appearance when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + + // [mobile portrait -]: Scale up 45 times + @include break-to-device(mobile portrait) { + transform: scale(45); + } + + // [mobile landscape]: Scale up 60 times + @include break-at-device(mobile landscape) { + transform: scale(60); + } + + // [tablet portrait]: Scale up 75 times + @include break-at-device(tablet portrait) { + transform: scale(75); + } + } + } + + // Search wrapper + &__inner { + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + + // [tablet portrait -]: Search modal + @include break-to-device(tablet portrait) { + position: fixed; + top: 0; + z-index: 2; + width: 0; + height: 0; + overflow: hidden; + opacity: 0; + transition: + width 0ms 300ms, + height 0ms 300ms, + transform 150ms 150ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 150ms 150ms; + transform: translateX(5%); + inset-inline-start: 0; + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translateX(-5%); + } + + // Adjust appearance when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + width: 100%; + height: 100%; + opacity: 1; + transition: + width 0ms 0ms, + height 0ms 0ms, + transform 150ms 150ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 150ms 150ms; + transform: translateX(0); + } + } + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + position: relative; + float: inline-end; + width: px2rem(234px); + padding: px2rem(2px) 0; + transition: width 250ms cubic-bezier(0.1, 0.7, 0.1, 1); + } + + // Adjust appearance when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + + // [tablet landscape]: Omit overlaying header title + @include break-at-device(tablet landscape) { + width: px2rem(468px); + } + + // [screen +]: Match width of content area + @include break-from-device(screen) { + width: px2rem(688px); + } + } + } + + // Search form + &__form { + position: relative; + z-index: 2; + height: px2rem(48px); + background-color: var(--md-default-bg-color); + box-shadow: 0 0 px2rem(12px) transparent; + transition: + color 250ms, + background-color 250ms; + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + height: px2rem(36px); + background-color: hsla(0, 0%, 0%, 0.26); + border-radius: px2rem(2px); + + // Search form on hover + &:hover { + background-color: hsla(0, 0%, 100%, 0.12); + } + } + + // Adjust appearance when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + color: var(--md-default-fg-color); + background-color: var(--md-default-bg-color); + border-radius: px2rem(2px) px2rem(2px) 0 0; + box-shadow: 0 0 px2rem(12px) hsla(0, 0%, 0%, 0.07); + } + } + + // Search input + &__input { + position: relative; + z-index: 2; + width: 100%; + height: 100%; + padding-inline: px2rem(72px) px2rem(44px); + font-size: px2rem(18px); + text-overflow: ellipsis; + background: transparent; + + // Search placeholder + &::placeholder { + transition: color 250ms; + } + + // Search icon and placeholder + ~ .md-search__icon, + &::placeholder { + color: var(--md-default-fg-color--light); + } + + // Remove the "x" rendered by Internet Explorer + &::-ms-clear { + display: none; + } + + // [tablet portrait -]: Search modal + @include break-to-device(tablet portrait) { + width: 100%; + height: px2rem(48px); + font-size: px2rem(18px); + } + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + padding-inline-start: px2rem(44px); + font-size: px2rem(16px); + color: inherit; + + // Search placeholder + &::placeholder { + color: var(--md-primary-bg-color--light); + } + + // Search icon + + .md-search__icon { + color: var(--md-primary-bg-color); + } + + // Adjust appearance when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + text-overflow: clip; + + // Search icon and placeholder + + .md-search__icon { + color: var(--md-default-fg-color--light); + } + + // Search placeholder + &::placeholder { + color: transparent; + } + } + } + } + + // Search icon + &__icon { + display: inline-block; + width: px2rem(24px); + height: px2rem(24px); + cursor: pointer; + transition: + color 250ms, + opacity 250ms; + + // Search icon on hover + &:hover { + opacity: 0.7; + } + + // Search focus button + &[for="__search"] { + position: absolute; + top: px2rem(6px); + inset-inline-start: px2rem(10px); + z-index: 2; + + // Adjust for right-to-left languages + [dir="rtl"] & svg { + transform: scaleX(-1); + } + + // [tablet portrait -]: Search modal + @include break-to-device(tablet portrait) { + top: px2rem(12px); + inset-inline-start: px2rem(16px); + + // Hide the magnifying glass + svg:first-child { + display: none; + } + } + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + pointer-events: none; + + // Hide the back arrow + svg:last-child { + display: none; + } + } + } + } + + // Search options + &__options { + position: absolute; + top: px2rem(6px); + inset-inline-end: px2rem(10px); + z-index: 2; + pointer-events: none; + + // [tablet portrait -]: Search modal + @include break-to-device(tablet portrait) { + top: px2rem(12px); + inset-inline-end: px2rem(16px); + } + + // Search option buttons + > .md-icon { + margin-inline-start: px2rem(4px); + color: var(--md-default-fg-color--light); + opacity: 0; + transition: + transform 150ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 150ms; + transform: scale(0.75); + + // Hide outline for pointer devices + &:not(.focus-visible) { + outline: none; + -webkit-tap-highlight-color: transparent; + } + + // Show buttons when search is active and input non-empty + [data-md-toggle="search"]:checked ~ .md-header // stylelint-disable-line + .md-search__input:valid ~ & { + pointer-events: initial; + opacity: 1; + transform: scale(1); + + // Search focus icon + &:hover { + opacity: 0.7; + } + } + } + } + + // Search suggestions + &__suggest { + position: absolute; + top: 0; + display: flex; + align-items: center; + width: 100%; + height: 100%; + padding-inline: px2rem(72px) px2rem(44px); + font-size: px2rem(18px); + color: var(--md-default-fg-color--lighter); + white-space: nowrap; + opacity: 0; + transition: opacity 50ms; + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + padding-inline-start: px2rem(44px); + font-size: px2rem(16px); + } + + // Show suggestions when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + opacity: 1; + transition: opacity 300ms 100ms; + } + } + + // Search output + &__output { + position: absolute; + z-index: 1; + width: 100%; + overflow: hidden; + border-end-start-radius: px2rem(2px); + border-end-end-radius: px2rem(2px); + + // [tablet portrait -]: Search modal + @include break-to-device(tablet portrait) { + top: px2rem(48px); + bottom: 0; + } + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + top: px2rem(38px); + opacity: 0; + transition: opacity 400ms; + + // Show output when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + box-shadow: var(--md-shadow-z3); + opacity: 1; + } + } + } + + // Search scroll wrapper + &__scrollwrap { + height: 100%; + overflow-y: auto; + // Hack: Chrome 88+ has weird overscroll behavior. Overall, scroll snapping + // seems to be something that is not ready for prime time on some browsers. + // scroll-snap-type: y mandatory; + touch-action: pan-y; + background-color: var(--md-default-bg-color); + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + + // Mitigiate excessive repaints on non-retina devices + @media (max-resolution: 1dppx) { + transform: translateZ(0); + } + + // [tablet landscape]: Set fixed width to omit unnecessary reflow + @include break-at-device(tablet landscape) { + width: px2rem(468px); + } + + // [screen +]: Set fixed width to omit unnecessary reflow + @include break-from-device(screen) { + width: px2rem(688px); + } + + // [tablet landscape +]: Limit height to viewport + @include break-from-device(tablet landscape) { + max-height: 0; + scrollbar-width: thin; + scrollbar-color: var(--md-default-fg-color--lighter) transparent; + + // Show scroll wrapper when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + max-height: 75vh; + } + + // Search scroll wrapper on hover + &:hover { + scrollbar-color: var(--md-accent-fg-color) transparent; + } + + // Webkit scrollbar + &::-webkit-scrollbar { + width: px2rem(4px); + height: px2rem(4px); + } + + // Webkit scrollbar thumb + &::-webkit-scrollbar-thumb { + background-color: var(--md-default-fg-color--lighter); + + // Webkit scrollbar thumb on hover + &:hover { + background-color: var(--md-accent-fg-color); + } + } + } + } +} + +// Search result +.md-search-result { + color: var(--md-default-fg-color); + word-break: break-word; + + // Search result metadata + &__meta { + padding: 0 px2rem(16px); + font-size: px2rem(12.8px); + line-height: px2rem(36px); + color: var(--md-default-fg-color--light); + background-color: var(--md-default-fg-color--lightest); + scroll-snap-align: start; + + // [tablet landscape +]: Adjust spacing + @include break-from-device(tablet landscape) { + padding-inline-start: px2rem(44px); + } + } + + // Search result list + &__list { + padding: 0; + margin: 0; + list-style: none; + // Hack: omit accidental text selection on fast toggle of more button + user-select: none; + } + + // Search result item + &__item { + box-shadow: 0 px2rem(-1px) var(--md-default-fg-color--lightest); + + // Omit border on first child + &:first-child { + box-shadow: none; + } + } + + // Search result link + &__link { + display: block; + outline: none; + transition: background-color 250ms; + scroll-snap-align: start; + + // Search result link on focus/hover + &:is(:focus, :hover) { + background-color: var(--md-accent-fg-color--transparent); + } + + // Adjust spacing on last child of last link + &:last-child p:last-child { + margin-bottom: px2rem(12px); + } + } + + // Search result more container + &__more > summary { + position: sticky; + top: 0; + z-index: 1; + display: block; + cursor: pointer; + outline: none; + scroll-snap-align: start; + + // Hide native details marker + &::marker { + display: none; + } + + // Hide native details marker - legacy, must be split into a seprate rule, + // so older browsers don't consider the selector list as invalid + &::-webkit-details-marker { + display: none; + } + + // Search result more button + > div { + padding: px2em(12px) px2rem(16px); + font-size: px2rem(12.8px); + color: var(--md-typeset-a-color); + transition: + color 250ms, + background-color 250ms; + + // [tablet landscape +]: Adjust spacing + @include break-from-device(tablet landscape) { + padding-inline-start: px2rem(44px); + } + } + + // Search result more link on focus/hover + &:is(:focus, :hover) > div { + color: var(--md-accent-fg-color); + background-color: var(--md-accent-fg-color--transparent); + } + } + + // Adjust background for more container in open state + &__more[open] > summary { + background-color: var(--md-default-bg-color); + // box-shadow: 0 px2rem(-1px) hsla(0, 0%, 0%, 0.07) inset; + } + + // Search result article + &__article { + position: relative; + padding: 0 px2rem(16px); + overflow: hidden; + + // [tablet landscape +]: Adjust spacing + @include break-from-device(tablet landscape) { + padding-inline-start: px2rem(44px); + } + } + + // Search result icon + &__icon { + position: absolute; + inset-inline-start: 0; + width: px2rem(24px); + height: px2rem(24px); + margin: px2rem(10px); + color: var(--md-default-fg-color--light); + + // [tablet portrait -]: Hide icon + @include break-to-device(tablet portrait) { + display: none; + } + + // Search result icon content + &::after { + display: inline-block; + width: 100%; + height: 100%; + content: ""; + background-color: currentcolor; + mask-image: var(--md-search-result-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: scaleX(-1); + } + } + } + + // Typesetted content + .md-typeset { + font-size: px2rem(12.8px); + line-height: 1.6; + color: var(--md-default-fg-color--light); + + // Search result article title + h1 { + margin: px2rem(11px) 0; + font-size: px2rem(16px); + font-weight: 400; + line-height: 1.4; + color: var(--md-default-fg-color); + + // Search term highlighting + mark { + text-decoration: none; + } + } + + // Search result section title + h2 { + margin: 0.5em 0; + font-size: px2rem(12.8px); + font-weight: 700; + line-height: 1.6; + color: var(--md-default-fg-color); + + // Search term highlighting + mark { + text-decoration: none; + } + } + } + + // Search result terms + &__terms { + display: block; + margin: 0.5em 0; + font-size: px2rem(12.8px); + font-style: italic; + color: var(--md-default-fg-color); + } + + // Search term highlighting + mark { + color: var(--md-accent-fg-color); + text-decoration: underline; + background-color: transparent; + } +} diff --git a/src/templates/assets/stylesheets/main/components/_select.scss b/src/templates/assets/stylesheets/main/components/_select.scss new file mode 100644 index 00000000..ed597a39 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_select.scss @@ -0,0 +1,115 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Selection +.md-select { + position: relative; + z-index: 1; + + // Selection tooltip + &__inner { + position: absolute; + top: calc(100% - #{px2rem(4px)}); + left: 50%; + max-height: 0; + margin-top: px2rem(4px); + color: var(--md-default-fg-color); + background-color: var(--md-default-bg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z2); + opacity: 0; + transition: + transform 250ms 375ms, + opacity 250ms 250ms, + max-height 0ms 500ms; + transform: translate3d(-50%, px2rem(6px), 0); + + // Selection bubble on parent focus/hover + .md-select:is(:focus-within, :hover) & { + max-height: px2rem(200px); + opacity: 1; + transition: + transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 250ms, + max-height 0ms; + transform: translate3d(-50%, 0, 0); + } + + // Selection bubble handle + &::after { + position: absolute; + top: 0; + left: 50%; + width: 0; + height: 0; + margin-top: px2rem(-4px); + margin-left: px2rem(-4px); + content: ""; + border: px2rem(4px) solid transparent; + border-top: 0; + border-bottom-color: var(--md-default-bg-color); + } + } + + // Selection list + &__list { + max-height: inherit; + padding: 0; + margin: 0; + overflow: auto; + font-size: px2rem(16px); + list-style-type: none; + border-radius: px2rem(2px); + } + + // Selection item + &__item { + line-height: px2rem(36px); + } + + // Selection link + &__link { + display: block; + width: 100%; + padding-inline: px2rem(12px) px2rem(24px); + cursor: pointer; + outline: none; + transition: + background-color 250ms, + color 250ms; + scroll-snap-align: start; + + // Link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + } + + // Link on focus + &:focus { + background-color: var(--md-default-fg-color--lightest); + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_sidebar.scss b/src/templates/assets/stylesheets/main/components/_sidebar.scss new file mode 100644 index 00000000..8a320c04 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_sidebar.scss @@ -0,0 +1,209 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Sidebar +.md-sidebar { + position: sticky; + top: px2rem(48px); + flex-shrink: 0; + align-self: flex-start; + width: px2rem(242px); + padding: px2rem(24px) 0; + + // [print]: Hide sidebar + @media print { + display: none; + } + + // Primary sidebar with navigation + &--primary { + + // [tablet -]: Show navigation as drawer + @include break-to-device(tablet) { + position: fixed; + top: 0; + z-index: 5; + display: block; + width: px2rem(242px); + height: 100%; + background-color: var(--md-default-bg-color); + transition: + transform 250ms cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 250ms; + transform: translateX(0); + inset-inline-start: px2rem(-242px); + + // Show sidebar when drawer is active + [data-md-toggle="drawer"]:checked ~ .md-container & { + box-shadow: var(--md-shadow-z3); + transform: translateX(px2rem(242px)); + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translateX(px2rem(-242px)); + } + } + + // Stretch scroll wrapper for primary sidebar + .md-sidebar__scrollwrap { + position: absolute; + inset: 0; + margin: 0; + scroll-snap-type: none; + overflow: hidden; + } + } + } + + // [screen +]: Show navigation as sidebar + @include break-from-device(screen) { + height: 0; + + // [no-js]: Switch to native sticky behavior + .no-js & { + height: auto; + } + + // Adjust spacing for sticky navigation tabs + .md-header--lifted ~ .md-container & { + top: px2rem(96px); + } + } + + // Secondary sidebar with table of contents + &--secondary { + display: none; + order: 2; + + // [tablet landscape +]: Show table of contents as sidebar + @include break-from-device(tablet landscape) { + height: 0; + + // [no-js]: Switch to native sticky behavior + .no-js & { + height: auto; + } + + // Sidebar is visible + &:not([hidden]) { + display: block; + } + + // Ensure smooth scrolling on iOS + .md-sidebar__scrollwrap { + touch-action: pan-y; + } + } + } + + // Sidebar scroll wrapper + &__scrollwrap { + margin: 0 px2rem(4px); + overflow-y: auto; + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + // Hack: Chrome 81+ exhibits a strange bug, where it scrolls the container + // to the bottom if `scroll-snap-type` is set on the initial render. For + // this reason, we disable scroll snapping until this is resolved (#1667). + // scroll-snap-type: y mandatory; + scrollbar-width: thin; + scrollbar-gutter: stable; + scrollbar-color: var(--md-default-fg-color--lighter) transparent; + + // Webkit scrollbar + &::-webkit-scrollbar { + width: px2rem(4px); + height: px2rem(4px); + } + + // Sidebar scroll wrapper on focus/hover + &:is(:focus-within, :hover) { + scrollbar-color: var(--md-accent-fg-color) transparent; + + // Webkit scrollbar thumb + &::-webkit-scrollbar-thumb { + background-color: var(--md-default-fg-color--lighter); + + // Webkit scrollbar thumb on hover + &:hover { + background-color: var(--md-accent-fg-color); + } + } + } + } + + // Hack: the scrollbar is only visible when the sidebar's contents overflow, + // which is nice, but leads to the problem where the chevrons of expandable + // sections will jump by `4px` when the sidebar is shown. We wanted to fix + // this problem for so long, but haven't found a clean way of doing it. + // Until now. The following declaration is only applied to Webkit browsers + // (e.g. Chrome and Safari), which support styling of scrollbars. The trick + // is to add conditional padding on the side of the scrollbar only if the + // sidebar's content doesn't overflow. This hack is inspired and adapted + // from Ayke van Laëthem's year old trick – see https://bit.ly/3Sb1qql + @supports selector(::-webkit-scrollbar) { + + // Sidebar scroll wrapper + &__scrollwrap { + scrollbar-gutter: auto; + } + + // Sidebar wrapper + &__inner { + padding-inline-end: calc(100% - #{px2rem(230px)}); + } + } +} + +// [tablet -]: Show overlay on active drawer +@include break-to-device(tablet) { + + // Drawer overlay + .md-overlay { + position: fixed; + top: 0; + z-index: 5; + width: 0; + height: 0; + background-color: hsla(0, 0%, 0%, 0.54); + opacity: 0; + transition: + width 0ms 250ms, + height 0ms 250ms, + opacity 250ms; + + // Show overlay when drawer is active + [data-md-toggle="drawer"]:checked ~ & { + width: 100%; + height: 100%; + opacity: 1; + transition: + width 0ms, + height 0ms, + opacity 250ms; + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_source.scss b/src/templates/assets/stylesheets/main/components/_source.scss new file mode 100644 index 00000000..a2b72009 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_source.scss @@ -0,0 +1,182 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Keyframes +// ---------------------------------------------------------------------------- + +// Show repository facts +@keyframes facts { + 0% { + height: 0; + } + + 100% { + height: px2rem(13px); + } +} + +// Show repository fact +@keyframes fact { + 0% { + opacity: 0; + transform: translateY(100%); + } + + 50% { + opacity: 0; + } + + 100% { + opacity: 1; + transform: translateY(0%); + } +} + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Repository information variables +:root { + --md-source-forks-icon: svg-load("octicons/repo-forked-16.svg"); + --md-source-repositories-icon: svg-load("octicons/repo-16.svg"); + --md-source-stars-icon: svg-load("octicons/star-16.svg"); + --md-source-version-icon: svg-load("octicons/tag-16.svg"); +} + +// ---------------------------------------------------------------------------- + +// Repository information +.md-source { + display: block; + font-size: px2rem(13px); + line-height: 1.2; + white-space: nowrap; + outline-color: var(--md-accent-fg-color); + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + transition: opacity 250ms; + + // Repository information on hover + &:hover { + opacity: 0.7; + } + + // Repository icon + &__icon { + display: inline-block; + width: px2rem(40px); + height: px2rem(48px); + vertical-align: middle; + + // Align with margin only (as opposed to normal button alignment) + svg { + margin-inline-start: px2rem(12px); + margin-top: px2rem(12px); + } + + // Adjust spacing if icon is present + + .md-source__repository { + padding-inline-start: px2rem(40px); + margin-inline-start: px2rem(-40px); + } + } + + // Repository name + &__repository { + display: inline-block; + max-width: calc(100% - #{px2rem(24px)}); + margin-inline-start: px2rem(12px); + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + } + + // Repository facts + &__facts { + display: flex; + gap: px2rem(8px); + width: 100%; + padding: 0; + margin: px2rem(2px) 0 0; + overflow: hidden; + font-size: px2rem(11px); + list-style-type: none; + opacity: 0.75; + + // Show after the data was loaded + .md-source__repository--active & { + animation: facts 250ms ease-in; + } + } + + // Repository fact + &__fact { + overflow: hidden; + text-overflow: ellipsis; + + // Show after the data was loaded + .md-source__repository--active & { + animation: fact 400ms ease-out; + } + + // Repository fact icon + &::before { + display: inline-block; + width: px2rem(12px); + height: px2rem(12px); + margin-inline-end: px2rem(2px); + vertical-align: text-top; + content: ""; + background-color: currentcolor; + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + + // Adjust spacing for 2nd+ fact + &:nth-child(1n+2) { + flex-shrink: 0; + } + + // Repository fact: version + &--version::before { + mask-image: var(--md-source-version-icon); + } + + // Repository fact: stars + &--stars::before { + mask-image: var(--md-source-stars-icon); + } + + // Repository fact: forks + &--forks::before { + mask-image: var(--md-source-forks-icon); + } + + // Repository fact: repositories + &--repositories::before { + mask-image: var(--md-source-repositories-icon); + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_status.scss b/src/templates/assets/stylesheets/main/components/_status.scss new file mode 100644 index 00000000..9e096021 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_status.scss @@ -0,0 +1,73 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Status variables +:root { + --md-status: svg-load("material/information-outline.svg"); + --md-status--new: svg-load("material/alert-decagram.svg"); + --md-status--deprecated: svg-load("material/trash-can.svg"); + --md-status--encrypted: svg-load("material/shield-lock.svg"); +} + +// ---------------------------------------------------------------------------- + +// Status +.md-status { + + // Status icon + &::after { + display: inline-block; + width: px2em(18px); + height: px2em(18px); + vertical-align: text-bottom; + content: ""; + background-color: var(--md-default-fg-color--light); + mask-image: var(--md-status); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + + // Status icon on hover + &:hover::after { + background-color: currentcolor; + } + + // Status: new + &--new::after { + mask-image: var(--md-status--new); + } + + // Status: deprecated + &--deprecated::after { + mask-image: var(--md-status--deprecated); + } + + // Status: encrypted + &--encrypted::after { + mask-image: var(--md-status--encrypted); + } +} diff --git a/src/templates/assets/stylesheets/main/components/_tabs.scss b/src/templates/assets/stylesheets/main/components/_tabs.scss new file mode 100644 index 00000000..0da3384b --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_tabs.scss @@ -0,0 +1,133 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Navigation tabs +.md-tabs { + // Must be higher than the z-index of the back-to-top button, or the button + // will overlay the navigation tabs bar when scrolling up fast. + z-index: 3; + display: block; + width: 100%; + overflow: auto; + line-height: 1.3; + color: var(--md-primary-bg-color); + background-color: var(--md-primary-fg-color); + + // [print]: Hide tabs + @media print { + display: none; + } + + // [tablet -]: Hide tabs + @include break-to-device(tablet) { + display: none; + } + + // Navigation tabs are hidden + &[hidden] { + pointer-events: none; + } + + // Navigation tabs list + &__list { + display: flex; + padding: 0; + margin: 0; + margin-inline-start: px2rem(4px); + overflow: auto; + white-space: nowrap; + list-style: none; + contain: content; + // Hack: don't show scrollbar when navigation tabs overflow, which should + // only happen in rare occasions, as adding too many top level sections is + // discouraged, since hiding content on horitontal axis doesn't lead to a + // good user experience. It's just harder to discover. + scrollbar-width: none; + + // Hack: see above + &::-webkit-scrollbar { + display: none; + } + } + + // Navigation tabs item + &__item { + height: px2rem(48px); + padding-inline: px2rem(12px); + + // Navigation tabs link in active navigation + &--active .md-tabs__link { + color: inherit; + opacity: 1; + } + } + + // Navigation tabs link - could be defined as block elements and aligned via + // line height, but this would imply more repaints when scrolling + &__link { + display: flex; + margin-top: px2rem(16px); + font-size: px2rem(14px); + outline-color: var(--md-accent-fg-color); + outline-offset: px2rem(4px); + // Hack: save a repaint when tabs are appearing on scrolling up + backface-visibility: hidden; + opacity: 0.7; + transition: + transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 250ms; + + // Navigation tabs link on focus/hover + &:is(:focus, :hover) { + color: inherit; + opacity: 1; + } + + // Navigation tabs link icon + svg { + height: 1.3em; + margin-inline-end: px2rem(8px); + fill: currentcolor; + } + + // Delay transitions by a small amount + @for $i from 2 through 16 { + .md-tabs__item:nth-child(#{$i}) & { + transition-delay: 20ms * ($i - 1); + } + } + + // Hide tabs upon scrolling - disable transition to minimizes repaints + // while scrolling down, while scrolling up seems to be okay + .md-tabs[hidden] & { + opacity: 0; + transition: + transform 0ms 100ms, + opacity 100ms; + transform: translateY(50%); + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_tag.scss b/src/templates/assets/stylesheets/main/components/_tag.scss new file mode 100644 index 00000000..9f31829d --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_tag.scss @@ -0,0 +1,105 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Tag variables +:root { + --md-tag-icon: svg-load("material/pound.svg"); +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Tag list + .md-tags { + display: inline-flex; + flex-wrap: wrap; + gap: px2em(8px); + margin-top: px2em(-2px); + margin-bottom: px2em(12px); + } + + // Tag + .md-tag { + display: inline-flex; + gap: px2em(8px); + align-items: center; + padding: px2em(4px, 12.8px) px2em(10px, 12.8px); + font-size: px2rem(12.8px); // Fallback + font-size: min(px2em(12.8px), px2rem(12.8px)); + font-weight: 700; + line-height: 1.6; + letter-spacing: initial; + background: var(--md-default-fg-color--lightest); + border-radius: px2rem(48px); + + // Linked tag + &[href] { + color: inherit; + outline: none; + -webkit-tap-highlight-color: transparent; + transition: + color 125ms, + background-color 125ms; + + // Linked tag on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-bg-color); + background-color: var(--md-accent-fg-color); + } + } + + // Tag inside headline + [id] > & { + vertical-align: text-top; + } + } + + // Tag icon + .md-tag-icon { + + // Tag icon content + &::before { + display: inline-block; + width: 1.2em; + height: 1.2em; + vertical-align: text-bottom; + content: ""; + background-color: var(--md-default-fg-color--lighter); + transition: background-color 125ms; + mask-image: var(--md-tag-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + + // Linked tag on focus/hover + &[href]:is(:focus, :hover)::before { + background-color: var(--md-accent-bg-color); + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_tooltip.scss b/src/templates/assets/stylesheets/main/components/_tooltip.scss new file mode 100644 index 00000000..421e5858 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_tooltip.scss @@ -0,0 +1,292 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Keyframes +// ---------------------------------------------------------------------------- + +// Continuous pulse animation +@keyframes pulse { + 0% { + transform: scale(0.95); + } + + 75% { + transform: scale(1); + } + + 100% { + transform: scale(0.95); + } +} + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Tooltip variables +:root { + --md-annotation-bg-icon: svg-load("material/circle.svg"); + --md-annotation-icon: svg-load("material/plus-circle.svg"); + --md-tooltip-width: #{px2rem(400px)}; +} + +// ---------------------------------------------------------------------------- + +// Tooltip +.md-tooltip { + position: absolute; + top: var(--md-tooltip-y); + left: + clamp( + var(--md-tooltip-0, #{px2rem(0px)}) + #{px2rem(16px)}, + var(--md-tooltip-x), + 100vw + + var(--md-tooltip-0, #{px2rem(0px)}) + #{px2rem(16px)} - + var(--md-tooltip-width) - + 2 * #{px2rem(16px)} + ); + // Hack: set an explicit `z-index` so we can transition it to ensure that any + // following elements are not overlaying the tooltip during the transition. + z-index: 0; + width: var(--md-tooltip-width); + max-width: calc(100vw - 2 * #{px2rem(16px)}); + font-family: var(--md-text-font-family); + color: var(--md-default-fg-color); + background-color: var(--md-default-bg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z2); + opacity: 0; + transition: + transform 0ms 250ms, + opacity 250ms, + z-index 250ms; + transform: translateY(px2rem(-8px)); + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + + // Active tooltip + &--active { + z-index: 2; + opacity: 1; + transition: + transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 250ms, + z-index 0ms; + transform: translateY(0); + } + + // Show outline on target and for keyboard devices + :is(.focus-visible > &, &:target) { + outline: var(--md-accent-fg-color) auto; + } + + // Tooltip wrapper + &__inner { + padding: px2rem(16px); + font-size: px2rem(12.8px); + + // Adjust spacing on first child + &.md-typeset > :first-child { + margin-top: 0; + } + + // Adjust spacing on last child + &.md-typeset > :last-child { + margin-bottom: 0; + } + } +} + +// ---------------------------------------------------------------------------- + +// Annotation +.md-annotation { + font-weight: 400; + white-space: normal; + vertical-align: text-bottom; + outline: none; + + // Adjust for right-to-left languages + [dir="rtl"] & { + direction: rtl; + } + + // Annotation index in code block + code & { + font-family: var(--md-code-font-family); + font-size: inherit; + } + + // Annotation is not hidden (e.g. when copying) + &:not([hidden]) { + display: inline-block; + // Hack: ensure that the line height doesn't exceed the line height of the + // hosting line, because it will lead to dancing pixels. + line-height: 1.25; + } + + // Annotation index + &__index { + position: relative; + z-index: 0; + display: inline-block; + margin-inline: 0.4ch; + vertical-align: text-top; + cursor: pointer; + user-select: none; + outline: none; + + // Hack: increase specificity to override default for anchors in typesetted + // content, because transitions are defined on anchor elements + .md-annotation & { + transition: z-index 250ms; + } + + // Hack: Work around Firefox bug that renders a subpixel outline when + // rotating a mask image element. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1671784 + overflow: hidden; // stylelint-disable-line order/properties-order + border-radius: 0.01px; + + // [screen]: Render annotation markers as icons + @media screen { + width: 2.2ch; + + // Annotation is visible + [data-md-visible] > & { + animation: pulse 2000ms infinite; + } + + // Annotation marker background + &::before { + position: absolute; + top: -0.1ch; + z-index: -1; + width: 2.2ch; + height: 2.2ch; + content: ""; + background: var(--md-default-bg-color); + mask-image: var(--md-annotation-bg-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + + // Annotation marker – the marker must be positioned absolutely behind + // the index, because it shouldn't impact the rendering of a code block. + // Otherwise, small rounding differences in browsers can sometimes mess up + // alignment of text following an annotation. + &::after { + position: absolute; + top: -0.1ch; + z-index: -1; + width: 2.2ch; + height: 2.2ch; + content: ""; + background-color: var(--md-default-fg-color--lighter); + transition: + background-color 250ms, + transform 250ms; + // Hack: promote to own layer to reduce jitter + transform: scale(1.0001); + mask-image: var(--md-annotation-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + + // Annotation marker for active tooltip + .md-tooltip--active + & { + transform: rotate(45deg); + } + + // Annotation marker for active tooltip or on hover + :is(.md-tooltip--active + &, :hover > &) { + background-color: var(--md-accent-fg-color); + } + } + } + + // Annotation index for active tooltip + .md-tooltip--active + & { + z-index: 2; + transition-duration: 0ms; + animation-play-state: paused; + } + + // Annotation marker + [data-md-annotation-id] { + display: inline-block; + + // [print]: Render annotation markers as numbers + @media print { + padding: 0 0.6ch; + font-weight: 700; + color: var(--md-default-bg-color); + white-space: nowrap; + background: var(--md-default-fg-color--lighter); + border-radius: 2ch; + + // Annotation marker content + &::after { + content: attr(data-md-annotation-id); + } + } + } + } +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Annotation list + .md-annotation-list { + list-style: none; + counter-reset: xxx; + + // Annotation list item + li { + position: relative; + + // Annotation list marker + &::before { + position: absolute; + top: px2em(4px); + inset-inline-start: px2em(-34px); + min-width: 2ch; + height: 2ch; + padding: 0 0.6ch; + font-size: px2em(14.2px); + font-weight: 700; + line-height: 1.25; + color: var(--md-default-bg-color); + text-align: center; + content: counter(xxx); + counter-increment: xxx; + background: var(--md-default-fg-color--lighter); + border-radius: 2ch; + } + } + } +} diff --git a/src/templates/assets/stylesheets/main/components/_top.scss b/src/templates/assets/stylesheets/main/components/_top.scss new file mode 100644 index 00000000..c24d44d1 --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_top.scss @@ -0,0 +1,83 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Back-to-top button +.md-top { + position: fixed; + top: px2rem(48px + 16px); + z-index: 2; + display: block; + padding: px2rem(8px) px2rem(16px); + margin-inline-start: 50%; + font-size: px2rem(14px); + color: var(--md-default-fg-color--light); + cursor: pointer; + background-color: var(--md-default-bg-color); + border-radius: px2rem(32px); + outline: none; + box-shadow: var(--md-shadow-z2); + transition: + color 125ms, + background-color 125ms, + transform 125ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 125ms; + transform: translate(-50%, 0); + + // [print]: Hide back-to-top button + @media print { + display: none; + } + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translate(50%, 0); + } + + // Back-to-top button is hidden + &[hidden] { + pointer-events: none; + opacity: 0; + transition-duration: 0ms; + transform: translate(-50%, px2rem(4px)); + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translate(50%, px2rem(4px)); + } + } + + // Back-to-top button on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-bg-color); + background-color: var(--md-accent-fg-color); + } + + // Inline icon + svg { + display: inline-block; + vertical-align: -0.5em; + } +} diff --git a/src/templates/assets/stylesheets/main/components/_version.scss b/src/templates/assets/stylesheets/main/components/_version.scss new file mode 100644 index 00000000..3f85d6cd --- /dev/null +++ b/src/templates/assets/stylesheets/main/components/_version.scss @@ -0,0 +1,150 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Keyframes +// ---------------------------------------------------------------------------- + +// See https://github.com/squidfunk/mkdocs-material/issues/2429 +@keyframes hoverfix { + 0% { + pointer-events: none; + } +} + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Version selection variables +:root { + --md-version-icon: svg-load("fontawesome/solid/caret-down.svg"); +} + +// ---------------------------------------------------------------------------- + +// Version selection +.md-version { + flex-shrink: 0; + height: px2rem(48px); + font-size: px2rem(16px); + + // Current selection + &__current { + position: relative; + // Hack: in general, we would use `vertical-align` to align the version at + // the bottom with the title, but since the list uses absolute positioning, + // this won't work consistently. Furthermore, we would need to use inline + // positioning to align the links, which looks jagged. + top: px2rem(1px); + margin-inline: px2rem(28px) px2rem(8px); + color: inherit; + cursor: pointer; + outline: none; + + // Version selection icon + &::after { + display: inline-block; + width: px2rem(8px); + height: px2rem(12px); + margin-inline-start: px2rem(8px); + content: ""; + background-color: currentcolor; + mask-image: var(--md-version-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + } + + // Version selection list + &__list { + position: absolute; + top: px2rem(3px); + z-index: 3; + max-height: 0; + padding: 0; + margin: px2rem(4px) px2rem(16px); + overflow: auto; + color: var(--md-default-fg-color); + list-style-type: none; + background-color: var(--md-default-bg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z2); + opacity: 0; + transition: + max-height 0ms 500ms, + opacity 250ms 250ms; + scroll-snap-type: y mandatory; + + // Version selection list on parent focus/hover + .md-version:is(:focus-within, :hover) & { + max-height: px2rem(200px); + opacity: 1; + transition: + max-height 0ms, + opacity 250ms; + } + + // Fix hover on touch devices + @media (pointer: coarse), (hover: none) { + // Switch off on hover + .md-version:hover & { + animation: hoverfix 250ms forwards; + } + + // Enable on focus + .md-version:focus-within & { + animation: none; + } + } + } + + // Version selection item + &__item { + line-height: px2rem(36px); + } + + // Version selection link + &__link { + display: block; + width: 100%; + padding-inline: px2rem(12px) px2rem(24px); + white-space: nowrap; + cursor: pointer; + outline: none; + transition: + color 250ms, + background-color 250ms; + scroll-snap-align: start; + + // Link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + } + + // Link on focus + &:focus { + background-color: var(--md-default-fg-color--lightest); + } + } +} diff --git a/src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss b/src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss new file mode 100644 index 00000000..bf517989 --- /dev/null +++ b/src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss @@ -0,0 +1,195 @@ +//// +/// 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 +//// + +@use "sass:color"; +@use "sass:list"; + +// ---------------------------------------------------------------------------- +// Variables +// ---------------------------------------------------------------------------- + +/// Admonition flavours +$admonitions: ( + "note": pencil-circle $clr-blue-a200, + "abstract": clipboard-text $clr-light-blue-a400, + "info": information $clr-cyan-a700, + "tip": fire $clr-teal-a700, + "success": check $clr-green-a700, + "question": help-circle $clr-light-green-a700, + "warning": alert $clr-orange-a400, + "failure": close $clr-red-a200, + "danger": lightning-bolt-circle $clr-red-a400, + "bug": shield-bug $clr-pink-a400, + "example": test-tube $clr-deep-purple-a200, + "quote": format-quote-close $clr-grey +) !default; + +// ---------------------------------------------------------------------------- +// Rules: layout +// ---------------------------------------------------------------------------- + +// Admonition variables +:root { + @each $name, $props in $admonitions { + --md-admonition-icon--#{$name}: + svg-load("material/#{list.nth($props, 1)}.svg"); + } +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Admonition - note that all styles also apply to details tags, which are + // rendered as collapsible admonitions with summary elements as titles. + .admonition { + display: flow-root; + padding: 0 px2rem(12px); + margin: px2em(20px, 12.8px) 0; + font-size: px2rem(12.8px); + color: var(--md-admonition-fg-color); + background-color: var(--md-admonition-bg-color); + border: px2rem(1.5px) solid $clr-blue-a200; + border-radius: px2rem(4px); + box-shadow: var(--md-shadow-z1); + transition: box-shadow 125ms; + page-break-inside: avoid; + + // [print]: Omit shadow as it may lead to rendering errors + @media print { + box-shadow: none; + } + + // Admonition on focus + &:focus-within { + box-shadow: 0 0 0 px2rem(4px) color.adjust($clr-blue-a200, $alpha: -0.9); + } + + // Hack: Chrome exhibits a weird issue where it will set nested elements to + // content-box. Doesn't happen in other browsers, so looks like a bug. + > * { + box-sizing: border-box; + } + + // Adjust vertical spacing for nested admonitions + .admonition { + margin-top: 1em; + margin-bottom: 1em; + } + + // Adjust spacing for contained table wrappers + .md-typeset__scrollwrap { + margin: 1em px2rem(-12px); + } + + // Adjust spacing for contained tables + .md-typeset__table { + padding: 0 px2rem(12px); + } + + // Adjust spacing for single-child tabbed block container + > .tabbed-set:only-child { + margin-top: 0; + } + + // Adjust spacing on last child + html & > :last-child { + margin-bottom: px2rem(12px); + } + } + + // Admonition title + .admonition-title { + position: relative; + padding-block: px2rem(8px); + padding-inline: px2rem(40px) px2rem(12px); + margin-block: 0; + margin-inline: px2rem(-12px); + font-weight: 700; + background-color: color.adjust($clr-blue-a200, $alpha: -0.9); + border: none; + border-inline-start-width: px2rem(4px); + border-start-start-radius: px2rem(2px); + border-start-end-radius: px2rem(2px); + + // Adjust spacing for title-only admonitions + html &:last-child { + margin-bottom: 0; + } + + // Admonition icon + &::before { + position: absolute; + top: px2em(10px); + width: px2rem(20px); + height: px2rem(20px); + content: ""; + background-color: $clr-blue-a200; + inset-inline-start: px2rem(12px); + mask-image: var(--md-admonition-icon--note); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + + // Inline code block + code { + box-shadow: 0 0 0 px2rem(1px) var(--md-default-fg-color--lightest); + } + } +} + +// ---------------------------------------------------------------------------- +// Rules: flavours +// ---------------------------------------------------------------------------- + +// Define admonition flavors +@each $name, $props in $admonitions { + $tint: list.nth($props, 2); + + // Admonition flavour + .md-typeset .admonition.#{$name} { + border-color: $tint; + + // Admonition on focus + &:focus-within { + box-shadow: 0 0 0 px2rem(4px) color.adjust($tint, $alpha: -0.9); + } + } + + // Admonition flavour title + .md-typeset .#{$name} > .admonition-title { + background-color: color.adjust($tint, $alpha: -0.9); + + // Admonition icon + &::before { + background-color: $tint; + mask-image: var(--md-admonition-icon--#{$name}); + } + + // Details marker + &::after { + color: $tint; + } + } +} diff --git a/src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss b/src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss new file mode 100644 index 00000000..59447d89 --- /dev/null +++ b/src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss @@ -0,0 +1,146 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Footnotes variables +:root { + --md-footnotes-icon: svg-load("material/keyboard-return.svg"); +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Footnote container + .footnote { + font-size: px2rem(12.8px); + color: var(--md-default-fg-color--light); + + // Footnote list - omit left indentation + > ol { + margin-inline-start: 0; + + // Footnote item - footnote items can contain lists, so we need to scope + // the spacing adjustments to the top-level footnote item. + > li { + transition: color 125ms; + + // Darken color on target + &:target { + color: var(--md-default-fg-color); + } + + // Show backreferences on footnote focus without transition + &:focus-within .footnote-backref { + opacity: 1; + transition: none; + transform: translateX(0); + } + + // Show backreferences on footnote hover/target + &:is(:hover, :target) .footnote-backref { + opacity: 1; + transform: translateX(0); + } + + // Adjust spacing on first child + > :first-child { + margin-top: 0; + } + } + } + } + + // Footnote reference + .footnote-ref { + font-size: px2em(12px, 16px); + font-weight: 700; + + // Hack: increase specificity to override default + html & { + outline-offset: px2rem(2px); + } + } + + // Show outline for all devices + [id^="fnref:"]:target > .footnote-ref { + outline: auto; + } + + // Footnote backreference + .footnote-backref { + display: inline-block; + // Hack: omit Unicode arrow for replacement with icon + font-size: 0; + color: var(--md-typeset-a-color); + vertical-align: text-bottom; + opacity: 0; + transition: + color 250ms, + transform 250ms 250ms, + opacity 125ms 250ms; + transform: translateX(px2rem(5px)); + + // [print]: Show footnote backreferences + @media print { + color: var(--md-typeset-a-color); + opacity: 1; + transform: translateX(0); + } + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translateX(px2rem(-5px)); + } + + // Adjust color on hover + &:hover { + color: var(--md-accent-fg-color); + } + + // Footnote backreference icon + &::before { + display: inline-block; + width: px2rem(16px); + height: px2rem(16px); + content: ""; + background-color: currentcolor; + mask-image: var(--md-footnotes-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + + // Adjust for right-to-left languages + [dir="rtl"] & { + + // Flip icon vertically + svg { + transform: scaleX(-1); + } + } + } + } +} diff --git a/src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss b/src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss new file mode 100644 index 00000000..8284a5c0 --- /dev/null +++ b/src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss @@ -0,0 +1,92 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Headerlink + .headerlink { + display: inline-block; + margin-inline-start: px2rem(10px); + color: var(--md-default-fg-color--lighter); + opacity: 0; + transition: + color 250ms, + opacity 125ms; + + // [print]: Hide headerlinks + @media print { + display: none; + } + } + + // Show headerlinks on parent hover + :is(:hover, :target) > .headerlink, + .headerlink:focus { + opacity: 1; + transition: + color 250ms, + opacity 125ms; + } + + // Adjust color on parent target or focus/hover + :target > .headerlink, + .headerlink:is(:focus, :hover) { + color: var(--md-accent-fg-color); + } + + // Adjust scroll margin for all elements with `id` attributes + :target { + --md-scroll-margin: #{px2rem(48px + 24px)}; + --md-scroll-offset: #{px2rem(0px)}; + // Scroll margin is finally ready for prime time - before, we used a hack + // for anchor correction based on pseudo elements but those times are gone. + scroll-margin-top: + calc( + var(--md-scroll-margin) - + var(--md-scroll-offset) + ); + + // [screen +]: Sticky navigation tabs + @include break-from-device(screen) { + + // Adjust scroll margin for sticky navigation tabs + .md-header--lifted ~ .md-container & { + --md-scroll-margin: #{px2rem(96px + 24px)}; + } + } + } + + // Adjust scroll offset for headlines of level 1-3 + :is(h1, h2, h3):target { + --md-scroll-offset: #{px2rem(4px)}; + } + + // Adjust scroll offset for headlines of level 4 + h4:target { + --md-scroll-offset: #{px2rem(3px)}; + } +} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss new file mode 100644 index 00000000..fe8ffd62 --- /dev/null +++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss @@ -0,0 +1,52 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Arithmatex container + div.arithmatex { + overflow: auto; + + // [mobile -]: Align with body copy + @include break-to-device(mobile) { + margin: 0 px2rem(-16px); + } + + // Arithmatex content + > * { + width: min-content; + padding: 0 px2rem(16px); + margin-inline: auto !important; // stylelint-disable-line + touch-action: auto; + + // MathJax container - see https://bit.ly/3HR8YJ5 + mjx-container { + margin: 0 !important; // stylelint-disable-line + } + } + } +} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss new file mode 100644 index 00000000..683705ce --- /dev/null +++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss @@ -0,0 +1,76 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Deletion + del.critic { + background-color: var(--md-typeset-del-color); + box-decoration-break: clone; + } + + // Addition + ins.critic { + background-color: var(--md-typeset-ins-color); + box-decoration-break: clone; + } + + // Comment + .critic.comment { + color: var(--md-code-hl-comment-color); + box-decoration-break: clone; + + // Comment opening mark + &::before { + content: "/* "; + } + + // Comment closing mark + &::after { + content: " */"; + } + } + + // Critic block + .critic.block { + display: block; + padding-inline: px2rem(16px); + margin: 1em 0; + overflow: auto; + box-shadow: none; + + // Adjust spacing on first child + > :first-child { + margin-top: 0.5em; + } + + // Adjust spacing on last child + > :last-child { + margin-bottom: 0.5em; + } + } +} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss new file mode 100644 index 00000000..8eea678a --- /dev/null +++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss @@ -0,0 +1,121 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Details variables +:root { + --md-details-icon: svg-load("material/chevron-right.svg"); +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Details + details { + @extend .admonition; + + display: flow-root; + padding-top: 0; + overflow: visible; + + // Details title icon - rotate icon on transition to open state + &[open] > summary::after { + transform: rotate(90deg); + } + + // Adjust spacing for details in closed state + &:not([open]) { + padding-bottom: 0; + box-shadow: none; + + // Hack: we cannot set `overflow: hidden` on the `details` element (which + // is why we set it to `overflow: visible`, as the outline would not be + // visible when focusing. Therefore, we must set the border radius on the + // summary explicitly. + > summary { + border-radius: px2rem(2px); + } + } + } + + // Details title + summary { + @extend .admonition-title; + + display: block; + min-height: px2rem(20px); + padding-inline-end: px2rem(36px); + cursor: pointer; + border-start-start-radius: px2rem(2px); + border-start-end-radius: px2rem(2px); + + // Show outline for keyboard devices + &.focus-visible { + outline-color: var(--md-accent-fg-color); + outline-offset: px2rem(4px); + } + + // Hide outline for pointer devices + &:not(.focus-visible) { + outline: none; + -webkit-tap-highlight-color: transparent; + } + + // Details marker + &::after { + position: absolute; + top: px2em(10px); + width: px2rem(20px); + height: px2rem(20px); + content: ""; + background-color: currentcolor; + transition: transform 250ms; + transform: rotate(0deg); + inset-inline-end: px2rem(8px); + mask-image: var(--md-details-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: rotate(180deg); + } + } + + // Hide native details marker - modern + &::marker { + display: none; + } + + // Hide native details marker - legacy, must be split into a seprate rule, + // so older browsers don't consider the selector list as invalid + &::-webkit-details-marker { + display: none; + } + } +} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss new file mode 100644 index 00000000..8b351013 --- /dev/null +++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss @@ -0,0 +1,43 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Emoji and icon container + :is(.emojione, .twemoji, .gemoji) { + display: inline-flex; + height: px2em(18px); + vertical-align: text-top; + + // Icon - inlined via mkdocs-material-extensions + svg { + width: px2em(18px); + max-height: 100%; + fill: currentcolor; + } + } +} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss new file mode 100644 index 00000000..7d297677 --- /dev/null +++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss @@ -0,0 +1,382 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules: syntax highlighting +// ---------------------------------------------------------------------------- + +// Code block +.highlight { + + // .o = Operator + // .ow = Operator, word + :is(.o, .ow) { + color: var(--md-code-hl-operator-color); + } + + .p { // Punctuation + color: var(--md-code-hl-punctuation-color); + } + + // .cpf = Comment, preprocessor file + // .l = Literal + // .s = Literal, string + // .sb = Literal, string backticks + // .sc = Literal, string char + // .s2 = Literal, string double + // .si = Literal, string interpol + // .s1 = Literal, string single + // .ss = Literal, string symbol + :is(.cpf, .l, .s, .sb, .sc, .s2, .si, .s1, .ss) { + color: var(--md-code-hl-string-color); + } + + // .cp = Comment, pre-processor + // .se = Literal, string escape + // .sh = Literal, string heredoc + // .sr = Literal, string regex + // .sx = Literal, string other + :is(.cp, .se, .sh, .sr, .sx) { + color: var(--md-code-hl-special-color); + } + + // .m = Number + // .mb = Number, binary + // .mf = Number, float + // .mh = Number, hex + // .mi = Number, integer + // .il = Number, integer long + // .mo = Number, octal + :is(.m, .mb, .mf, .mh, .mi, .il, .mo) { + color: var(--md-code-hl-number-color); + } + + // .k = Keyword, + // .kd = Keyword, declaration + // .kn = Keyword, namespace + // .kp = Keyword, pseudo + // .kr = Keyword, reserved + // .kt = Keyword, type + :is(.k, .kd, .kn, .kp, .kr, .kt) { + color: var(--md-code-hl-keyword-color); + } + + // .kc = Keyword, constant + // .n = Name + :is(.kc, .n) { + color: var(--md-code-hl-name-color); + } + + // .no = Name, constant + // .nb = Name, builtin + // .bp = Name, builtin pseudo + :is(.no, .nb, .bp) { + color: var(--md-code-hl-constant-color); + } + + // .nc = Name, class + // .ne = Name, exception + // .nf = Name, function + // .nn = Name, namespace + :is(.nc, .ne, .nf, .nn) { + color: var(--md-code-hl-function-color); + } + + // .nd = Name, decorator + // .ni = Name, entity + // .nl = Name, label + // .nt = Name, tag + :is(.nd, .ni, .nl, .nt) { + color: var(--md-code-hl-keyword-color); + } + + // .c = Comment + // .cm = Comment, multiline + // .c1 = Comment, single + // .ch = Comment, shebang + // .cs = Comment, special + // .sd = Literal, string doc + :is(.c, .cm, .c1, .ch, .cs, .sd) { + color: var(--md-code-hl-comment-color); + } + + // .na = Name, attribute + // .nv = Variable, + // .vc = Variable, class + // .vg = Variable, global + // .vi = Variable, instance + :is(.na, .nv, .vc, .vg, .vi) { + color: var(--md-code-hl-variable-color); + } + + // .ge = Generic, emph + // .gr = Generic, error + // .gh = Generic, heading + // .go = Generic, output + // .gp = Generic, prompt + // .gs = Generic, strong + // .gu = Generic, subheading + // .gt = Generic, traceback + :is(.ge, .gr, .gh, .go, .gp, .gs, .gu, .gt) { + color: var(--md-code-hl-generic-color); + } + + // .gd = Diff, delete + // .gi = Diff, insert + :is(.gd, .gi) { + padding: 0 px2em(2px); + margin: 0 px2em(-2px); + border-radius: px2rem(2px); + } + + .gd { // Diff, delete + background-color: var(--md-typeset-del-color); + } + + .gi { // Diff, insert + background-color: var(--md-typeset-ins-color); + } + + // Highlighted line + .hll { + display: block; + padding: 0 px2em(16px, 13.6px); + margin: 0 px2em(-16px, 13.6px); + background-color: var(--md-code-hl-color--light); + box-shadow: 2px 0 0 0 var(--md-code-hl-color) inset; + } + + // Code block title + span.filename { + position: relative; + display: flow-root; + padding: px2em(9px, 13.6px) px2em(16px, 13.6px); + margin-top: 1em; + font-size: px2em(13.6px); + font-weight: 700; + background-color: var(--md-code-bg-color); + border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest); + border-top-left-radius: px2rem(2px); + border-top-right-radius: px2rem(2px); + + // Adjust spacing for code block + + pre { + margin-top: 0; + + // Remove rounded border on top side + > code { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + } + + // Code block line numbers (pymdownx-inline) + [data-linenos]::before { + position: sticky; + left: px2em(-16px, 13.6px); + // A `z-index` of 3 is necessary for ensuring that code block annotations + // don't overlay line numbers, as active annotations have a `z-index` of 2. + z-index: 3; + float: left; + padding-left: px2em(16px, 13.6px); + margin-right: px2em(16px, 13.6px); + margin-left: px2em(-16px, 13.6px); + color: var(--md-default-fg-color--light); + content: attr(data-linenos); + user-select: none; + background-color: var(--md-code-bg-color); + box-shadow: px2rem(-1px) 0 var(--md-default-fg-color--lightest) inset; + } + + // Code block line anchors - Chrome and Safari seem to have a strange bug + // where scroll margin is not applied to anchors inside code blocks. Setting + // positioning to absolute seems to fix the problem. Interestingly, this does + // not happen in Firefox. Furthermore we must set `visibility: hidden` or + // the copy to clipboard functionality will include an empty line between + // each set of lines. + code a[id] { + position: absolute; + visibility: hidden; + } + + // Copying in progress - this class is set before the content is copied and + // removed after copying is done to mitigate whitespace-related issues. + code[data-md-copying] { + + // Temporarily remove highlighted lines - see https://bit.ly/32iVGWh + .hll { + display: contents; + } + + // Temporarily remove annotations + .md-annotation { + display: none; + } + } +} + +// ---------------------------------------------------------------------------- +// Rules: layout +// ---------------------------------------------------------------------------- + +// Code block with line numbers +.highlighttable { + display: flow-root; + + // Set table elements to block layout, because otherwise the whole flexbox + // hacking won't work correctly + :is(tbody, td) { + display: block; + padding: 0; + } + + // We need to use flexbox layout, because otherwise it's not possible to + // make the code container scroll while keeping the line numbers static + tr { + display: flex; + } + + // The pre tags are nested inside a table, so we need to omit the margin + // because it collapses below all the overflows + pre { + margin: 0; + } + + // Code block title container + th.filename { + flex-grow: 1; + padding: 0; + text-align: left; + + // Adjust spacing + span.filename { + margin-top: 0; + } + } + + // Code block line numbers - disable user selection, so code can be easily + // copied without accidentally also copying the line numbers + .linenos { + padding: px2em(10.5px, 13.6px) px2em(16px, 13.6px); + padding-right: 0; + font-size: px2em(13.6px); + user-select: none; + background-color: var(--md-code-bg-color); + border-top-left-radius: px2rem(2px); + border-bottom-left-radius: px2rem(2px); + } + + // Code block line numbers container + .linenodiv { + padding-right: px2em(8px, 13.6px); + box-shadow: px2rem(-1px) 0 var(--md-default-fg-color--lightest) inset; + + // Adjust colors and alignment + pre { + color: var(--md-default-fg-color--light); + text-align: right; + } + } + + // Code block container - stretch to remaining space + .code { + flex: 1; + min-width: 0; + } +} + +// Code block line numbers container +.linenodiv a { + color: inherit; +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Code block with line numbers - unfortunately, these selectors need to be + // overly specific so they don't bleed into code blocks in annotations. + .highlighttable { + margin: 1em 0; + direction: ltr; + + // Remove rounded borders on code blocks + > tbody > tr > .code > div > pre > code { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } + + // Code block result container + .highlight + .result { + padding: 0 px2em(16px); + margin-top: calc(-1em + #{px2em(-2px)}); + overflow: visible; + border: px2rem(1px) solid var(--md-code-bg-color); + border-top-width: px2rem(2px); + border-bottom-right-radius: px2rem(2px); + border-bottom-left-radius: px2rem(2px); + + // Clearfix, because we can't use overflow: auto + &::after { + display: block; + clear: both; + content: ""; + } + } +} + +// ---------------------------------------------------------------------------- +// Rules: top-level +// ---------------------------------------------------------------------------- + +// [mobile -]: Align with body copy +@include break-to-device(mobile) { + + // Top-level code block + .md-content__inner > .highlight { + margin: 1em px2rem(-16px); + + // Remove rounded borders + > .filename, + > pre > code { + border-radius: 0; + } + + // Code block with line numbers - unfortunately, these selectors need to be + // overly specific so they don't bleed into code blocks in annotations. + > .highlighttable > tbody > tr > .filename span.filename, + > .highlighttable > tbody > tr > .linenos, + > .highlighttable > tbody > tr > .code > div > pre > code { + border-radius: 0; + } + + // Code block result container + + .result { + margin-inline: px2rem(-16px); + border-inline-width: 0; + border-radius: 0; + } + } +} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss new file mode 100644 index 00000000..8749f08c --- /dev/null +++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss @@ -0,0 +1,115 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Keyboard key + .keys { + + // Keyboard key icon + kbd:is(::before, ::after) { + position: relative; + margin: 0; + color: inherit; + -moz-osx-font-smoothing: initial; + -webkit-font-smoothing: initial; + } + + // Surrounding text + span { + padding: 0 px2em(3.2px); + color: var(--md-default-fg-color--light); + } + + // Define keyboard keys with left icon + @each $name, $code in ( + + // Modifiers + "alt": "\2387", + "left-alt": "\2387", + "right-alt": "\2387", + "command": "\2318", + "left-command": "\2318", + "right-command": "\2318", + "control": "\2303", + "left-control": "\2303", + "right-control": "\2303", + "meta": "\25C6", + "left-meta": "\25C6", + "right-meta": "\25C6", + "option": "\2325", + "left-option": "\2325", + "right-option": "\2325", + "shift": "\21E7", + "left-shift": "\21E7", + "right-shift": "\21E7", + "super": "\2756", + "left-super": "\2756", + "right-super": "\2756", + "windows": "\229E", + "left-windows": "\229E", + "right-windows": "\229E", + + // Other keys + "arrow-down": "\2193", + "arrow-left": "\2190", + "arrow-right": "\2192", + "arrow-up": "\2191", + "backspace": "\232B", + "backtab": "\21E4", + "caps-lock": "\21EA", + "clear": "\2327", + "context-menu": "\2630", + "delete": "\2326", + "eject": "\23CF", + "end": "\2913", + "escape": "\238B", + "home": "\2912", + "insert": "\2380", + "page-down": "\21DF", + "page-up": "\21DE", + "print-screen": "\2399" + ) { + .key-#{$name}::before { + padding-right: px2em(6.4px); + content: $code; + } + } + + // Define keyboard keys with right icon + @each $name, $code in ( + "tab": "\21E5", + "num-enter": "\2324", + "enter": "\23CE" + ) { + .key-#{$name}::after { + padding-left: px2em(6.4px); + content: $code; + } + } + } +} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss new file mode 100644 index 00000000..9df91bfc --- /dev/null +++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss @@ -0,0 +1,400 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Tabbed variables +:root { + --md-tabbed-icon--prev: svg-load("material/chevron-left.svg"); + --md-tabbed-icon--next: svg-load("material/chevron-right.svg"); +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Tabbed container + .tabbed-set { + position: relative; + display: flex; + flex-flow: column wrap; + margin: 1em 0; + border-radius: px2rem(2px); + + // Tab radio button - the Tabbed extension will generate radio buttons with + // labels, so tabs can be triggered without the necessity for JavaScript. + // This is pretty cool, as it has great accessibility out-of-the box, so + // we just hide the radio button and toggle the label color for indication. + > input { + position: absolute; + width: 0; + height: 0; + opacity: 0; + + // Adjust scroll margin + &:target { + --md-scroll-offset: #{px2em(10px, 16px)}; + } + + // Tab label states + @for $i from 20 through 1 { + &:nth-child(#{$i}) { + + // Tab is active + &:checked { + + // Tab label + ~ .tabbed-labels > :nth-child(#{$i}) { + @extend %tabbed-label; + } + + // Tab content + ~ .tabbed-content > :nth-child(#{$i}) { + @extend %tabbed-content; + } + } + + // Tab label on keyboard focus + &.focus-visible ~ .tabbed-labels > :nth-child(#{$i}) { + @extend %tabbed-label-focus-visible; + } + } + } + + // Tab indicator on keyboard focus + &.focus-visible ~ .tabbed-labels::before { + background-color: var(--md-accent-fg-color); + } + } + } + + // Tabbed labels + .tabbed-labels { + display: flex; + max-width: 100%; + overflow: auto; + box-shadow: 0 px2rem(-1px) var(--md-default-fg-color--lightest) inset; + -ms-overflow-style: none; // IE, Edge + scrollbar-width: none; // Firefox + + // [print]: Move one layer up for ordering + @media print { + display: contents; + } + + // [screen and no reduced motion]: Disable animation + @media screen { + + // [js]: Show animated tab indicator + .js & { + position: relative; + + // Tab indicator + &::before { + position: absolute; + bottom: 0; + left: 0; + display: block; + width: var(--md-indicator-width); + height: 2px; + content: ""; + background: var(--md-default-fg-color); + transition: + width 225ms, + background-color 250ms, + transform 250ms; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transform: translateX(var(--md-indicator-x)); + } + } + } + + // Webkit scrollbar + &::-webkit-scrollbar { + display: none; // Chrome, Safari + } + + // Tab label + > label { + flex-shrink: 0; + width: auto; + padding: px2em(10px, 12.8px) 1.25em px2em(8px, 12.8px); + font-size: px2rem(12.8px); + font-weight: 700; + color: var(--md-default-fg-color--light); + white-space: nowrap; + cursor: pointer; + border-bottom: px2rem(2px) solid transparent; + border-radius: px2rem(2px) px2rem(2px) 0 0; + transition: + background-color 250ms, + color 250ms; + scroll-margin-inline-start: px2rem(20px); + + // [print]: Intersperse labels with containers + @media print { + + // Ensure correct order of labels + @for $i from 1 through 20 { + &:nth-child(#{$i}) { + order: $i; + } + } + } + + // Tab label on hover + &:hover { + color: var(--md-default-fg-color); + } + } + } + + // Tabbed content + .tabbed-content { + width: 100%; + + // [print]: Move one layer up for ordering + @media print { + display: contents; + } + } + + // Tabbed block + .tabbed-block { + display: none; + + // [print]: Intersperse labels with containers + @media print { + display: block; + + // Ensure correct order of containers + @for $i from 1 through 20 { + &:nth-child(#{$i}) { + order: $i; + } + } + } + + // Code block is the first child of a tab - remove margin and mirror + // previous (now deprecated) SuperFences code block grouping behavior + > pre:first-child, + > .highlight:first-child > pre { + margin: 0; + + // Remove rounded borders on code block + > code { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + + // Code block is the first child of a tab - remove margin and mirror + // previous (now deprecated) SuperFences code block grouping behavior + > .highlight:first-child { + + // Code block title - remove spacing and rounded borders + > .filename { + margin: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + // Code block with line numbers - unfortunately, these selectors need to + // be overly specific so they don't bleed into code blocks in annotations. + > .highlighttable { + margin: 0; + + // Remove rounded borders on line numbers and titles + > tbody > tr > .filename span.filename, + > tbody > tr > .linenos { + margin: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + // Remove rounded borders on code blocks + > tbody > tr > .code > div > pre > code { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + + // Code block result container - adjust spacing + + .result { + margin-top: px2em(-2px); + } + } + + // Adjust spacing for nested tabbed container + > .tabbed-set { + margin: 0; + } + } + + // Tabbed button + .tabbed-button { + display: block; + align-self: center; + width: px2rem(18px); + height: px2rem(18px); + margin-top: px2rem(2px); + color: var(--md-default-fg-color--light); + pointer-events: initial; + cursor: pointer; + border-radius: 100%; + transition: background-color 250ms; + + // Tabbed button on hover + &:hover { + color: var(--md-accent-fg-color); + background-color: var(--md-accent-fg-color--transparent); + } + + // Tabbed button icon + &::after { + display: block; + width: 100%; + height: 100%; + content: ""; + background-color: currentcolor; + transition: + background-color 250ms, + transform 250ms; + mask-image: var(--md-tabbed-icon--prev); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + } + + // Tabbed control + .tabbed-control { + position: absolute; + display: flex; + justify-content: start; + width: px2rem(24px); + height: px2rem(38px); + pointer-events: none; + background: + linear-gradient( + to right, + var(--md-default-bg-color) 60%, + transparent + ); + transition: opacity 125ms; + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: rotate(180deg); + } + + // Tabbed control is hidden + &[hidden] { + opacity: 0; + } + + // Tabbed control next + &--next { + right: 0; + justify-content: end; + background: + linear-gradient( + to left, + var(--md-default-bg-color) 60%, + transparent + ); + + // Tabbed button icon content + .tabbed-button::after { + mask-image: var(--md-tabbed-icon--next); + } + } + } +} + +// ---------------------------------------------------------------------------- +// Rules: top-level +// ---------------------------------------------------------------------------- + +// [mobile -]: Align with body copy +@include break-to-device(mobile) { + + // Top-level tabbed labels + .md-content__inner > .tabbed-set .tabbed-labels { + max-width: 100vw; + padding-inline-start: px2rem(16px); + margin: 0 px2rem(-16px); + scroll-padding-inline-start: px2rem(16px); + + // Hack: some browsers ignore the right padding on flex containers, + // see https://bit.ly/3lsPS3S + &::after { + padding-inline-end: px2rem(16px); + content: ""; + } + + // Tabbed control previous + ~ .tabbed-control--prev { + width: px2rem(40px); + padding-inline-start: px2rem(16px); + margin-inline-start: px2rem(-16px); + } + + // Tabbed control next + ~ .tabbed-control--next { + width: px2rem(40px); + padding-inline-end: px2rem(16px); + margin-inline-end: px2rem(-16px); + } + } +} + +// ---------------------------------------------------------------------------- +// Placeholders: improve colocation for better compression +// ---------------------------------------------------------------------------- + +// Tab label placeholder +%tabbed-label { + + // [screen]: Show active state + @media screen { + color: var(--md-default-fg-color); + + // [no-js]: Show border (indicator is animated with JavaScript) + .no-js & { + border-color: var(--md-default-fg-color); + } + } +} + +// Tab label on keyboard focus placeholder +%tabbed-label-focus-visible { + color: var(--md-accent-fg-color); +} + +// Tab content placeholder +%tabbed-content { + display: block; +} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss new file mode 100644 index 00000000..a1d1117c --- /dev/null +++ b/src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss @@ -0,0 +1,78 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Tasklist variables +:root { + --md-tasklist-icon: svg-load("octicons/check-circle-fill-24.svg"); + --md-tasklist-icon--checked: svg-load("octicons/check-circle-fill-24.svg"); +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Tasklist item + .task-list-item { + position: relative; + list-style-type: none; + + // Make checkbox items align with normal list items, but position + // everything in ems for correct layout at smaller font sizes + [type="checkbox"] { + position: absolute; + top: 0.45em; + inset-inline-start: -2em; + } + } + + // Hide native checkbox, when custom classes are enabled + .task-list-control [type="checkbox"] { + z-index: -1; + opacity: 0; + } + + // Tasklist indicator in unchecked state + .task-list-indicator::before { + position: absolute; + top: 0.15em; + width: px2em(20px); + height: px2em(20px); + content: ""; + background-color: var(--md-default-fg-color--lightest); + inset-inline-start: px2em(-24px); + mask-image: var(--md-tasklist-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + + // Tasklist indicator in checked state + [type="checkbox"]:checked + .task-list-indicator::before { + background-color: $clr-green-a400; + mask-image: var(--md-tasklist-icon--checked); + } +} diff --git a/src/templates/assets/stylesheets/main/integrations/_mermaid.scss b/src/templates/assets/stylesheets/main/integrations/_mermaid.scss new file mode 100644 index 00000000..d0325f39 --- /dev/null +++ b/src/templates/assets/stylesheets/main/integrations/_mermaid.scss @@ -0,0 +1,67 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Mermaid variables +:root > * { + --md-mermaid-font-family: var(--md-text-font-family), sans-serif; + + // General colors + --md-mermaid-edge-color: var(--md-code-fg-color); + --md-mermaid-node-bg-color: var(--md-accent-fg-color--transparent); + --md-mermaid-node-fg-color: var(--md-accent-fg-color); + --md-mermaid-label-bg-color: var(--md-default-bg-color); + --md-mermaid-label-fg-color: var(--md-code-fg-color); + + // Sequence diagram colors + --md-mermaid-sequence-actor-bg-color: var(--md-mermaid-label-bg-color); + --md-mermaid-sequence-actor-fg-color: var(--md-mermaid-label-fg-color); + --md-mermaid-sequence-actor-border-color: var(--md-mermaid-node-fg-color); + --md-mermaid-sequence-actor-line-color: var(--md-default-fg-color--lighter); + --md-mermaid-sequence-actorman-bg-color: var(--md-mermaid-label-bg-color); + --md-mermaid-sequence-actorman-line-color: var(--md-mermaid-node-fg-color); + --md-mermaid-sequence-box-bg-color: var(--md-mermaid-node-bg-color); + --md-mermaid-sequence-box-fg-color: var(--md-mermaid-edge-color); + --md-mermaid-sequence-label-bg-color: var(--md-mermaid-node-bg-color); + --md-mermaid-sequence-label-fg-color: var(--md-mermaid-node-fg-color); + --md-mermaid-sequence-loop-bg-color: var(--md-mermaid-node-bg-color); + --md-mermaid-sequence-loop-fg-color: var(--md-mermaid-edge-color); + --md-mermaid-sequence-loop-border-color: var(--md-mermaid-node-fg-color); + --md-mermaid-sequence-message-fg-color: var(--md-mermaid-edge-color); + --md-mermaid-sequence-message-line-color: var(--md-mermaid-edge-color); + --md-mermaid-sequence-note-bg-color: var(--md-mermaid-label-bg-color); + --md-mermaid-sequence-note-fg-color: var(--md-mermaid-edge-color); + --md-mermaid-sequence-note-border-color: var(--md-mermaid-label-fg-color); + --md-mermaid-sequence-number-bg-color: var(--md-mermaid-node-fg-color); + --md-mermaid-sequence-number-fg-color: var(--md-accent-bg-color); +} + +// ---------------------------------------------------------------------------- + +// Mermaid container +.mermaid { + margin: 1em 0; + line-height: normal; +} diff --git a/src/templates/assets/stylesheets/palette.scss b/src/templates/assets/stylesheets/palette.scss new file mode 100644 index 00000000..ff73a982 --- /dev/null +++ b/src/templates/assets/stylesheets/palette.scss @@ -0,0 +1,40 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Dependencies +// ---------------------------------------------------------------------------- + +@import "material-color"; + +// ---------------------------------------------------------------------------- +// Local imports +// ---------------------------------------------------------------------------- + +@import "utilities/break"; +@import "utilities/convert"; + +@import "config"; + +@import "palette/scheme"; +@import "palette/accent"; +@import "palette/primary"; diff --git a/src/templates/assets/stylesheets/palette/_accent.scss b/src/templates/assets/stylesheets/palette/_accent.scss new file mode 100644 index 00000000..9f69b596 --- /dev/null +++ b/src/templates/assets/stylesheets/palette/_accent.scss @@ -0,0 +1,61 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Define accent colors +@each $name, $color in ( + "red": $clr-red-a400, + "pink": $clr-pink-a400, + "purple": $clr-purple-a200, + "deep-purple": $clr-deep-purple-a200, + "indigo": $clr-indigo-a200, + "blue": $clr-blue-a200, + "light-blue": $clr-light-blue-a700, + "cyan": $clr-cyan-a700, + "teal": $clr-teal-a700, + "green": $clr-green-a700, + "light-green": $clr-light-green-a700, + "lime": $clr-lime-a700, + "yellow": $clr-yellow-a700, + "amber": $clr-amber-a700, + "orange": $clr-orange-a400, + "deep-orange": $clr-deep-orange-a200 +) { + + // Color palette + [data-md-color-accent="#{$name}"] { + --md-accent-fg-color: hsla(#{hex2hsl($color)}, 1); + --md-accent-fg-color--transparent: hsla(#{hex2hsl($color)}, 0.1); + + // Inverted text for lighter shades + @if index("lime" "yellow" "amber" "orange", $name) { + --md-accent-bg-color: hsla(0, 0%, 0%, 0.87); + --md-accent-bg-color--light: hsla(0, 0%, 0%, 0.54); + } @else { + --md-accent-bg-color: hsla(0, 0%, 100%, 1); + --md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7); + } + } +} diff --git a/src/templates/assets/stylesheets/palette/_primary.scss b/src/templates/assets/stylesheets/palette/_primary.scss new file mode 100644 index 00000000..a8653f0f --- /dev/null +++ b/src/templates/assets/stylesheets/palette/_primary.scss @@ -0,0 +1,203 @@ +//// +/// 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 +//// + +@use "sass:list"; + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Define primary colors +@each $name, $colors in ( + "red": $clr-red-400 $clr-red-300 $clr-red-600, + "pink": $clr-pink-500 $clr-pink-400 $clr-pink-700, + "purple": $clr-purple-400 $clr-purple-300 $clr-purple-600, + "deep-purple": $clr-deep-purple-400 $clr-deep-purple-300 $clr-deep-purple-500, + "indigo": $clr-indigo-500 $clr-indigo-400 $clr-indigo-700, + "blue": $clr-blue-500 $clr-blue-400 $clr-blue-700, + "light-blue": $clr-light-blue-500 $clr-light-blue-400 $clr-light-blue-700, + "cyan": $clr-cyan-500 $clr-cyan-400 $clr-cyan-700, + "teal": $clr-teal-500 $clr-teal-400 $clr-teal-700, + "green": $clr-green-500 $clr-green-400 $clr-green-700, + "light-green": $clr-light-green-500 $clr-light-green-400 $clr-light-green-700, + "lime": $clr-lime-500 $clr-lime-400 $clr-lime-700, + "yellow": $clr-yellow-500 $clr-yellow-400 $clr-yellow-700, + "amber": $clr-amber-500 $clr-amber-400 $clr-amber-700, + "orange": $clr-orange-400 $clr-orange-400 $clr-orange-600, + "deep-orange": $clr-deep-orange-400 $clr-deep-orange-300 $clr-deep-orange-600, + "brown": $clr-brown-500 $clr-brown-400 $clr-brown-700, + "grey": $clr-grey-600 $clr-grey-500 $clr-grey-700, + "blue-grey": $clr-blue-grey-600 $clr-blue-grey-500 $clr-blue-grey-700 +) { + + // Color palette + [data-md-color-primary="#{$name}"] { + --md-primary-fg-color: hsl(#{hex2hsl(list.nth($colors, 1))}); + --md-primary-fg-color--light: hsl(#{hex2hsl(list.nth($colors, 2))}); + --md-primary-fg-color--dark: hsl(#{hex2hsl(list.nth($colors, 3))}); + + // Inverted text for lighter shades + @if index("lime" "yellow" "amber" "orange", $name) { + --md-primary-bg-color: hsla(0, 0%, 0%, 0.87); + --md-primary-bg-color--light: hsla(0, 0%, 0%, 0.54); + } @else { + --md-primary-bg-color: hsla(0, 0%, 100%, 1); + --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7); + } + + // Typeset color shades + @if index("grey" "blue-grey", $name) { + --md-typeset-a-color: hsl(#{hex2hsl($clr-indigo-500)}); + } + } +} + +// ---------------------------------------------------------------------------- + +// Adjust link colors for light primary colors +@each $name, $color in ( + "light-green": hsl(88, 58%, 43%), + "lime": hsl(66, 88%, 32%), + "yellow": hsl(54, 100%, 36%), + "amber": hsl(45, 100%, 41%), + "orange": hsl(36, 100%, 45%) +) { + [data-md-color-primary="#{$name}"]:not([data-md-color-scheme="slate"]) { + --md-typeset-a-color: #{$color}; + } +} + +// ---------------------------------------------------------------------------- +// Rules: white +// ---------------------------------------------------------------------------- + +// Define primary colors for white +[data-md-color-primary="white"] { + --md-primary-fg-color: hsla(var(--md-hue), 0%, 100%, 1); + --md-primary-fg-color--light: hsla(var(--md-hue), 0%, 100%, 0.7); + --md-primary-fg-color--dark: hsla(var(--md-hue), 0%, 0%, 0.07); + --md-primary-bg-color: hsla(var(--md-hue), 0%, 0%, 0.87); + --md-primary-bg-color--light: hsla(var(--md-hue), 0%, 0%, 0.54); + + // Typeset `a` color shades + --md-typeset-a-color: hsl(#{hex2hsl($clr-indigo-500)}); + + // Form button + .md-button { + color: var(--md-typeset-a-color); + + // Primary button + &--primary { + color: hsla(var(--md-hue), 0%, 100%, 1); + background-color: var(--md-typeset-a-color); + border-color: var(--md-typeset-a-color); + } + } + + // [tablet portrait +]: Header-embedded search + @include break-from-device(tablet landscape) { + + // Search form + .md-search__form { + background-color: hsla(var(--md-hue), 0%, 0%, 0.07); + + // Search form on hover + &:hover { + background-color: hsla(var(--md-hue), 0%, 0%, 0.32); + } + } + + // Search icon + .md-search__input + .md-search__icon { + color: hsla(var(--md-hue), 0%, 0%, 0.87); + } + } + + // [screen +]: Add bottom border for tabs + @include break-from-device(screen) { + + // Navigation tabs + .md-tabs { + border-bottom: px2rem(1px) solid hsla(0, 0%, 0%, 0.07); + } + } +} + +// ---------------------------------------------------------------------------- +// Rules: black +// ---------------------------------------------------------------------------- + +// Define primary colors for black +[data-md-color-primary="black"] { + --md-primary-fg-color: hsla(var(--md-hue), 15%, 9%, 1); + --md-primary-fg-color--light: hsla(var(--md-hue), 15%, 9%, 0.54); + --md-primary-fg-color--dark: hsla(var(--md-hue), 15%, 9%, 1); + --md-primary-bg-color: hsla(var(--md-hue), 15%, 100%, 1); + --md-primary-bg-color--light: hsla(var(--md-hue), 15%, 100%, 0.7); + + // Typeset `a` color shades + --md-typeset-a-color: hsl(#{hex2hsl($clr-indigo-500)}); + + // Form button + .md-button { + color: var(--md-typeset-a-color); + + // Primary button + &--primary { + color: hsla(var(--md-hue), 0%, 100%, 1); + background-color: var(--md-typeset-a-color); + border-color: var(--md-typeset-a-color); + } + } + + // Header + .md-header { + background-color: hsla(var(--md-hue), 15%, 9%, 1); + } + + // [tablet portrait -]: Layered navigation + @include break-to-device(tablet portrait) { + + // Repository information container + .md-nav__source { + background-color: hsla(var(--md-hue), 15%, 11%, 0.87); + } + } + + // [tablet -]: Layered navigation + @include break-to-device(tablet) { + + // Site title in main navigation + html & .md-nav--primary .md-nav__title[for="__drawer"] { + background-color: hsla(var(--md-hue), 15%, 9%, 1); + } + } + + // [screen +]: Set background color for tabs + @include break-from-device(screen) { + + // Navigation tabs + .md-tabs { + background-color: hsla(var(--md-hue), 15%, 9%, 1); + } + } +} diff --git a/src/templates/assets/stylesheets/palette/_scheme.scss b/src/templates/assets/stylesheets/palette/_scheme.scss new file mode 100644 index 00000000..0a9f9823 --- /dev/null +++ b/src/templates/assets/stylesheets/palette/_scheme.scss @@ -0,0 +1,145 @@ +//// +/// 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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Only use dark mode on screens +@media screen { + + // Slate theme, i.e. dark mode + [data-md-color-scheme="slate"] { + + // Indicate that the site is rendered with a dark color scheme + color-scheme: dark; + + // Default color shades + --md-default-fg-color: hsla(var(--md-hue), 15%, 90%, 0.82); + --md-default-fg-color--light: hsla(var(--md-hue), 15%, 90%, 0.56); + --md-default-fg-color--lighter: hsla(var(--md-hue), 15%, 90%, 0.32); + --md-default-fg-color--lightest: hsla(var(--md-hue), 15%, 90%, 0.12); + --md-default-bg-color: hsla(var(--md-hue), 15%, 14%, 1); + --md-default-bg-color--light: hsla(var(--md-hue), 15%, 14%, 0.54); + --md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 14%, 0.26); + --md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 14%, 0.07); + + // Code color shades + --md-code-fg-color: hsla(var(--md-hue), 18%, 86%, 0.82); + --md-code-bg-color: hsla(var(--md-hue), 15%, 18%, 1); + + // Code highlighting color shades + --md-code-hl-color--light: hsla(#{hex2hsl($clr-blue-a200)}, 0.15); + --md-code-hl-number-color: hsla(6, 74%, 63%, 1); + --md-code-hl-special-color: hsla(340, 83%, 66%, 1); + --md-code-hl-function-color: hsla(291, 57%, 65%, 1); + --md-code-hl-constant-color: hsla(250, 62%, 70%, 1); + --md-code-hl-keyword-color: hsla(219, 66%, 64%, 1); + --md-code-hl-string-color: hsla(150, 58%, 44%, 1); + --md-code-hl-name-color: var(--md-code-fg-color); + --md-code-hl-operator-color: var(--md-default-fg-color--light); + --md-code-hl-punctuation-color: var(--md-default-fg-color--light); + --md-code-hl-comment-color: var(--md-default-fg-color--light); + --md-code-hl-generic-color: var(--md-default-fg-color--light); + --md-code-hl-variable-color: var(--md-default-fg-color--light); + + // Typeset color shades + --md-typeset-color: var(--md-default-fg-color); + + // Typeset `a` color shades + --md-typeset-a-color: var(--md-primary-fg-color); + + // Typeset `kbd` color shades + --md-typeset-kbd-color: hsla(var(--md-hue), 15%, 90%, 0.12); + --md-typeset-kbd-accent-color: hsla(var(--md-hue), 15%, 90%, 0.2); + --md-typeset-kbd-border-color: hsla(var(--md-hue), 15%, 14%, 1); + + // Typeset `mark` color shades + --md-typeset-mark-color: hsla(#{hex2hsl($clr-blue-a200)}, 0.3); + + // Typeset `table` color shades + --md-typeset-table-color: hsla(var(--md-hue), 15%, 95%, 0.12); + --md-typeset-table-color--light: hsla(var(--md-hue), 15%, 95%, 0.035); + + // Admonition color shades + --md-admonition-fg-color: var(--md-default-fg-color); + --md-admonition-bg-color: var(--md-default-bg-color); + + // Footer color shades + --md-footer-bg-color: hsla(var(--md-hue), 15%, 10%, 0.87); + --md-footer-bg-color--dark: hsla(var(--md-hue), 15%, 8%, 1); + + // Shadow depth 1 + --md-shadow-z1: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.05), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.1); + + // Shadow depth 2 + --md-shadow-z2: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.25), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.25); + + // Shadow depth 3 + --md-shadow-z3: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.4), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.35); + + // Hide images for light mode + img[src$="#only-light"], + img[src$="#gh-light-mode-only"] { + display: none; + } + } + + // -------------------------------------------------------------------------- + + // Adjust link colors for dark primary colors + @each $name, $color in ( + "pink": hsl(340, 81%, 63%), + "purple": hsl(291, 53%, 63%), + "deep-purple": hsl(262, 73%, 70%), + "indigo": hsl(219, 76%, 62%), + "teal": hsl(174, 100%, 40%), + "green": hsl(122, 39%, 60%), + "deep-orange": hsl(14, 100%, 65%), + "brown": hsl(16, 45%, 56%), + + // Set neutral colors to indigo + "grey": hsl(219, 66%, 62%), + "blue-grey": hsl(219, 66%, 62%), + "white": hsl(219, 66%, 62%), + "black": hsl(219, 66%, 62%) + ) { + [data-md-color-scheme="slate"][data-md-color-primary="#{$name}"] { + --md-typeset-a-color: #{$color}; + } + } + + // -------------------------------------------------------------------------- + + // Switching in progress - disable all transitions temporarily + [data-md-color-switching] *, + [data-md-color-switching] *::before, + [data-md-color-switching] *::after { + transition-duration: 0ms !important; // stylelint-disable-line + } +} diff --git a/src/templates/assets/stylesheets/utilities/_break.scss b/src/templates/assets/stylesheets/utilities/_break.scss new file mode 100644 index 00000000..7ccd8622 --- /dev/null +++ b/src/templates/assets/stylesheets/utilities/_break.scss @@ -0,0 +1,219 @@ +//// +/// 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 +//// + +@use "sass:list"; +@use "sass:map"; +@use "sass:math"; + +// ---------------------------------------------------------------------------- +// Variables +// ---------------------------------------------------------------------------- + +/// +/// Device-specific breakpoints +/// +/// @example +/// $break-devices: ( +/// mobile: ( +/// portrait: 220px 479px, +/// landscape: 480px 719px +/// ), +/// tablet: ( +/// portrait: 720px 959px, +/// landscape: 960px 1219px +/// ), +/// screen: ( +/// small: 1220px 1599px, +/// medium: 1600px 1999px, +/// large: 2000px +/// ) +/// ); +/// +$break-devices: () !default; + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- + +/// +/// Choose minimum and maximum device widths +/// +@function break-select-min-max($devices) { + $min: 1000000; + $max: 0; + @each $key, $value in $devices { + @while type-of($value) == map { + $value: break-select-min-max($value); + } + @if type-of($value) == list { + @each $number in $value { + @if type-of($number) == number { + $min: math.min($number, $min); + @if $max { + $max: math.max($number, $max); + } + } @else { + @error "Invalid number: #{$number}"; + } + } + } @else if type-of($value) == number { + $min: math.min($value, $min); + $max: null; + } @else { + @error "Invalid value: #{$value}"; + } + } + @return $min, $max; +} + +/// +/// Select minimum and maximum widths for a device breakpoint +/// +@function break-select-device($device) { + $current: $break-devices; + @for $n from 1 through length($device) { + @if type-of($current) == map { + $current: map.get($current, list.nth($device, $n)); + } @else { + @error "Invalid device map: #{$devices}"; + } + } + @if type-of($current) == list or type-of($current) == number { + $current: (default: $current); + } + @return break-select-min-max($current); +} + +// ---------------------------------------------------------------------------- +// Mixins +// ---------------------------------------------------------------------------- + +/// +/// A minimum-maximum media query breakpoint +/// +@mixin break-at($breakpoint) { + @if type-of($breakpoint) == number { + @media screen and (min-width: $breakpoint) { + @content; + } + } @else if type-of($breakpoint) == list { + $min: list.nth($breakpoint, 1); + $max: list.nth($breakpoint, 2); + @if type-of($min) == number and type-of($max) == number { + @media screen and (min-width: $min) and (max-width: $max) { + @content; + } + } @else { + @error "Invalid breakpoint: #{$breakpoint}"; + } + } @else { + @error "Invalid breakpoint: #{$breakpoint}"; + } +} + +/// +/// An orientation media query breakpoint +/// +@mixin break-at-orientation($breakpoint) { + @if type-of($breakpoint) == string { + @media screen and (orientation: $breakpoint) { + @content; + } + } @else { + @error "Invalid breakpoint: #{$breakpoint}"; + } +} + +/// +/// A maximum-aspect-ratio media query breakpoint +/// +@mixin break-at-ratio($breakpoint) { + @if type-of($breakpoint) == number { + @media screen and (max-aspect-ratio: $breakpoint) { + @content; + } + } @else { + @error "Invalid breakpoint: #{$breakpoint}"; + } +} + +/// +/// A minimum-maximum media query device breakpoint +/// +@mixin break-at-device($device) { + @if type-of($device) == string { + $device: $device,; + } + @if type-of($device) == list { + $breakpoint: break-select-device($device); + @if list.nth($breakpoint, 2) { + $min: list.nth($breakpoint, 1); + $max: list.nth($breakpoint, 2); + + @media screen and (min-width: $min) and (max-width: $max) { + @content; + } + } @else { + @error "Invalid device: #{$device}"; + } + } @else { + @error "Invalid device: #{$device}"; + } +} + +/// +/// A minimum media query device breakpoint +/// +@mixin break-from-device($device) { + @if type-of($device) == string { + $device: $device,; + } + @if type-of($device) == list { + $breakpoint: break-select-device($device); + $min: list.nth($breakpoint, 1); + + @media screen and (min-width: $min) { + @content; + } + } @else { + @error "Invalid device: #{$device}"; + } +} + +/// +/// A maximum media query device breakpoint +/// +@mixin break-to-device($device) { + @if type-of($device) == string { + $device: $device,; + } + @if type-of($device) == list { + $breakpoint: break-select-device($device); + $max: list.nth($breakpoint, 2); + + @media screen and (max-width: $max) { + @content; + } + } @else { + @error "Invalid device: #{$device}"; + } +} diff --git a/src/templates/assets/stylesheets/utilities/_convert.scss b/src/templates/assets/stylesheets/utilities/_convert.scss new file mode 100644 index 00000000..8199c9c8 --- /dev/null +++ b/src/templates/assets/stylesheets/utilities/_convert.scss @@ -0,0 +1,79 @@ +//// +/// 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 +//// + +@use "sass:math"; + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- + +/// +/// Strip units from a number +/// +@function strip-units($number) { + @return math.div($number, ($number * 0 + 1)); +} + +/// +/// Convert color in HEX to HSL +/// +/// Note, that we need to strip the `deg` units from the `hue` value, as they +/// were added in Color Level 4, which not all browsers support. +/// +@function hex2hsl($color) { + @return + round(strip-units(hue($color))), + round(saturation($color)), + round(lightness($color)); +} + +// ---------------------------------------------------------------------------- + +/// +/// Convert font size in px to em +/// +@function px2em($size, $base: 16px) { + @if unit($size) == px { + @if unit($base) == px { + @return math.div($size, $base) * 1em; + } @else { + @error "Invalid base: #{$base} - unit must be 'px'"; + } + } @else { + @error "Invalid size: #{$size} - unit must be 'px'"; + } +} + +/// +/// Convert font size in px to rem +/// +@function px2rem($size, $base: 20px) { + @if unit($size) == px { + @if unit($base) == px { + @return math.div($size, $base) * 1rem; + } @else { + @error "Invalid base: #{$base} - unit must be 'px'"; + } + } @else { + @error "Invalid size: #{$size} - unit must be 'px'"; + } +} diff --git a/src/templates/base.html b/src/templates/base.html new file mode 100644 index 00000000..8323e76e --- /dev/null +++ b/src/templates/base.html @@ -0,0 +1,445 @@ +<!-- + 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 "partials/language.html" as lang with context %} + +<!doctype html> +<html lang="{{ lang.t('language') }}" class="no-js"> + <head> + + <!-- Meta tags --> + {% block site_meta %} + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width,initial-scale=1" /> + + <!-- Page description --> + {% if page.meta and page.meta.description %} + <meta name="description" content="{{ page.meta.description }}" /> + {% elif config.site_description %} + <meta name="description" content="{{ config.site_description }}" /> + {% endif %} + + <!-- Page author --> + {% if page.meta and page.meta.author %} + <meta name="author" content="{{ page.meta.author }}" /> + {% elif config.site_author %} + <meta name="author" content="{{ config.site_author }}" /> + {% endif %} + + <!-- Canonical --> + {% if page.canonical_url %} + <link rel="canonical" href="{{ page.canonical_url }}" /> + {% endif %} + + <!-- Previous page --> + {% if page.previous_page %} + <link rel="prev" href="{{ page.previous_page.url | url }}" /> + {% endif %} + + <!-- Next page --> + {% if page.next_page %} + <link rel="next" href="{{ page.next_page.url | url }}" /> + {% endif %} + + <!-- RSS feed --> + {% if "rss" in config.plugins %} + <link + rel="alternate" + type="application/rss+xml" + title="{{ lang.t('rss.created') }}" + href="{{ 'feed_rss_created.xml' | url }}" + /> + <link + rel="alternate" + type="application/rss+xml" + title="{{ lang.t('rss.updated') }}" + href="{{ 'feed_rss_updated.xml' | url }}" + /> + {% endif %} + + <!-- Favicon --> + <link rel="icon" href="{{ config.theme.favicon | url }}" /> + + <!-- Generator banner --> + <meta + name="generator" + content="mkdocs-{{ mkdocs_version }}, $md-name$-$md-version$" + /> + {% endblock %} + + <!-- Site title --> + {% block htmltitle %} + {% if page.meta and page.meta.title %} + <title>{{ page.meta.title }} - {{ config.site_name }}</title> + {% elif page.title and not page.is_homepage %} + <title>{{ page.title | striptags }} - {{ config.site_name }}</title> + {% else %} + <title>{{ config.site_name }}</title> + {% endif %} + {% endblock %} + + <!-- Theme-related style sheets --> + {% block styles %} + <link rel="stylesheet" href="{{ 'assets/stylesheets/main.css' | url }}" /> + + <!-- Extra color palette --> + {% if config.theme.palette %} + {% set palette = config.theme.palette %} + <link + rel="stylesheet" + href="{{ 'assets/stylesheets/palette.css' | url }}" + /> + {% endif %} + + <!-- Custom icons --> + {% include "partials/icons.html" %} + {% endblock %} + + <!-- JavaScript libraries --> + {% block libs %} + {% for script in config.extra.polyfills %} + {{ script | script_tag }} + {% endfor %} + {% endblock %} + + <!-- Webfonts --> + {% block fonts %} + + <!-- Load fonts from Google --> + {% if config.theme.font != false %} + {% set text = config.theme.font.get("text", "Roboto") %} + {% set code = config.theme.font.get("code", "Roboto Mono") %} + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> + <link + rel="stylesheet" + href="https://fonts.googleapis.com/css?family={{ + text | replace(' ', '+') + ':300,300i,400,400i,700,700i%7C' + + code | replace(' ', '+') + ':400,400i,700,700i' + }}&display=fallback" + /> + <style> + :root { + --md-text-font: "{{ text }}"; + --md-code-font: "{{ code }}"; + } + </style> + {% endif %} + {% endblock %} + + <!-- Custom style sheets --> + {% for path in config.extra_css %} + <link rel="stylesheet" href="{{ path | url }}" /> + {% endfor %} + + <!-- Helper functions for inline scripts --> + {% include "partials/javascripts/base.html" %} + + <!-- Analytics --> + {% block analytics %} + {% include "partials/integrations/analytics.html" %} + {% endblock %} + + <!-- Meta tags from front matter or plugins --> + {% if page.meta and page.meta.meta %} + {% for tag in page.meta.meta %} + <meta + {% for key, value in tag.items() %} + {{ key }}="{{value}}" + {% endfor %} + /> + {% endfor %} + {% endif %} + + <!-- Custom front matter --> + {% block extrahead %}{% endblock %} + </head> + + <!-- Set text direction and color palette, if defined --> + {% set direction = config.theme.direction or lang.t("direction") %} + {% if config.theme.palette %} + {% set palette = config.theme.palette %} + {% if not palette is mapping %} + {% set palette = palette | first %} + {% endif %} + {% set scheme = palette.scheme | d("default", true) %} + {% set primary = palette.primary | d("indigo", true) %} + {% set accent = palette.accent | d("indigo", true) %} + <body + dir="{{ direction }}" + data-md-color-scheme="{{ scheme | replace(' ', '-') }}" + data-md-color-primary="{{ primary | replace(' ', '-') }}" + data-md-color-accent="{{ accent | replace(' ', '-') }}" + > + {% else %} + <body dir="{{ direction }}"> + {% endif %} + {% set features = config.theme.features or [] %} + + <!-- User preference: color palette --> + {% if not config.theme.palette is mapping %} + {% include "partials/javascripts/palette.html" %} + {% endif %} + + <!-- + State toggles - we need to set autocomplete="off" in order to reset the + drawer on back button invocation in some browsers + --> + <input + class="md-toggle" + data-md-toggle="drawer" + type="checkbox" + id="__drawer" + autocomplete="off" + /> + <input + class="md-toggle" + data-md-toggle="search" + type="checkbox" + id="__search" + autocomplete="off" + /> + + <!-- Overlay for expanded drawer --> + <label class="md-overlay" for="__drawer"></label> + + <!-- Skip to content --> + <div data-md-component="skip"> + {% if page.toc | first is defined %} + {% set skip = page.toc | first %} + <a href="{{ skip.url | url }}" class="md-skip"> + {{ lang.t("action.skip") }} + </a> + {% endif %} + </div> + + <!-- Announcement bar --> + <div data-md-component="announce"> + {% if self.announce() %} + <aside class="md-banner"> + <div class="md-banner__inner md-grid md-typeset"> + + <!-- Button to dismiss announcement --> + {% if "announce.dismiss" in features %} + <button + class="md-banner__button md-icon" + aria-label="{{ lang.t('announce.dismiss') }}" + > + {% set icon = config.theme.icon.close or "material/close" %} + {% include ".icons/" ~ icon ~ ".svg" %} + </button> + {% endif %} + + <!-- Announcement bar content --> + {% block announce %}{% endblock %} + </div> + {% if "announce.dismiss" in features %} + {% include "partials/javascripts/announce.html" %} + {% endif %} + </aside> + {% endif %} + </div> + + <!-- Version warning --> + {% if config.extra.version %} + <div data-md-color-scheme="default" data-md-component="outdated" hidden> + {% if self.outdated() %} + <aside class="md-banner md-banner--warning"> + <div class="md-banner__inner md-grid md-typeset"> + {% block outdated %}{% endblock %} + </div> + {% include "partials/javascripts/outdated.html" %} + </aside> + {% endif %} + </div> + {% endif %} + + <!-- Header --> + {% block header %} + {% include "partials/header.html" %} + {% endblock %} + + <!-- Container --> + <div class="md-container" data-md-component="container"> + + <!-- Hero teaser --> + {% block hero %}{% endblock %} + + <!-- Navigation tabs (collapsing) --> + {% block tabs %} + {% if "navigation.tabs.sticky" not in features %} + {% if "navigation.tabs" in features %} + {% include "partials/tabs.html" %} + {% endif %} + {% endif %} + {% endblock %} + + <!-- Main area --> + <main class="md-main" data-md-component="main"> + <div class="md-main__inner md-grid"> + + <!-- Sidebars --> + {% block site_nav %} + + <!-- Navigation --> + {% if nav %} + {% if page.meta and page.meta.hide %} + {% set hidden = "hidden" if "navigation" in page.meta.hide %} + {% endif %} + <div + class="md-sidebar md-sidebar--primary" + data-md-component="sidebar" + data-md-type="navigation" + {{ hidden }} + > + <div class="md-sidebar__scrollwrap"> + <div class="md-sidebar__inner"> + {% include "partials/nav.html" %} + </div> + </div> + </div> + {% endif %} + + <!-- Table of contents --> + {% if "toc.integrate" not in features %} + {% if page.meta and page.meta.hide %} + {% set hidden = "hidden" if "toc" in page.meta.hide %} + {% endif %} + <div + class="md-sidebar md-sidebar--secondary" + data-md-component="sidebar" + data-md-type="toc" + {{ hidden }} + > + <div class="md-sidebar__scrollwrap"> + <div class="md-sidebar__inner"> + {% include "partials/toc.html" %} + </div> + </div> + </div> + {% endif %} + {% endblock %} + + <!-- Page content --> + {% block container %} + <div class="md-content" data-md-component="content"> + <article class="md-content__inner md-typeset"> + {% block content %} + {% include "partials/content.html" %} + {% endblock %} + </article> + </div> + {% endblock %} + + <!-- User preference: content --> + {% include "partials/javascripts/content.html" %} + </div> + + <!-- Back-to-top button --> + {% if "navigation.top" in features %} + {% include "partials/top.html" %} + {% endif %} + </main> + + <!-- Footer --> + {% block footer %} + {% include "partials/footer.html" %} + {% endblock %} + </div> + + <!-- Dialog --> + <div class="md-dialog" data-md-component="dialog"> + <div class="md-dialog__inner md-typeset"></div> + </div> + + <!-- Progress indicator --> + {% if "navigation.instant.progress" in features %} + {% include "partials/progress.html" %} + {% endif %} + + <!-- Consent --> + {% if config.extra.consent %} + <div class="md-consent" data-md-component="consent" id="__consent" hidden> + <div class="md-consent__overlay"></div> + <aside class="md-consent__inner"> + <form class="md-consent__form md-grid md-typeset" name="consent"> + {% include "partials/consent.html" %} + </form> + </aside> + </div> + + <!-- User preference: consent --> + {% include "partials/javascripts/consent.html" %} + {% endif %} + + <!-- Theme-related configuration --> + {% block config %} + {%- set app = { + "base": base_url, + "features": features, + "translations": {}, + "search": "assets/javascripts/workers/search.js" | url + } -%} + + <!-- Versioning --> + {%- if config.extra.version -%} + {%- set _ = app.update({ "version": config.extra.version }) -%} + {%- endif -%} + + <!-- Tags --> + {%- if config.extra.tags -%} + {%- set _ = app.update({ "tags": config.extra.tags }) -%} + {%- endif -%} + + <!-- Translations --> + {%- set translations = app.translations -%} + {%- for key in [ + "clipboard.copy", + "clipboard.copied", + "search.result.placeholder", + "search.result.none", + "search.result.one", + "search.result.other", + "search.result.more.one", + "search.result.more.other", + "search.result.term.missing", + "select.version" + ] -%} + {%- set _ = translations.update({ key: lang.t(key) }) -%} + {%- endfor -%} + + <!-- Configuration --> + <script id="__config" type="application/json"> + {{- app | tojson -}} + </script> + {% endblock %} + + <!-- Theme-related JavaScript --> + {% block scripts %} + <script src="{{ 'assets/javascripts/bundle.js' | url }}"></script> + + <!-- Custom JavaScript --> + {% for script in config.extra_javascript %} + {{ script | script_tag }} + {% endfor %} + {% endblock %} + </body> +</html> diff --git a/src/templates/blog-post.html b/src/templates/blog-post.html new file mode 100644 index 00000000..73fb669f --- /dev/null +++ b/src/templates/blog-post.html @@ -0,0 +1,164 @@ +<!-- + 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. +--> + +{% extends "main.html" %} + +{% import "partials/nav-item.html" as item with context %} + +<!-- Page content --> +{% block container %} + <div class="md-content md-content--post" data-md-component="content"> + + <!-- Sidebar --> + <div + class="md-sidebar md-sidebar--post" + data-md-component="sidebar" + data-md-type="navigation" + > + <div class="md-sidebar__scrollwrap"> + <div class="md-sidebar__inner md-post"> + <nav class="md-nav md-nav--primary"> + + <!-- Back to overview link --> + <div class="md-post__back"> + <div class="md-nav__title md-nav__container"> + <a href="{{ page.parent.url | url }}" class=" md-nav__link"> + {% include ".icons/material/arrow-left.svg" %} + <span class="md-ellipsis"> + {{ lang.t("blog.index") }} + </span> + </a> + </div> + </div> + + <!-- Post authors --> + {% if page.authors %} + <div class="md-post__authors md-typeset"> + {% for author in page.authors %} + <div class="md-profile md-post__profile"> + <span class="md-author md-author--long"> + <img src="{{ author.avatar }}" alt="{{ author.name }}" /> + </span> + <span class="md-profile__description"> + <strong>{{ author.name }}</strong><br /> + {{ author.description }} + </span> + </div> + {% endfor %} + </div> + {% endif %} + + <!-- Post metadata --> + <ul class="md-post__meta md-nav__list"> + <li class="md-nav__item md-nav__item--section"> + <div class="md-post__title"> + <span class="md-ellipsis"> + {{ lang.t("blog.meta") }} + </span> + </div> + <nav class="md-nav"> + <ul class="md-nav__list"> + + <!-- Post date --> + <li class="md-nav__item"> + <div class="md-nav__link"> + {% include ".icons/material/calendar.svg" %} + <time + datetime="{{ page.config.date.created }}" + class="md-ellipsis" + > + {{- page.config.date.created | date -}} + </time> + </div> + </li> + + <!-- Post date updated --> + {% if page.config.date.updated %} + <li class="md-nav__item"> + <div class="md-nav__link"> + {% include ".icons/material/calendar-clock.svg" %} + <time + datetime="{{ page.config.date.updated }}" + class="md-ellipsis" + > + {{- page.config.date.updated | date -}} + </time> + </div> + </li> + {% endif %} + + <!-- Post categories --> + {% if page.categories %} + <li class="md-nav__item"> + <div class="md-nav__link"> + {% include ".icons/material/bookshelf.svg" %} + <span class="md-ellipsis"> + {{ lang.t("blog.categories.in") }} + {% for category in page.categories %} + <a href="{{ category.url | url }}"> + {{- category.title -}} + </a> + {%- if loop.revindex > 1 %}, {% endif -%} + {% endfor -%} + </span> + </div> + </li> + {% endif %} + + <!-- Post readtime --> + {% if page.config.readtime %} + {% set time = page.config.readtime %} + <li class="md-nav__item"> + <div class="md-nav__link"> + {% include ".icons/material/clock-outline.svg" %} + <span class="md-ellipsis"> + {% if time == 1 %} + {{ lang.t("readtime.one") }} + {% else %} + {{ lang.t("readtime.other") | replace("#", time) }} + {% endif %} + </span> + </div> + </li> + {% endif %} + </ul> + </nav> + </li> + </ul> + </nav> + + <!-- Table of contents, if integrated --> + {% if "toc.integrate" in features %} + {% include "partials/toc.html" %} + {% endif %} + </div> + </div> + </div> + + <!-- Page content --> + <article class="md-content__inner md-typeset"> + {% block content %} + {% include "partials/content.html" %} + {% endblock %} + </article> + </div> +{% endblock %} diff --git a/src/templates/blog.html b/src/templates/blog.html new file mode 100644 index 00000000..eedc77d9 --- /dev/null +++ b/src/templates/blog.html @@ -0,0 +1,48 @@ +<!-- + 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. +--> + +{% extends "main.html" %} + +<!-- Page content --> +{% block container %} + <div class="md-content" data-md-component="content"> + <div class="md-content__inner"> + + <!-- Header --> + <header class="md-typeset"> + {{ page.content }} + </header> + + <!-- Posts --> + {% for post in posts %} + {% include "partials/post.html" %} + {% endfor %} + + <!-- Pagination --> + {% if pagination %} + {% block pagination %} + {% include "partials/pagination.html" %} + {% endblock %} + {% endif %} + </div> + </div> +{% endblock %} diff --git a/src/templates/main.html b/src/templates/main.html new file mode 100644 index 00000000..3b77d200 --- /dev/null +++ b/src/templates/main.html @@ -0,0 +1,23 @@ +<!-- + 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. +--> + +{% extends "base.html" %} diff --git a/src/templates/mkdocs_theme.yml b/src/templates/mkdocs_theme.yml new file mode 100644 index 00000000..aaa47f5e --- /dev/null +++ b/src/templates/mkdocs_theme.yml @@ -0,0 +1,50 @@ +# 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. + +# Language for theme localization +language: en + +# Text direction (can be ltr or rtl), default: ltr +direction: + +# Feature flags for functionality that alters behavior significantly, and thus +# may be a matter of taste +features: [] + +# Fonts used by Material, automatically loaded from Google Fonts - see the site +# for a list of available fonts +font: + + # Default font for text + text: Roboto + + # Fixed-width font for code listings + code: Roboto Mono + +# From Material 5.x on, icons are inlined into the HTML and CSS as SVGs. +# Icons that are part of the HTML can be configured and replaced +icon: + +# Favicon to be rendered +favicon: assets/images/favicon.png + +# Static pages to build +static_templates: + - 404.html diff --git a/src/templates/partials/actions.html b/src/templates/partials/actions.html new file mode 100644 index 00000000..75fcb8eb --- /dev/null +++ b/src/templates/partials/actions.html @@ -0,0 +1,54 @@ +<!-- + 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. +--> + +<!-- Actions --> +{% if page.edit_url %} + + <!-- Edit button --> + {% if "content.action.edit" in features %} + <a + href="{{ page.edit_url }}" + title="{{ lang.t('action.edit') }}" + class="md-content__button md-icon" + > + {% set icon = config.theme.icon.edit or "material/file-edit-outline" %} + {% include ".icons/" ~ icon ~ ".svg" %} + </a> + {% endif %} + + <!-- View button --> + {% if "content.action.view" in features %} + {% if "/blob/" in page.edit_url %} + {% set part = "blob" %} + {% else %} + {% set part = "edit" %} + {% endif %} + <a + href="{{ page.edit_url | replace(part, 'raw') }}" + title="{{ lang.t('action.view') }}" + class="md-content__button md-icon" + > + {% set icon = config.theme.icon.view or "material/file-eye-outline" %} + {% include ".icons/" ~ icon ~ ".svg" %} + </a> + {% endif %} +{% endif %} diff --git a/src/templates/partials/alternate.html b/src/templates/partials/alternate.html new file mode 100644 index 00000000..7d7c925b --- /dev/null +++ b/src/templates/partials/alternate.html @@ -0,0 +1,49 @@ +<!-- + 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. +--> + +<!-- Site language selector --> +<div class="md-header__option"> + <div class="md-select"> + {% set icon = config.theme.icon.alternate or "material/translate" %} + <button + class="md-header__button md-icon" + aria-label="{{ lang.t('select.language') }}" + > + {% include ".icons/" ~ icon ~ ".svg" %} + </button> + <div class="md-select__inner"> + <ul class="md-select__list"> + {% for alt in config.extra.alternate %} + <li class="md-select__item"> + <a + href="{{ alt.link | url }}" + hreflang="{{ alt.lang }}" + class="md-select__link" + > + {{ alt.name }} + </a> + </li> + {% endfor %} + </ul> + </div> + </div> +</div> diff --git a/src/templates/partials/comments.html b/src/templates/partials/comments.html new file mode 100644 index 00000000..6641d20e --- /dev/null +++ b/src/templates/partials/comments.html @@ -0,0 +1,23 @@ +<!-- + 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. +--> + +<!-- Comment system --> diff --git a/src/templates/partials/consent.html b/src/templates/partials/consent.html new file mode 100644 index 00000000..c84622bc --- /dev/null +++ b/src/templates/partials/consent.html @@ -0,0 +1,107 @@ +<!-- + 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. +--> + +<!-- Determine cookies --> +{% set cookies = config.extra.consent.cookies | d({}) %} +{% if config.extra.analytics %} + {% if "analytics" not in cookies %} + {% set _ = cookies.update({ "analytics": "Google Analytics" }) %} + {% endif %} +{% endif %} +{% if config.repo_url and "github.com" in config.repo_url %} + {% if "github" not in cookies %} + {% set _ = cookies.update({ "github": "GitHub" }) %} + {% endif %} +{% endif %} + +<!-- Determine actions --> +{% set actions = config.extra.consent.actions %} +{% if not actions %} + {% set actions = ["accept", "manage"] %} +{% endif %} + +<!-- Determine initial settings state --> +{% if "manage" not in actions %} + {% set checked = "checked" %} +{% endif %} + +<!-- Consent title --> +<h4>{{ config.extra.consent.title }}</h4> +<p>{{ config.extra.consent.description }}</p> + +<!-- Consent settings --> +<input + class="md-toggle" + type="checkbox" + id="__settings" + {{ checked }} +/> +<div class="md-consent__settings"> + <ul class="task-list"> + {% for type in cookies %} + {% set checked = "" %} + {% if cookies[type] is string %} + {% set name = cookies[type] %} + {% set checked = "checked" %} + {% else %} + {% set name = cookies[type].name %} + {% if cookies[type].checked %} + {% set checked = "checked" %} + {% endif %} + {% endif %} + <li class="task-list-item"> + <label class="task-list-control"> + <input type="checkbox" name="{{ type }}" {{ checked }}> + <span class="task-list-indicator"></span> + {{ name }} + </label> + </li> + {% endfor %} + </ul> +</div> + +<!-- Consent controls --> +<div class="md-consent__controls"> + {% for action in actions %} + + <!-- Button to accept cookies --> + {% if action == "accept" %} + <button class="md-button md-button--primary"> + {{- lang.t("consent.accept") -}} + </button> + {% endif %} + + <!-- Button to reject cookies --> + {% if action == "reject" %} + <button type="reset" class="md-button md-button--primary"> + {{- lang.t("consent.reject") -}} + </button> + {% endif %} + + <!-- Button to manage settings --> + {% if action == "manage" %} + <label class="md-button" for="__settings"> + {{- lang.t("consent.manage") -}} + </label> + {% endif %} + {% endfor %} +</div> diff --git a/src/templates/partials/content.html b/src/templates/partials/content.html new file mode 100644 index 00000000..2b78b09b --- /dev/null +++ b/src/templates/partials/content.html @@ -0,0 +1,54 @@ +<!-- + 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. +--> + +<!-- Tags --> +{% if "material/tags" in config.plugins and tags %} + {% include "partials/tags.html" %} +{% endif %} + +<!-- Actions --> +{% include "partials/actions.html" %} + +<!-- + Hack: check whether the content contains a h1 headline. If it doesn't, the + page title (or respectively site name) is used as the main headline. +--> +{% if "\x3ch1" not in page.content %} + <h1>{{ page.title | d(config.site_name, true)}}</h1> +{% endif %} + +<!-- Page content --> +{{ page.content }} + +<!-- Source file information --> +{% if page.meta and ( + page.meta.git_revision_date_localized or + page.meta.revision_date +) %} + {% include "partials/source-file.html" %} +{% endif %} + +<!-- Was this page helpful? --> +{% include "partials/feedback.html" %} + +<!-- Comment system --> +{% include "partials/comments.html" %} diff --git a/src/templates/partials/copyright.html b/src/templates/partials/copyright.html new file mode 100644 index 00000000..070948d2 --- /dev/null +++ b/src/templates/partials/copyright.html @@ -0,0 +1,39 @@ +<!-- + 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. +--> + +<!-- Copyright and theme information --> +<div class="md-copyright"> + {% if config.copyright %} + <div class="md-copyright__highlight"> + {{ config.copyright }} + </div> + {% endif %} + {% if not config.extra.generator == false %} + Made with + <a + href="https://squidfunk.github.io/mkdocs-material/" + target="_blank" rel="noopener" + > + Material for MkDocs + </a> + {% endif %} +</div> diff --git a/src/templates/partials/feedback.html b/src/templates/partials/feedback.html new file mode 100644 index 00000000..bf27c640 --- /dev/null +++ b/src/templates/partials/feedback.html @@ -0,0 +1,79 @@ +<!-- + 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. +--> + +<!-- Determine feedback configuration --> +{% if config.extra.analytics %} + {% set feedback = config.extra.analytics.feedback %} +{% endif %} + +<!-- Determine whether to show feedback --> +{% if page.meta and page.meta.hide %} + {% if "feedback" in page.meta.hide %} + {% set feedback = None %} + {% endif %} +{% endif %} + +<!-- Was this page helpful? --> +{% if feedback %} + <form class="md-feedback" name="feedback" hidden> + <fieldset> + <legend class="md-feedback__title"> + {{ feedback.title }} + </legend> + <div class="md-feedback__inner"> + + <!-- Feedback ratings --> + <div class="md-feedback__list"> + {% for rating in feedback.ratings %} + <button + class="md-feedback__icon md-icon" + type="submit" + title="{{ rating.name }}" + data-md-value="{{ rating.data }}" + > + {% include ".icons/" ~ rating.icon ~ ".svg" %} + </button> + {% endfor %} + </div> + + <!-- Feedback rating notes (shown after submission) --> + <div class="md-feedback__note"> + {% for rating in feedback.ratings %} + <div data-md-value="{{ rating.data }}" hidden> + {% set url = "/" ~ page.url %} + + <!-- Determine title --> + {% if page.meta and page.meta.title %} + {% set title = page.meta.title | urlencode %} + {% else %} + {% set title = page.title | urlencode %} + {% endif %} + + <!-- Replace {url} and {title} placeholders in note --> + {{ rating.note.format(url = url, title = title) }} + </div> + {% endfor %} + </div> + </div> + </fieldset> + </form> +{% endif %} diff --git a/src/templates/partials/footer.html b/src/templates/partials/footer.html new file mode 100644 index 00000000..ebe9278f --- /dev/null +++ b/src/templates/partials/footer.html @@ -0,0 +1,98 @@ +<!-- + 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. +--> + +<!-- Footer --> +<footer class="md-footer"> + + <!-- Link to previous and/or next page --> + {% if "navigation.footer" in features %} + {% if page.previous_page or page.next_page %} + {% if page.meta and page.meta.hide %} + {% set hidden = "hidden" if "footer" in page.meta.hide %} + {% endif %} + <nav + class="md-footer__inner md-grid" + aria-label="{{ lang.t('footer') }}" + {{ hidden }} + > + + <!-- Link to previous page --> + {% if page.previous_page %} + {% set direction = lang.t("footer.previous") %} + <a + href="{{ page.previous_page.url | url }}" + class="md-footer__link md-footer__link--prev" + aria-label="{{ direction }}: {{ page.previous_page.title | e }}" + > + <div class="md-footer__button md-icon"> + {% set icon = config.theme.icon.previous or "material/arrow-left" %} + {% include ".icons/" ~ icon ~ ".svg" %} + </div> + <div class="md-footer__title"> + <span class="md-footer__direction"> + {{ direction }} + </span> + <div class="md-ellipsis"> + {{ page.previous_page.title }} + </div> + </div> + </a> + {% endif %} + + <!-- Link to next page --> + {% if page.next_page %} + {% set direction = lang.t("footer.next") %} + <a + href="{{ page.next_page.url | url }}" + class="md-footer__link md-footer__link--next" + aria-label="{{ direction }}: {{ page.next_page.title | e }}" + > + <div class="md-footer__title"> + <span class="md-footer__direction"> + {{ direction }} + </span> + <div class="md-ellipsis"> + {{ page.next_page.title }} + </div> + </div> + <div class="md-footer__button md-icon"> + {% set icon = config.theme.icon.next or "material/arrow-right" %} + {% include ".icons/" ~ icon ~ ".svg" %} + </div> + </a> + {% endif %} + </nav> + {% endif %} + {% endif %} + + <!-- Further information --> + <div class="md-footer-meta md-typeset"> + <div class="md-footer-meta__inner md-grid"> + {% include "partials/copyright.html" %} + + <!-- Social links --> + {% if config.extra.social %} + {% include "partials/social.html" %} + {% endif %} + </div> + </div> +</footer> diff --git a/src/templates/partials/header.html b/src/templates/partials/header.html new file mode 100644 index 00000000..9b6d2e2e --- /dev/null +++ b/src/templates/partials/header.html @@ -0,0 +1,112 @@ +<!-- + 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. +--> + +<!-- Determine classes --> +{% set class = "md-header" %} +{% if "navigation.tabs.sticky" in features %} + {% set class = class ~ " md-header--shadow md-header--lifted" %} +{% elif "navigation.tabs" not in features %} + {% set class = class ~ " md-header--shadow" %} +{% endif %} + +<!-- Header --> +<header class="{{ class }}" data-md-component="header"> + <nav + class="md-header__inner md-grid" + aria-label="{{ lang.t('header') }}" + > + + <!-- Link to home --> + <a + href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}" + title="{{ config.site_name | e }}" + class="md-header__button md-logo" + aria-label="{{ config.site_name }}" + data-md-component="logo" + > + {% include "partials/logo.html" %} + </a> + + <!-- Button to open drawer --> + <label class="md-header__button md-icon" for="__drawer"> + {% set icon = config.theme.icon.menu or "material/menu" %} + {% include ".icons/" ~ icon ~ ".svg" %} + </label> + + <!-- Header title --> + <div class="md-header__title" data-md-component="header-title"> + <div class="md-header__ellipsis"> + <div class="md-header__topic"> + <span class="md-ellipsis"> + {{ config.site_name }} + </span> + </div> + <div class="md-header__topic" data-md-component="header-topic"> + <span class="md-ellipsis"> + {% if page.meta and page.meta.title %} + {{ page.meta.title }} + {% else %} + {{ page.title }} + {% endif %} + </span> + </div> + </div> + </div> + + <!-- Color palette toggle --> + {% if config.theme.palette %} + {% if not config.theme.palette is mapping %} + {% include "partials/palette.html" %} + {% endif %} + {% endif %} + + <!-- Site language selector --> + {% if config.extra.alternate %} + {% include "partials/alternate.html" %} + {% endif %} + + <!-- Button to open search modal --> + {% if "material/search" in config.plugins %} + <label class="md-header__button md-icon" for="__search"> + {% set icon = config.theme.icon.search or "material/magnify" %} + {% include ".icons/" ~ icon ~ ".svg" %} + </label> + + <!-- Search interface --> + {% include "partials/search.html" %} + {% endif %} + + <!-- Repository information --> + {% if config.repo_url %} + <div class="md-header__source"> + {% include "partials/source.html" %} + </div> + {% endif %} + </nav> + + <!-- Navigation tabs (sticky) --> + {% if "navigation.tabs.sticky" in features %} + {% if "navigation.tabs" in features %} + {% include "partials/tabs.html" %} + {% endif %} + {% endif %} +</header> diff --git a/src/templates/partials/icons.html b/src/templates/partials/icons.html new file mode 100644 index 00000000..17dd20d8 --- /dev/null +++ b/src/templates/partials/icons.html @@ -0,0 +1,72 @@ +<!-- + 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. +--> + +<!-- Custom admonition icons --> +{% if config.theme.icon.admonition %} + {% set style = ["\x3cstyle\x3e:root{"] %} + {% for type, icon in config.theme.icon.admonition.items() %} + {% import ".icons/" ~ icon ~ ".svg" as icon %} + {% set _ = style.append( + "--md-admonition-icon--" ~ type ~ ":" ~ + "url('data:image/svg+xml;charset=utf-8," ~ + icon | replace("\n", "") ~ + "');" + ) %} + {% endfor %} + {% set _ = style.append("}\x3c/style\x3e") %} + {{ style | join }} +{% endif %} + +<!-- Custom annotation icon --> +{% if config.theme.icon.annotation %} + {% set style = ["\x3cstyle\x3e:root{"] %} + {% import ".icons/" ~ config.theme.icon.annotation ~ ".svg" as icon %} + {% set _ = style.append( + "--md-annotation-icon:" ~ + "url('data:image/svg+xml;charset=utf-8," ~ + icon | replace("\n", "") ~ + "');" + ) %} + {% set _ = style.append("}\x3c/style\x3e") %} + {{ style | join }} +{% endif %} + +<!-- Custom tag icons --> +{% if config.theme.icon.tag %} + {% set style = ["\x3cstyle\x3e"] %} + {% for type, icon in config.theme.icon.tag.items() %} + {% import ".icons/" ~ icon ~ ".svg" as icon %} + {% if type != "default" %} + {% set modifier = "--" ~ type %} + {% endif %} + {% set _ = style.append( + ".md-tag" ~ modifier ~ "{" ~ + "--md-tag-icon:" ~ + "url('data:image/svg+xml;charset=utf-8," ~ + icon | replace("\n", "") ~ + "');" ~ + "}" + ) %} + {% endfor %} + {% set _ = style.append("\x3c/style\x3e") %} + {{ style | join }} +{% endif %} diff --git a/src/templates/partials/integrations/analytics.html b/src/templates/partials/integrations/analytics.html new file mode 100644 index 00000000..4b483046 --- /dev/null +++ b/src/templates/partials/integrations/analytics.html @@ -0,0 +1,49 @@ +<!-- + 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. +--> + +<!-- Determine analytics provider --> +{% if config.extra.analytics %} + {% set provider = config.extra.analytics.provider %} +{% endif %} + +<!-- Set up analytics provider --> +{% if provider %} + {% include "partials/integrations/analytics/" ~ provider ~ ".html" %} + + <!-- Consent necessary --> + {% if config.extra.consent %} + <script> + if (typeof __md_analytics !== "undefined") { + var consent = __md_get("__consent") + if (consent && consent.analytics) + __md_analytics() + } + </script> + + <!-- Consent unnecessary --> + {% else %} + <script> + if (typeof __md_analytics !== "undefined") + __md_analytics() + </script> + {% endif %} +{% endif %} diff --git a/src/templates/partials/integrations/analytics/google.html b/src/templates/partials/integrations/analytics/google.html new file mode 100644 index 00000000..a9fa37d9 --- /dev/null +++ b/src/templates/partials/integrations/analytics/google.html @@ -0,0 +1,97 @@ +<!-- + 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. +--> + +<!-- Determine analytics property --> +{% if config.extra.analytics %} + {% set property = config.extra.analytics.property | d("", true) %} +{% endif %} + +<!-- Integrate with Google Analytics 4 --> +<script id="__analytics"> + function __md_analytics() { + window.dataLayer = window.dataLayer || [] + function gtag() { dataLayer.push(arguments) } + + /* Set up integration and send page view */ + gtag("js", new Date()) + gtag("config", "{{ property }}") + + /* Register event handlers after documented loaded */ + document.addEventListener("DOMContentLoaded", function() { + + /* Set up search tracking */ + if (document.forms.search) { + var query = document.forms.search.query + query.addEventListener("blur", function() { + if (this.value) + gtag("event", "search", { search_term: this.value }) + }) + } + + /* Set up feedback, i.e. "Was this page helpful?" */ + document$.subscribe(function() { + var feedback = document.forms.feedback + if (typeof feedback === "undefined") + return + + /* Send feedback to Google Analytics */ + for (var button of feedback.querySelectorAll("[type=submit]")) { + button.addEventListener("click", function(ev) { + ev.preventDefault() + + /* Retrieve and send data */ + var page = document.location.pathname + var data = this.getAttribute("data-md-value") + gtag("event", "feedback", { page, data }) + + /* Disable form and show note, if given */ + feedback.firstElementChild.disabled = true + var note = feedback.querySelector( + ".md-feedback__note [data-md-value='" + data + "']" + ) + if (note) + note.hidden = false + }) + + /* Show feedback */ + feedback.hidden = false + } + }) + + /* Send page view on location change */ + location$.subscribe(function(url) { + gtag("config", "{{ property }}", { + page_path: url.pathname + }) + }) + }) + + /* Create script tag */ + var script = document.createElement("script") + script.async = true + script.src = "https://www.googletagmanager.com/gtag/js?id={{ property }}" + + /* Inject script tag */ + var container = document.getElementById("__analytics") + container.insertAdjacentElement("afterEnd", script) + } +</script> diff --git a/src/templates/partials/javascripts/announce.html b/src/templates/partials/javascripts/announce.html new file mode 100644 index 00000000..f13961b2 --- /dev/null +++ b/src/templates/partials/javascripts/announce.html @@ -0,0 +1,31 @@ +<!-- + 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. +--> + +<!-- Announcement bar --> +<script> + var el = document.querySelector("[data-md-component=announce]") + if (el) { + var content = el.querySelector(".md-typeset") + if (__md_hash(content.innerHTML) === __md_get("__announce")) + el.hidden = true + } +</script> diff --git a/src/templates/partials/javascripts/base.html b/src/templates/partials/javascripts/base.html new file mode 100644 index 00000000..f0eeeb8a --- /dev/null +++ b/src/templates/partials/javascripts/base.html @@ -0,0 +1,48 @@ +<!-- + 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. +--> + +<!-- + A collection of functions used from within some partials to allow the usage + of state saved in local or session storage, e.g. to model preferences. +--> +<script> + + /* Compute base path once to integrate with instant loading */ + __md_scope = new URL("{{ config.extra.scope | d(base_url) }}", location) + + /* Compute hash from the given string - see https://bit.ly/3pvPjXG */ + __md_hash = v => [...v].reduce((h, c) => (h << 5) - h + c.charCodeAt(0), 0) + + /* Fetch the value for a key from the given storage */ + __md_get = (key, storage = localStorage, scope = __md_scope) => ( + JSON.parse(storage.getItem(scope.pathname + "." + key)) + ) + + /* Persist a key-value pair in the given storage */ + __md_set = (key, value, storage = localStorage, scope = __md_scope) => { + try { + storage.setItem(scope.pathname + "." + key, JSON.stringify(value)) + } catch (err) { + /* Uncritical, just swallow */ + } + } +</script> diff --git a/src/templates/partials/javascripts/consent.html b/src/templates/partials/javascripts/consent.html new file mode 100644 index 00000000..13730da7 --- /dev/null +++ b/src/templates/partials/javascripts/consent.html @@ -0,0 +1,61 @@ +<!-- + 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. +--> + +<!-- User-preference: consent --> +<script> + var consent = __md_get("__consent") + if (consent) { + for (var input of document.forms.consent.elements) + if (input.name) + input.checked = consent[input.name] || false + + /* Show consent with a small delay, but not if browsing locally */ + } else if (location.protocol !== "file:") { + setTimeout(function() { + var el = document.querySelector("[data-md-component=consent]") + el.hidden = false + }, 250) + } + + /* Intercept submission of consent form */ + var form = document.forms.consent + for (var action of ["submit", "reset"]) + form.addEventListener(action, function(ev) { + ev.preventDefault() + + /* Reject all cookies */ + if (ev.type === "reset") + for (var input of document.forms.consent.elements) + if (input.name) + input.checked = false + + /* Grab and serialize form data */ + __md_set("__consent", Object.fromEntries( + Array.from(new FormData(form).keys()) + .map(function(key) { return [key, true] }) + )) + + /* Remove anchor to omit consent from reappearing and reload */ + location.hash = ''; + location.reload() + }) +</script> diff --git a/src/templates/partials/javascripts/content.html b/src/templates/partials/javascripts/content.html new file mode 100644 index 00000000..d361f18b --- /dev/null +++ b/src/templates/partials/javascripts/content.html @@ -0,0 +1,39 @@ +<!-- + 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. +--> + +<!-- User-preference: link content tabs --> +{% if "content.tabs.link" in features %} + <script> + var tabs = __md_get("__tabs") + if (Array.isArray(tabs)) + main: for (var set of document.querySelectorAll(".tabbed-set")) { + var labels = set.querySelector(".tabbed-labels") + for (var tab of tabs) + for (var label of labels.getElementsByTagName("label")) + if (label.innerText.trim() === tab) { + var input = document.getElementById(label.htmlFor) + input.checked = true + continue main + } + } + </script> +{% endif %} diff --git a/src/templates/partials/javascripts/outdated.html b/src/templates/partials/javascripts/outdated.html new file mode 100644 index 00000000..576f3c85 --- /dev/null +++ b/src/templates/partials/javascripts/outdated.html @@ -0,0 +1,29 @@ +<!-- + 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. +--> + +<!-- Version warning --> +<script> + var el = document.querySelector("[data-md-component=outdated]") + var outdated = __md_get("__outdated", sessionStorage) + if (outdated === true && el) + el.hidden = false +</script> diff --git a/src/templates/partials/javascripts/palette.html b/src/templates/partials/javascripts/palette.html new file mode 100644 index 00000000..a2daef1d --- /dev/null +++ b/src/templates/partials/javascripts/palette.html @@ -0,0 +1,29 @@ +<!-- + 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. +--> + +<!-- User preference: color palette --> +<script> + var palette = __md_get("__palette") + if (palette && typeof palette.color === "object") + for (var key of Object.keys(palette.color)) + document.body.setAttribute("data-md-color-" + key, palette.color[key]) +</script> diff --git a/src/templates/partials/language.html b/src/templates/partials/language.html new file mode 100644 index 00000000..e37b953b --- /dev/null +++ b/src/templates/partials/language.html @@ -0,0 +1,28 @@ +<!-- + 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 translations for given language and fallback --> +{% import "partials/languages/" ~ config.theme.language ~ ".html" as lang %} +{% import "partials/languages/en.html" as fallback %} + +<!-- Re-export translations --> +{% macro t(key) %}{{ lang.t(key) or fallback.t(key) or key }}{% endmacro %} diff --git a/src/templates/partials/languages/af.html b/src/templates/partials/languages/af.html new file mode 100644 index 00000000..b7f9f8fa --- /dev/null +++ b/src/templates/partials/languages/af.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Afrikaans --> +{% macro t(key) %}{{ { + "language": "af", + "action.edit": "Wysig hierdie bladsy", + "action.skip": "Slaan oor na inhoud", + "action.view": "Bekyk bron van hierdie bladsy", + "announce.dismiss": "Moenie dit weer wys nie", + "blog.archive": "Argief", + "blog.categories": "Kategorieë", + "blog.categories.in": "binne", + "blog.continue": "Lees verder", + "blog.draft": "Konsep", + "blog.index": "Terug na indeks", + "blog.meta": "Metadata", + "blog.references": "Verwante skakels", + "clipboard.copy": "Kopieer na knipbord", + "clipboard.copied": "gekopieer na knipbord", + "consent.accept": "Aanvaar", + "consent.manage": "Bestuur instellings", + "consent.reject": "Verwerp", + "footer": "Voetskrif", + "footer.next": "Volgende", + "footer.previous": "Vorige", + "header": "Kopskrif", + "meta.comments": "Kommentaar", + "meta.source": "Bron", + "nav": "Navigasie", + "readtime.one": "1 minuut se lees", + "readtime.other": "# minuut se lees", + "rss.created": "RSS-voer geskep", + "rss.updated": "RSS-voer van opgedateerde inhoud", + "search": "Soek", + "search.config.lang": "nl", + "search.placeholder": "Soek", + "search.share": "Deel", + "search.reset": "Terugstel", + "search.result.initializer": "Inisialisering van soektog", + "search.result.placeholder": "Tik om te begin soek", + "search.result.none": "Geen ooreenstemmende dokumente", + "search.result.one": "1 ooreenstemmende dokument", + "search.result.other": "# ooreenstemmende dokumente", + "search.result.more.one": "1 meer op hierdie bladsy", + "search.result.more.other": "# meer op hierdie bladsy", + "search.result.term.missing": "Vermis", + "select.language": "Kies taal", + "select.version": "Kies weergawe", + "source": "Slaan oor na inhoud", + "source.file.contributors": "Medewerkers", + "source.file.date.created": "Geskep", + "source.file.date.updated": "Laaste opdatering", + "tabs": "Duimgids", + "toc": "Inhoudsopgawe", + "top": "Terug na bo" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ar.html b/src/templates/partials/languages/ar.html new file mode 100644 index 00000000..4d5da33a --- /dev/null +++ b/src/templates/partials/languages/ar.html @@ -0,0 +1,77 @@ +<!-- + 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. +--> + +<!-- Translations: Arabic --> +{% macro t(key) %}{{ { + "language": "ar", + "direction": "rtl", + "action.edit": "عدل الصفحة", + "action.skip": "انتقل إلى المحتوى", + "action.view": "عرض مصدر هذه الصفحة", + "announce.dismiss": "لا تظهر هذا مرة أخرى", + "blog.archive": "أرشيف", + "blog.categories": "فئات", + "blog.categories.in": "ضمن", + "blog.continue": "أكمل القراءة", + "blog.draft": "مسودة", + "blog.index": "رجوع إلى الفهرس", + "blog.meta": "بيانات وصفية", + "blog.references": "روابط ذات علاقة", + "clipboard.copy": "نسخ إلى الحافظة", + "clipboard.copied": "تم النسخ الى الحافظة", + "consent.accept": "قبول", + "consent.manage": "إدارة الإعدادات", + "consent.reject": "رفض", + "footer": "هامش سفلي", + "footer.next": "التالية", + "footer.previous": "السابقة", + "header": "عنوان العارضة", + "meta.comments": "التعليقات", + "meta.source": "المصدر", + "nav": "تصفح", + "readtime.one": "قراءة لمدة دقيقة", + "readtime.other": "دقائق قراءة #", + "rss.created": "ملقم بالخلاصات", + "rss.updated": "ملقم بالخلاصات المحدثة", + "search": "إبحث", + "search.config.pipeline": " ", + "search.placeholder": "بحث", + "search.share": "شارك", + "search.reset": "مسح كلي", + "search.result.initializer": "بدء البحث", + "search.result.placeholder": "اكتب لبدء البحث", + "search.result.none": "لا توجد نتائج", + "search.result.one": "نتائج البحث مستند واحد", + "search.result.other": "نتائج البحث # مستندات", + "search.result.more.one": "أكثر من 1 في هذه الصفحة", + "search.result.more.other": "أكثر من # في هذه الصفحة", + "search.result.term.missing": "مفقود", + "select.language": "إختر اللغة", + "select.version": "إختر الإصدار", + "source": "اذهب إلى المصدر", + "source.file.contributors": "المساهمون", + "source.file.date.created": "خلقت", + "source.file.date.updated": "اخر تحديث", + "tabs": "نوافذ", + "toc": "جدول المحتويات", + "top": "عد إلى الأعلى" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/be.html b/src/templates/partials/languages/be.html new file mode 100644 index 00000000..c36c8402 --- /dev/null +++ b/src/templates/partials/languages/be.html @@ -0,0 +1,77 @@ +<!-- + 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. +--> + +<!-- Translations: Belarusian --> +{% macro t(key) %}{{ { + "language": "be", + "direction": "ltr", + "action.edit": "Правіць старонку", + "action.skip": "Перайсці да зместа", + "action.view": "Паглядзець зыходны код старонкі", + "announce.dismiss": "Больш не паказваць", + "blog.archive": "Заархіваваць", + "blog.categories": "Катэгорыі", + "blog.categories.in": "у", + "blog.continue": "Працягнуць чытаць", + "blog.draft": "Чарнавік", + "blog.index": "Вярнуцца на хатнюю", + "blog.meta": "Метаданыя", + "blog.references": "Спасылкі па тэме", + "clipboard.copy": "Скапіраваць у буфер абмена", + "clipboard.copied": "Скапіравана ў буфер абмена", + "consent.accept": "Прыняць", + "consent.manage": "Кіраваць наладамі", + "consent.reject": "Адхіліць", + "footer": "Ніжні калантытул", + "footer.next": "Наступная", + "footer.previous": "Папярэдняя", + "header": "Верхні калантытул", + "meta.comments": "Каментарыі", + "meta.source": "Зыходны код", + "nav": "Навігацыя", + "readtime.one": "Прачытанне зойме 1 хв", + "readtime.other": "Прачытанне зойме # хв", + "rss.created": "RSS стужка", + "rss.updated": "RSS стужка з абноўленым зместам", + "search": "Пошук", + "search.config.lang": "ru", + "search.placeholder": "Пошук", + "search.share": "Падзяліцца", + "search.reset": "Ачысціць", + "search.result.initializer": "Пачынаем пошук", + "search.result.placeholder": "Пачніце друкаваць для пошуку", + "search.result.none": "Нічога ня знойдзена", + "search.result.one": "Адзін адпаведны дакумент", + "search.result.other": "Адпаведных дакументаў: #", + "search.result.more.one": "Яшчэ 1 на гэтай старонцы", + "search.result.more.other": "Яшчэ # на гэтай старонцы", + "search.result.term.missing": "Адсутнічае", + "select.language": "Выберыце мову", + "select.version": "Выберыце версію", + "source": "Перайсці ў рэпазітар", + "source.file.contributors": "Укладальнікі", + "source.file.date.created": "Створана", + "source.file.date.updated": "Апошняе абнаўленне", + "tabs": "Укладкі", + "toc": "Змест", + "top": "Вярнуцца да пачатку" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/bg.html b/src/templates/partials/languages/bg.html new file mode 100644 index 00000000..f36fd437 --- /dev/null +++ b/src/templates/partials/languages/bg.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Bulgarian --> +{% macro t(key) %}{{ { + "language": "bg", + "action.edit": "Редактирай тази страница", + "action.skip": "Към съдържанието", + "action.view": "Виж съдържанието на тази страница", + "announce.dismiss": "Не показвай повече", + "blog.archive": "Архив", + "blog.categories": "Категории", + "blog.categories.in": "В", + "blog.continue": "Продължи четенето", + "blog.draft": "Чернова", + "blog.index": "Назад към индекса", + "blog.meta": "Метаданни", + "blog.references": "Свързани линкове", + "clipboard.copy": "Копирай", + "clipboard.copied": "Копирано", + "consent.accept": "Приеми", + "consent.manage": "Управление на настойките", + "consent.reject": "Откажи", + "footer": "Долен колонтитул", + "footer.next": "Следваща", + "footer.previous": "Предишна", + "header": "Горен колонтитул", + "meta.comments": "Коментари", + "meta.source": "Код", + "nav": "Навигация", + "readtime.one": "1 мин четено", + "readtime.other": "# мин четено", + "rss.created": "RSS публикации", + "rss.updated": "RSS публикации с актуализирано съдържание", + "search": "Търси", + "search.config.lang": "ru", + "search.placeholder": "Търси", + "search.share": "Сподели", + "search.reset": "Изчисти", + "search.result.initializer": "Инициализирано търсене", + "search.result.placeholder": "Започнете да пишете, за да търсите", + "search.result.none": "Няма резултати", + "search.result.one": "1 резултат", + "search.result.other": "# резултата", + "search.result.more.one": "още 1 на тази страница", + "search.result.more.other": "още # на тази страница", + "search.result.term.missing": "Липсващо", + "select.language": "Избери език", + "select.version": "Избери версия", + "source": "Към хранилището", + "source.file.contributors": "Участници", + "source.file.date.created": "Създаден", + "source.file.date.updated": "Последна промяна", + "tabs": "Табове", + "toc": "Съдържание", + "top": "Върни се в началото" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/bn.html b/src/templates/partials/languages/bn.html new file mode 100644 index 00000000..0a3ee6d0 --- /dev/null +++ b/src/templates/partials/languages/bn.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Bengali (Bangla) --> +{% macro t(key) %}{{ { + "language": "bn", + "action.edit": "এই পেজ এডিট করুন", + "action.skip": "কনটেন্টে যান", + "action.view": "পেজের ভিউ", + "announce.dismiss": "আর কখনো দেখাবে না", + "blog.archive": "সংরক্ষণাগার", + "blog.categories": "বিভাগ", + "blog.categories.in": "বিভাগের মধ্যে", + "blog.continue": "পড়তে থাকুন", + "blog.draft": "খসড়া", + "blog.index": "ইনডেক্সে ফিরে যান", + "blog.meta": "মেটাডেটা", + "blog.references": "সম্পর্কিত লিংক", + "clipboard.copy": "ক্লিপবোর্ডে কপি করুন", + "clipboard.copied": "ক্লিপবোর্ডে কপি হয়েছে", + "consent.accept": "গ্রহণ", + "consent.manage": "সেটিংস ব্যবস্থাপনা", + "consent.reject": "প্রত্যাখ্যান", + "footer": "ফুটার", + "footer.next": "পরে", + "footer.previous": "পূর্ববর্তী", + "header": "হেডার", + "meta.comments": "মন্তব্য", + "meta.source": "উৎস", + "nav": "ন্যাভিগেশন", + "readtime.one": "১ মিনিট পড়া", + "readtime.other": "# মিনিট পড়া", + "rss.created": "আরএসএস ফিড", + "rss.updated": "আপডেট করা বিষয়বস্তুর আরএসএস ফিড", + "search": "অনুসন্ধান করুন", + "search.config.pipeline": " ", + "search.placeholder": "অনুসন্ধান করুন", + "search.share": "শেয়ার", + "search.reset": "রিসেট", + "search.result.initializer": "অনুসন্ধান শুরু করা হচ্ছে", + "search.result.placeholder": "সার্চ টাইপ করুন", + "search.result.none": "কিছু পাওয়া যায়নি", + "search.result.one": "১ টা ডকুমেন্ট", + "search.result.other": "# টা ডকুমেন্ট", + "search.result.more.one": "এই পৃষ্ঠায় আরও ১টি আছে", + "search.result.more.other": "এই পৃষ্ঠায় আরও #টি আছে", + "search.result.term.missing": "অনুপস্থিত", + "select.language": "ভাষা নির্বাচন করুণ", + "select.version": "সংস্করণ নির্বাচন করুণ", + "source": "রিপোজিটরিতে যান", + "source.file.contributors": "অবদানকারী", + "source.file.date.created": "তৈরি হয়েছে", + "source.file.date.updated": "শেষ আপডেট", + "tabs": "ট্যাব", + "toc": "সূচি তালিকা", + "top": "উপরে ফিরে যাও" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ca.html b/src/templates/partials/languages/ca.html new file mode 100644 index 00000000..8fd2b03a --- /dev/null +++ b/src/templates/partials/languages/ca.html @@ -0,0 +1,75 @@ +<!-- + 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. +--> + +<!-- Translations: Catalan --> +{% macro t(key) %}{{ { + "language": "ca", + "action.edit": "Edita aquesta pàgina", + "action.skip": "Salta el contingut", + "action.view": "Visualitza el codi font", + "announce.dismiss": "No ho tornis a mostrar", + "blog.archive": "Arxiva", + "blog.categories": "Categories", + "blog.categories.in": "a", + "blog.continue": "Continua llegint", + "blog.draft": "Esborrany", + "blog.index": "Torna a l'índex", + "blog.meta": "Metadades", + "blog.references": "Enllaços relacionats", + "clipboard.copy": "Còpia al porta-retalls", + "clipboard.copied": "Copiat al porta-retalls", + "consent.accept": "Accepta", + "consent.manage": "Gestiona la configuració", + "consent.reject": "Rebutja", + "footer": "Peu de pàgina", + "footer.next": "Següent", + "footer.previous": "Anterior", + "header": "Capçalera", + "meta.comments": "Comentaris", + "meta.source": "Codi font", + "nav": "Navegació", + "readtime.one": "1 min de lectura", + "readtime.other": "# min de lectura", + "rss.created": "Canal RSS", + "rss.updated": "Canal RSS de contingut actualitzat", + "search": "Cerca", + "search.placeholder": "Cerca", + "search.share": "Comparteix", + "search.reset": "Neteja", + "search.result.initializer": "Inicialitzant cerca", + "search.result.placeholder": "Escriu per a començar a cercar", + "search.result.none": "Cap document coincideix", + "search.result.one": "1 document coincident", + "search.result.other": "# documents coincidents", + "search.result.more.one": "1 més en aquesta pàgina", + "search.result.more.other": "# més en aquesta pàgina", + "search.result.term.missing": "Desaparegut", + "select.language": "Selecciona la llengua", + "select.version": "Selecciona la versió", + "source": "Ves al repositori", + "source.file.contributors": "Col·laboradors", + "source.file.date.created": "Creada", + "source.file.date.updated": "Darrera actualització", + "tabs": "Pestanyes", + "toc": "Taula de continguts", + "top": "Torna a l'inici" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/cs.html b/src/templates/partials/languages/cs.html new file mode 100644 index 00000000..fb955865 --- /dev/null +++ b/src/templates/partials/languages/cs.html @@ -0,0 +1,75 @@ +<!-- + 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. +--> + +<!-- Translations: Czech --> +{% macro t(key) %}{{ { + "language": "cs", + "action.edit": "Upravit tuto stránku", + "action.skip": "Přeskočit obsah", + "action.view": "Zobrazit zdroj této stránky", + "announce.dismiss": "Již nezobrazovat", + "blog.archive": "Archiv", + "blog.categories": "Kategorie", + "blog.categories.in": "v", + "blog.continue": "Pokračovat ve čtení", + "blog.draft": "Návrh", + "blog.index": "Zpět na index", + "blog.meta": "Metadata", + "blog.references": "Související odkazy", + "clipboard.copy": "Kopírovat do schránky", + "clipboard.copied": "Zkopírováno do schránky", + "consent.accept": "Akceptovat", + "consent.manage": "Spravovat nastavení", + "consent.reject": "Odmítnout", + "footer": "Zápatí", + "footer.next": "Další", + "footer.previous": "Předchozí", + "header": "Záhlaví", + "meta.comments": "Komentáře", + "meta.source": "Zdroj", + "nav": "Navigace", + "readtime.one": "1 min čtení", + "readtime.other": "# min čtení", + "rss.created": "RSS kanál", + "rss.updated": "RSS zdroj aktualizovaného obsahu", + "search": "Vyhledávání", + "search.placeholder": "Hledat", + "search.share": "Sdílet", + "search.reset": "Vyčistit", + "search.result.initializer": "Inicializace vyhledávání", + "search.result.placeholder": "Pište co se má vyhledat", + "search.result.none": "Nenalezeny žádné dokumenty", + "search.result.one": "Nalezený dokument: 1", + "search.result.other": "Nalezené dokumenty: #", + "search.result.more.one": "1 další na této stránce", + "search.result.more.other": "# více na této stránce", + "search.result.term.missing": "Chybějící", + "select.language": "Zvolte jazyk", + "select.version": "Vyberte verzi", + "source": "Přejít do repozitáře", + "source.file.contributors": "Přispěvatelé", + "source.file.date.created": "Vytvořeno", + "source.file.date.updated": "Poslední aktualizace", + "tabs": "Karty", + "toc": "Obsah", + "top": "Zpět na začátek" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/da.html b/src/templates/partials/languages/da.html new file mode 100644 index 00000000..2f9da2db --- /dev/null +++ b/src/templates/partials/languages/da.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Danish --> +{% macro t(key) %}{{ { + "language": "da", + "action.edit": "Redigér denne side", + "action.skip": "Gå til indholdet", + "action.view": "Vis kildetekst på denne side", + "announce.dismiss": "Vis ikke dette igen", + "blog.archive": "Arkiv", + "blog.categories": "Kategorier", + "blog.categories.in": "i", + "blog.continue": "Læs mere", + "blog.draft": "Udkast", + "blog.index": "Gå tilbage", + "blog.meta": "Metadata", + "blog.references": "Relateret indhold", + "clipboard.copy": "Kopiér til udklipsholderen", + "clipboard.copied": "Kopieret til udklipsholderen", + "consent.accept": "Accepter", + "consent.manage": "Administrer indstillinger", + "consent.reject": "Afvis", + "footer": "Sidefod", + "footer.next": "Næste", + "footer.previous": "Forrige", + "header": "Sidehoved", + "meta.comments": "Kommentarer", + "meta.source": "Kilde", + "nav": "Navigation", + "readtime.one": "1 minuts læsetid", + "readtime.other": "# minuts læstid", + "rss.created": "RSS feed", + "rss.updated": "RSS feed af opdateret indhold", + "search": "Søg", + "search.config.lang": "da", + "search.placeholder": "Søg", + "search.share": "Del", + "search.reset": "Nulstil søgning", + "search.result.initializer": "Start søgning", + "search.result.placeholder": "Indtast søgeord", + "search.result.none": "Ingen resultater fundet", + "search.result.one": "1 resultat", + "search.result.other": "# resultater", + "search.result.more.one": "1 resultat mere på denne side", + "search.result.more.other": "# resultater mere på denne side", + "search.result.term.missing": "Mangler", + "select.language": "Vælg sprog", + "select.version": "Vælg version", + "source": "Åbn arkiv", + "source.file.contributors": "Bidragydere", + "source.file.date.created": "Oprettet", + "source.file.date.updated": "Sidste ændring", + "tabs": "Faner", + "toc": "Indholdsfortegnelse", + "top": "Tilbage til start" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/de.html b/src/templates/partials/languages/de.html new file mode 100644 index 00000000..bfd8b909 --- /dev/null +++ b/src/templates/partials/languages/de.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: German --> +{% macro t(key) %}{{ { + "language": "de", + "action.edit": "Seite editieren", + "action.skip": "Zum Inhalt", + "action.view": "Quellcode der Seite anzeigen", + "announce.dismiss": "Nicht mehr anzeigen", + "blog.archive": "Archiv", + "blog.categories": "Kategorien", + "blog.categories.in": "in", + "blog.continue": "Weiterlesen", + "blog.draft": "Entwurf", + "blog.index": "Zur Übersicht", + "blog.meta": "Metadaten", + "blog.references": "Weiterführende Links", + "clipboard.copy": "In Zwischenablage kopieren", + "clipboard.copied": "In Zwischenablage kopiert", + "consent.accept": "Akzeptieren", + "consent.manage": "Einstellungen", + "consent.reject": "Ablehnen", + "footer": "Fußzeile", + "footer.next": "Weiter", + "footer.previous": "Zurück", + "header": "Kopfzeile", + "meta.comments": "Kommentare", + "meta.source": "Quellcode", + "nav": "Navigation", + "readtime.one": "1 Min. Lesezeit", + "readtime.other": "# Min. Lesezeit", + "rss.created": "RSS Feed", + "rss.updated": "RSS Feed der aktualisierten Inhalte", + "search": "Suche", + "search.config.lang": "de", + "search.placeholder": "Suche", + "search.share": "Teilen", + "search.reset": "Zurücksetzen", + "search.result.initializer": "Suche wird initialisiert", + "search.result.placeholder": "Suchbegriff eingeben", + "search.result.none": "Keine Suchergebnisse", + "search.result.one": "1 Suchergebnis", + "search.result.other": "# Suchergebnisse", + "search.result.more.one": "1 weiteres Suchergebnis auf dieser Seite", + "search.result.more.other": "# weitere Suchergebnisse auf dieser Seite", + "search.result.term.missing": "Es fehlt", + "select.language": "Sprache wechseln", + "select.version": "Version auswählen", + "source": "Zum Repository", + "source.file.contributors": "Mitwirkende", + "source.file.date.created": "Erstellt", + "source.file.date.updated": "Letztes Update", + "tabs": "Hauptnavigation", + "toc": "Inhaltsverzeichnis", + "top": "Zurück zum Seitenanfang" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/el.html b/src/templates/partials/languages/el.html new file mode 100644 index 00000000..8dce1793 --- /dev/null +++ b/src/templates/partials/languages/el.html @@ -0,0 +1,74 @@ +<!-- + 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. +--> + +<!-- Translations: Greek --> +{% macro t(key) %}{{ { + "language": "el", + "action.edit": "Επεξεργασία αυτής της σελίδας", + "action.skip": "Μετάβαση στο περιεχόμενο", + "action.view": "Προβολή πηγαίου κώδικα", + "announce.dismiss": "Μην το ξαναδείξετε αυτό", + "blog.archive": "Aρχείο", + "blog.categories": "Κατηγορίες", + "blog.categories.in": "Στο", + "blog.continue": "Περισσότερα", + "blog.draft": "Πρόχειρο", + "blog.index": "Eπιστροφή", + "blog.references": "Σχετικοί σύνδεσμοι", + "clipboard.copy": "Αντιγραφή στο πρόχειρο", + "clipboard.copied": "Αντιγράφηκε στο πρόχειρο", + "consent.accept": "Αποδοχή", + "consent.manage": "Περισσότερες επιλογές", + "consent.reject": "Απόρριψη", + "footer": "Υποσέλιδο", + "footer.next": "Επόμενο", + "footer.previous": "Προηγούμενο", + "header": "Κεφαλίδα", + "meta.comments": "Σχόλια", + "meta.source": "Πηγή", + "nav": "Πλοήγηση", + "readtime.one": "1 λεπτό διάβασμα", + "readtime.other": "# λεπτά διάβασμα", + "rss.created": "Ροές Δεδομένων RSS", + "rss.updated": "Ροές Δεδομένων RSS. Τελευταία νέα", + "search": "Αναζήτηση", + "search.placeholder": "Αναζήτηση", + "search.share": "Διαμοίραση", + "search.reset": "Καθαρισμός", + "search.result.initializer": "Αρχικοποίηση αναζήτησης", + "search.result.placeholder": "Πληκτρολογήστε για να αρχίσει η αναζήτηση", + "search.result.none": "δεν βρήκε κάποιο έγγραφο", + "search.result.one": "1 έγγραφο που ταιριάζει", + "search.result.other": "# έγγραφα που ταιριάζουν", + "search.result.more.one": "1 ακόμα σε αυτήν τη σελίδα", + "search.result.more.other": "# ακόμα σε αυτήν τη σελίδα", + "search.result.term.missing": "Λείπει", + "select.language": "Επιλογή γλώσσας", + "select.version": "Επιλογή έκδοσης", + "source": "Μετάβαση στο αποθετήριο", + "source.file.contributors": "Συνεισφέροντες", + "source.file.date.created": "Δημιουργήθηκε", + "source.file.date.updated": "τελευταία ενημέρωση", + "tabs": "Καρτέλες", + "toc": "Πίνακας περιεχομένων", + "top": "Επιστροφή στην αρχή" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/en.html b/src/templates/partials/languages/en.html new file mode 100644 index 00000000..0e6a73ac --- /dev/null +++ b/src/templates/partials/languages/en.html @@ -0,0 +1,79 @@ +<!-- + 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. +--> + +<!-- Translations: English --> +{% macro t(key) %}{{ { + "language": "en", + "direction": "ltr", + "action.edit": "Edit this page", + "action.skip": "Skip to content", + "action.view": "View source of this page", + "announce.dismiss": "Don't show this again", + "blog.archive": "Archive", + "blog.categories": "Categories", + "blog.categories.in": "in", + "blog.continue": "Continue reading", + "blog.draft": "Draft", + "blog.index": "Back to index", + "blog.meta": "Metadata", + "blog.references": "Related links", + "clipboard.copy": "Copy to clipboard", + "clipboard.copied": "Copied to clipboard", + "consent.accept": "Accept", + "consent.manage": "Manage settings", + "consent.reject": "Reject", + "footer": "Footer", + "footer.next": "Next", + "footer.previous": "Previous", + "header": "Header", + "meta.comments": "Comments", + "meta.source": "Source", + "nav": "Navigation", + "readtime.one": "1 min read", + "readtime.other": "# min read", + "rss.created": "RSS feed", + "rss.updated": "RSS feed of updated content", + "search": "Search", + "search.config.lang": "en", + "search.config.pipeline": "stopWordFilter", + "search.config.separator": "[\\s\\-]+", + "search.placeholder": "Search", + "search.share": "Share", + "search.reset": "Clear", + "search.result.initializer": "Initializing search", + "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.language": "Select language", + "select.version": "Select version", + "source": "Go to repository", + "source.file.contributors": "Contributors", + "source.file.date.created": "Created", + "source.file.date.updated": "Last update", + "tabs": "Tabs", + "toc": "Table of contents", + "top": "Back to top" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/eo.html b/src/templates/partials/languages/eo.html new file mode 100644 index 00000000..cd3829a8 --- /dev/null +++ b/src/templates/partials/languages/eo.html @@ -0,0 +1,49 @@ +<!-- + 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. +--> + +<!-- Translations: Esperanto --> +{% macro t(key) %}{{ { + "language": "eo", + "action.edit": "Redakti ĉi tiun paĝon", + "action.skip": "Saltu al enhavo", + "clipboard.copy": "Kopii al tondujo", + "clipboard.copied": "Kopiado al klipo", + "footer": "Piedlinio", + "footer.next": "Sekva", + "footer.previous": "Antaŭa", + "header": "Kaplinio", + "meta.comments": "Komentoj", + "meta.source": "Fontkodo", + "nav": "Navigado", + "search.config.lang": "es", + "search.placeholder": "Serĉo", + "search.reset": "Klara", + "search.result.placeholder": "Tajpu por komenci serĉadon", + "search.result.none": "Neniuj kongruaj dokumentoj", + "search.result.one": "1 kongrua dokumento", + "search.result.other": "# kongruaj dokumentoj", + "source": "Iru al deponejo", + "source.file.date.created": "Kreita", + "source.file.date.updated": "Lasta ĝisdatigo", + "tabs": "Langetoj", + "toc": "Enhavtabelo" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/es.html b/src/templates/partials/languages/es.html new file mode 100644 index 00000000..bbbd9dc1 --- /dev/null +++ b/src/templates/partials/languages/es.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Spanish --> +{% macro t(key) %}{{ { + "language": "es", + "action.edit": "Editar esta página", + "action.skip": "Saltar a contenido", + "action.view": "Ver código fuente de esta página", + "announce.dismiss": "No mostrar esto de nuevo", + "blog.archive": "Archivo", + "blog.categories": "Categorías", + "blog.categories.in": "en", + "blog.continue": "Seguir leyendo", + "blog.draft": "Borrador", + "blog.index": "Regresar al índice", + "blog.meta": "Metadata", + "blog.references": "Enlaces relacionados", + "clipboard.copy": "Copiar al portapapeles", + "clipboard.copied": "Copiado al portapapeles", + "consent.accept": "Aceptar", + "consent.manage": "Gestionar cookies", + "consent.reject": "Rechazar", + "footer": "Pie", + "footer.next": "Siguiente", + "footer.previous": "Anterior", + "header": "Cabecera", + "meta.comments": "Comentarios", + "meta.source": "Fuente", + "nav": "Navegación", + "readtime.one": "1 minuto de lectura", + "readtime.other": "# minutos de lectura", + "rss.created": "Fuente RSS", + "rss.updated": "Fuente RSS de contenido actualizado", + "search": "Buscar", + "search.config.lang": "es", + "search.placeholder": "Búsqueda", + "search.share": "Compartir", + "search.reset": "Limpiar", + "search.result.initializer": "Inicializando búsqueda", + "search.result.placeholder": "Teclee para comenzar búsqueda", + "search.result.none": "No se encontraron documentos", + "search.result.one": "1 documento encontrado", + "search.result.other": "# documentos encontrados", + "search.result.more.one": "1 más en esta página", + "search.result.more.other": "# más en esta página", + "search.result.term.missing": "Falta", + "select.language": "Seleccionar idioma", + "select.version": "Seleccionar versión", + "source": "Ir al repositorio", + "source.file.contributors": "Contribuidores", + "source.file.date.created": "Creado", + "source.file.date.updated": "Última actualización", + "tabs": "Pestañas", + "toc": "Tabla de contenidos", + "top": "Volver al principio" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/et.html b/src/templates/partials/languages/et.html new file mode 100644 index 00000000..8add4225 --- /dev/null +++ b/src/templates/partials/languages/et.html @@ -0,0 +1,43 @@ +<!-- + 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. +--> + +<!-- Translations: Estonian --> +{% macro t(key) %}{{ { + "language": "et", + "action.edit": "Muuda seda lehte", + "action.skip": "Keri sisuni", + "clipboard.copy": "Kopeeri lõikelauale", + "clipboard.copied": "Kopeeritud", + "footer.next": "Järgmine", + "footer.previous": "Eelmine", + "meta.comments": "Kommentaarid", + "meta.source": "Lähtekood", + "search.placeholder": "Otsi", + "search.result.placeholder": "Otsimiseks kirjuta siia", + "search.result.none": "Otsingule ei leitud ühtegi vastet", + "search.result.one": "Leiti üks tulemus", + "search.result.other": "Leiti # tulemust", + "source": "Ava repositooriumis", + "source.file.date.created": "Loodud", + "source.file.date.updated": "Viimane uuendus", + "toc": "Sisukord" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/eu.html b/src/templates/partials/languages/eu.html new file mode 100644 index 00000000..0e52f925 --- /dev/null +++ b/src/templates/partials/languages/eu.html @@ -0,0 +1,75 @@ +<!-- + 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. +--> + +<!-- Translations: Basque --> +{% macro t(key) %}{{ { + "language": "eu", + "action.edit": "Editatu orri hau", + "action.skip": "Joan zuzenean edukira", + "action.view": "Ikusi orri honen iturburua", + "announce.dismiss": "Ez erakutsi hau berriro", + "blog.archive": "Artxiboa", + "blog.categories": "Kategoriak", + "blog.categories.in": "kategoria", + "blog.continue": "Jarraitu irakurtzen", + "blog.draft": "Zirriborroa", + "blog.index": "Itzuli aurkibidera", + "blog.meta": "Metadatuak", + "blog.references": "Erlazionatutako estekak", + "clipboard.copy": "Kopiatu arbelean", + "clipboard.copied": "Arbelean kopiatuta", + "consent.accept": "Onartu", + "consent.manage": "Kudeatu ezarpenak", + "consent.reject": "Ukatu", + "footer": "Orri-oina", + "footer.next": "Hurrengoa", + "footer.previous": "Aurrekoa", + "header": "Atalburua", + "meta.comments": "Iruzkinak", + "meta.source": "Iturburua", + "nav": "Nabigazioa", + "readtime.one": "Minutu batean irakurtzeko", + "readtime.other": "# minututan irakurtzeko", + "rss.created": "RSS jarioa", + "rss.updated": "Eduki eguneratuen RSS jarioa", + "search": "Bilatu", + "search.placeholder": "Bilatu", + "search.share": "Partekatu", + "search.reset": "Garbitu", + "search.result.initializer": "Bilaketa hasieratzen", + "search.result.placeholder": "Idatzi bilatzen hasteko", + "search.result.none": "Bat datorren dokumenturik ez", + "search.result.one": "Bat datorren dokumentu bat", + "search.result.other": "Bat datozen # dokumentu", + "search.result.more.one": "Bat gehiago orri honetan", + "search.result.more.other": "# gehiago orri honetan", + "search.result.term.missing": "Falta da", + "select.language": "Hautatu hizkuntza", + "select.version": "Hautatu bertsioa", + "source": "Joan biltegira", + "source.file.contributors": "Kolaboratzaileak", + "source.file.date.created": "Sortze data", + "source.file.date.updated": "Azken eguneratzea", + "tabs": "Fitxak", + "toc": "Edukiak", + "top": "Igo goraino" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/fa.html b/src/templates/partials/languages/fa.html new file mode 100644 index 00000000..deaa8bca --- /dev/null +++ b/src/templates/partials/languages/fa.html @@ -0,0 +1,77 @@ +<!-- + 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. +--> + +<!-- Translations: Persian (Farsi) --> +{% macro t(key) %}{{ { + "language": "fa", + "direction": "rtl", + "action.edit": "این صفحه را ویرایش کنید", + "action.skip": "پرش به محتویات", + "action.view": "محتویات این صفحه را نشان بده", + "announce.dismiss": "این را دیگر نشان نده", + "blog.archive": "بایگانی", + "blog.categories": "دستهبندیها", + "blog.categories.in": "در", + "blog.continue": "ادامه به خواندن", + "blog.draft": "پیشنویس", + "blog.index": "برگشت به فهرست", + "blog.meta": "فراداده", + "blog.references": "پیوندهای مربوط", + "clipboard.copy": "کپی کردن", + "clipboard.copied": "کپی شد", + "consent.accept": "تایید", + "consent.manage": "مدیریت تنظیمات", + "consent.reject": "رد کردن", + "footer": "پاورقی", + "footer.next": "بعدی", + "footer.previous": "قبلی", + "header": "سرتیتر", + "meta.comments": "نظرات", + "meta.source": "منبع", + "nav": "هدایت", + "readtime.one": "1 دقیقه زمان خواندن", + "readtime.other": "# دقیقه زمان خواندن", + "rss.created": "خوراک آراساس", + "rss.updated": "خوراک آراساس محتویات بهروز شده", + "search": "جستجو", + "search.config.pipeline": " ", + "search.placeholder": "جستجو", + "search.share": "همرسانی", + "search.reset": "بازنشانی", + "search.result.initializer": "راهاندازی جستجو", + "search.result.placeholder": "برای شروع جستجو تایپ کنید", + "search.result.none": "سندی یافت نشد", + "search.result.one": "1 سند یافت شد", + "search.result.other": "# سند یافت شد", + "search.result.more.one": "1 مورد دیگر در این صفحه", + "search.result.more.other": "# مورد دیگر در این صفحه", + "search.result.term.missing": "موجود نیست", + "select.language": "انتخاب زبان", + "select.version": "انتخاب ویرایش", + "source": "رفتن به مخزن", + "source.file.contributors": "مشارکت کنندگان", + "source.file.date.created": "ایجاد شده", + "source.file.date.updated": "اخرین بروزرسانی", + "tabs": "زبانهها", + "toc": "فهرست موضوعات", + "top": "برگشت به بالا" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/fi.html b/src/templates/partials/languages/fi.html new file mode 100644 index 00000000..8ee09122 --- /dev/null +++ b/src/templates/partials/languages/fi.html @@ -0,0 +1,44 @@ +<!-- + 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. +--> + +<!-- Translations: Finnish --> +{% macro t(key) %}{{ { + "language": "fi", + "action.edit": "Muokkaa tätä sivua", + "action.skip": "Hyppää sisältöön", + "clipboard.copy": "Kopioi leikepöydälle", + "clipboard.copied": "Kopioitu leikepöydälle", + "footer.next": "Seuraava", + "footer.previous": "Edellinen", + "meta.comments": "Kommentit", + "meta.source": "Lähdekodi", + "search.config.lang": "fi", + "search.placeholder": "Hae", + "search.result.placeholder": "Kirjoita aloittaaksesi haun", + "search.result.none": "Ei täsmääviä dokumentteja", + "search.result.one": "1 täsmäävä dokumentti", + "search.result.other": "# täsmäävää dokumenttia", + "source": "Mene repositoryyn", + "source.file.date.created": "Luotu", + "source.file.date.updated": "Viimeisin päivitys", + "toc": "Sisällysluettelo" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/fr.html b/src/templates/partials/languages/fr.html new file mode 100644 index 00000000..9d49535a --- /dev/null +++ b/src/templates/partials/languages/fr.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: French --> +{% macro t(key) %}{{ { + "language": "fr", + "action.edit": "Editer cette page", + "action.skip": "Aller au contenu", + "action.view": "Consulter la source de cette page", + "announce.dismiss": "Ne plus montrer cela", + "blog.archive": "Archive", + "blog.categories": "Catégories", + "blog.categories.in": "dans", + "blog.continue": "Continuer à lire", + "blog.draft": "Brouillon", + "blog.index": "Retourner à l'index", + "blog.meta": "Metadonnées", + "blog.references": "Liens connexes", + "clipboard.copy": "Copier dans le presse-papier", + "clipboard.copied": "Copié dans le presse-papier", + "consent.accept": "Accepter", + "consent.manage": "Paramétrer vos choix", + "consent.reject": "Refuser", + "footer": "Pied de page", + "footer.next": "Suivant", + "footer.previous": "Précédent", + "header": "En-tête", + "meta.comments": "Commentaires", + "meta.source": "Source", + "nav": "Navigation", + "readtime.one": "1 min de lecture", + "readtime.other": "# min de lecture", + "rss.created": "Flux RSS", + "rss.updated": "Flux RSS du contenu mis à jour", + "search": "Recherche", + "search.config.lang": "fr", + "search.placeholder": "Rechercher", + "search.share": "Partager", + "search.reset": "Effacer", + "search.result.initializer": "Initialisation de la recherche", + "search.result.placeholder": "Taper pour démarrer la recherche", + "search.result.none": "Aucun document trouvé", + "search.result.one": "1 document trouvé", + "search.result.other": "# documents trouvés", + "search.result.more.one": "1 de plus sur cette page", + "search.result.more.other": "# de plus sur cette page", + "search.result.term.missing": "Non trouvé", + "select.language": "Sélectionner la langue", + "select.version": "Sélectionner la version", + "source": "Aller au dépôt", + "source.file.contributors": "Contributeurs", + "source.file.date.created": "Créé", + "source.file.date.updated": "Dernière mise à jour", + "tabs": "Onglets", + "toc": "Table des matières", + "top": "Retour en haut de la page" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/gl.html b/src/templates/partials/languages/gl.html new file mode 100644 index 00000000..ecb54ffd --- /dev/null +++ b/src/templates/partials/languages/gl.html @@ -0,0 +1,56 @@ +<!-- + 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. +--> + +<!-- Translations: Galician --> +{% macro t(key) %}{{ { + "language": "gl", + "action.edit": "Editar esta páxina", + "action.skip": "Ir ao contido", + "clipboard.copy": "Copiar no cortapapeis", + "clipboard.copied": "Copiado no cortapapeis", + "footer": "Pé", + "footer.next": "Seguinte", + "footer.previous": "Anterior", + "header": "Cabeceira", + "meta.comments": "Comentarios", + "meta.source": "Fonte", + "nav": "Navegación", + "search.config.lang": "es", + "search.placeholder": "Procura", + "search.reset": "Limpar", + "search.result.initializer": "Inicializando procura", + "search.result.placeholder": "Insira un termo", + "search.result.none": "Sen resultados", + "search.result.one": "1 resultado atopado", + "search.result.other": "# resultados atopados", + "search.result.more.one": "1 máis nesta páxina", + "search.result.more.other": "# máis nesta páxina", + "search.result.term.missing": "Falta", + "select.language": "Seleccionar idioma", + "select.version": "Seleccionar version", + "source": "Ir ao repositorio", + "source.file.date.created": "Creada", + "source.file.date.updated": "Última actualización", + "tabs": "Pestanas", + "toc": "Táboa de contidos", + "top": "Volver ao principio" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/he.html b/src/templates/partials/languages/he.html new file mode 100644 index 00000000..128edab5 --- /dev/null +++ b/src/templates/partials/languages/he.html @@ -0,0 +1,77 @@ +<!-- + 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. +--> + +<!-- Translations: Hebrew --> +{% macro t(key) %}{{ { + "language": "he", + "direction": "rtl", + "action.edit": "עריכת הדף הזה", + "action.skip": "לדלג לתוכן", + "action.view": "צפה במקור של דף זה", + "announce.dismiss": "לא להציג את זה שוב", + "blog.archive": "ארכיון", + "blog.categories": "קטגוריות", + "blog.categories.in": "בתוך", + "blog.continue": "המשך לקרוא", + "blog.draft": "טיוטה", + "blog.index": "חזרה לאינדקס", + "blog.meta": "מטא-נתונים", + "blog.references": "קישורים קשורים", + "clipboard.copy": "העתקה ללוח", + "clipboard.copied": "הועתק ללוח", + "consent.accept": "לקבל", + "consent.manage": "לנהל הגדרות", + "consent.reject": "לדחות", + "footer": "כותרת תחתונה", + "footer.next": "הבא", + "footer.previous": "הקודם", + "header": "כותרת עליונה", + "meta.comments": "הערות", + "meta.source": "מקור", + "nav": "ניווט", + "readtime.one": "קריאה 1 דקות", + "readtime.other": "# דקות קריאה", + "rss.created": "RSS הזנת", + "rss.updated": "הזנת RSS של תוכן מעודכן", + "search": "חיפוש", + "search.config.pipeline": " ", + "search.placeholder": "חיפוש", + "search.share": "שיתוף", + "search.reset": "ניקוי", + "search.result.initializer": "אתחול חיפוש", + "search.result.placeholder": "יש להקליד כדי להתחיל לחפש", + "search.result.none": "אין מסמכים תואמים", + "search.result.one": "מסמך1 תואם", + "search.result.other": "# מסמך תואם", + "search.result.more.one": "עוד אחד בדף הזה", + "search.result.more.other": "עוד # בדף הזה", + "search.result.term.missing": "חסר", + "select.language": "בחירת שפה", + "select.version": "בחירת גרסה", + "source": "לעבור אל המאגר", + "source.file.contributors": "תורמים", + "source.file.date.created": "נוצר", + "source.file.date.updated": "עדכון אחרון", + "tabs": "לשוניות", + "toc": "תוכן העניינים", + "top": "חזרה למעלה" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/hi.html b/src/templates/partials/languages/hi.html new file mode 100644 index 00000000..8c27c259 --- /dev/null +++ b/src/templates/partials/languages/hi.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Hindi --> +{% macro t(key) %}{{ { + "language": "hi", + "action.edit": "इस पृष्ठ को संपादित करें", + "action.skip": "विषय पर बढ़ें", + "action.view": "इस पृष्ठ का सूत्र देखें", + "announce.dismiss": "इसे पुनः न दिखायें", + "blog.archive": "पुरालेख", + "blog.categories": "वर्ग", + "blog.categories.in": "में", + "blog.continue": "पढ़ते रहिये", + "blog.draft": "मसौदा", + "blog.index": "सूचि को लौटें", + "blog.meta": "मेटाडेटा", + "blog.references": "सम्बंधित लिंक", + "clipboard.copy": "क्लिपबोर्ड पर कॉपी करें", + "clipboard.copied": "क्लिपबोर्ड पर कॉपी कर दिया गया", + "consent.accept": "स्वीकार करें", + "consent.manage": "सेटिंग्स मैनेज करें", + "consent.reject": "अस्वीकार करें", + "footer": "फुटर", + "footer.next": "आगामी", + "footer.previous": "पिछला", + "header": "शीर्षक", + "meta.comments": "टिप्पणियाँ", + "meta.source": "स्रोत", + "nav": "नैविगेशन", + "readtime.one": "1 मिनट पढ़ने को", + "readtime.other": "# मिनट पढ़ने को", + "rss.created": "RSS फीड", + "rss.updated": "नवीनतम विषयवस्तु का RSS feed", + "search": "खोजें", + "search.config.lang": "hi", + "search.placeholder": "खोज", + "search.share": "शेयर करें", + "search.reset": "हटा दें", + "search.result.initializer": "खोज शुरू करें", + "search.result.placeholder": "खोज शुरू करने के लिए टाइप करें", + "search.result.none": "कोई मिलान डॉक्यूमेंट नहीं", + "search.result.one": "1 मिलान डॉक्यूमेंट", + "search.result.other": "# मिलान डाक्यूमेंट्स", + "search.result.more.one": "1 और इस पृष्ठ पर", + "search.result.more.other": "# और इस पृष्ठ पर", + "search.result.term.missing": "ग़ायब", + "select.language": "भाषा चुनें", + "select.version": "वर्शन चुनें", + "source": "रिपॉजिटरी पर जाएं", + "source.file.contributors": "योगदाता", + "source.file.date.created": "बनाया था", + "source.file.date.updated": "आखिरी अपडेट", + "tabs": "टैब", + "toc": "विषय - सूची", + "top": "शीर्षभाग पर लौटें" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/hr.html b/src/templates/partials/languages/hr.html new file mode 100644 index 00000000..30afe6a9 --- /dev/null +++ b/src/templates/partials/languages/hr.html @@ -0,0 +1,75 @@ +<!-- + 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. +--> + +<!-- Translations: Croatian --> +{% macro t(key) %}{{ { + "language": "hr", + "action.edit": "Uredi stranicu", + "action.skip": "Preskoči na sadržaj", + "action.view": "Pregledaj izvorni kod ove stranice", + "announce.dismiss": "Ne prikazuj ovo opet", + "blog.archive": "Arhiva", + "blog.categories": "Kategorije", + "blog.categories.in": "u", + "blog.continue": "Nastavi čitati", + "blog.draft": "Nacrt", + "blog.index": "Natrag na indeks", + "blog.meta": "Metapodaci", + "blog.references": "Srodne poveznice", + "clipboard.copy": "Kopiraj u međuspremnik", + "clipboard.copied": "Kopirano u međuspremnik", + "consent.accept": "Prihvati", + "consent.manage": "Upravljaj postavkama", + "consent.reject": "Odbij", + "footer": "Podnožje", + "footer.next": "Sljedeće", + "footer.previous": "Prethodno", + "header": "Zaglavlje", + "meta.comments": "Komentari", + "meta.source": "Izvor", + "nav": "Navigacija", + "readtime.one": "1 minuta čitanja", + "readtime.other": "# minut(a/e) čitanja", + "rss.created": "RSS izvor", + "rss.updated": "RSS izvor osvježenog sadržaja", + "search": "Pretraživanje", + "search.placeholder": "Pretraži", + "search.share": "Podijeli", + "search.reset": "Očisti", + "search.result.initializer": "Inicijaliziranje pretraživanja", + "search.result.placeholder": "Unesite pojam pretraživanja", + "search.result.none": "Ništa nije pronađeno", + "search.result.one": "1 rezultat pretraživanja", + "search.result.other": "# rezultat(a) pretraživanja", + "search.result.more.one": "1 rezultat na ovoj stranici", + "search.result.more.other": "# rezultat(a) na ovoj stranici", + "search.result.term.missing": "Nedostaje", + "select.language": "Odabir jezika", + "select.version": "Odabir verzije", + "source": "Idi u repozitorij", + "source.file.contributors": "Suradnici", + "source.file.date.created": "Stvaranje", + "source.file.date.updated": "Posljednje ažuriranje", + "tabs": "Kartice", + "toc": "Sadržaj", + "top": "Vrati se na vrh" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/hu.html b/src/templates/partials/languages/hu.html new file mode 100644 index 00000000..44798dd8 --- /dev/null +++ b/src/templates/partials/languages/hu.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Hungarian --> +{% macro t(key) %}{{ { + "language": "hu", + "action.edit": "Oldal szerkesztése", + "action.skip": "Kihagyás", + "action.view": "Oldal forrásának megtekintése", + "announce.dismiss": "Ne mutasd többet", + "blog.archive": "Archívum", + "blog.categories": "Kategóriák", + "blog.categories.in": "Kategória:", + "blog.continue": "Folytatás", + "blog.draft": "Piszkozat", + "blog.index": "Vissza a főoldalra", + "blog.meta": "Metaadat", + "blog.references": "Kapcsolódó linkek", + "clipboard.copy": "Másolás vágólapra", + "clipboard.copied": "Vágólapra másolva", + "consent.accept": "Elfogadás", + "consent.manage": "Beállítások", + "consent.reject": "Visszautasítás", + "footer": "Élőláb", + "footer.next": "Következő", + "footer.previous": "Előző", + "header": "Élőfej", + "meta.comments": "Hozzászólások", + "meta.source": "Forrás", + "nav": "Navigáció", + "readtime.one": "1 percnyi", + "readtime.other": "# percnyi", + "rss.created": "RSS feed", + "rss.updated": "Frissített tartalom RSS feedje", + "search": "Keresés", + "search.config.lang": "hu", + "search.placeholder": "Keresés", + "search.share": "Megosztás", + "search.reset": "Törlés", + "search.result.initializer": "Keresés inicializálása", + "search.result.placeholder": "Kereséshez írj ide valamit", + "search.result.none": "Nincs találat", + "search.result.one": "1 egyező dokumentum", + "search.result.other": "# egyező dokumentum", + "search.result.more.one": "1 további találat az oldalon", + "search.result.more.other": "# további találat az oldalon", + "search.result.term.missing": "Üres", + "select.language": "Nyelvváltás", + "select.version": "Verzióváltás", + "source": "Főoldalra ugrás", + "source.file.contributors": "Szerzők", + "source.file.date.created": "Létrehozva", + "source.file.date.updated": "Utolsó frissítés", + "tabs": "Lapok", + "toc": "Tartalomjegyzék", + "top": "Vissza a tetejére" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/hy.html b/src/templates/partials/languages/hy.html new file mode 100644 index 00000000..e3418341 --- /dev/null +++ b/src/templates/partials/languages/hy.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Armenian --> +{% macro t(key) %}{{ { + "language": "hy", + "action.edit": "Խմբագրել այս էջը", + "action.skip": "Անցնել պարունակությանը", + "action.view": "Դիտել այս էջի սկզբնաղբյուրը", + "announce.dismiss": "Այլևս չցուցադրել", + "blog.archive": "Արխիվ", + "blog.categories": "Կատեգորիաներ", + "blog.categories.in": "in", + "blog.continue": "Շարունակել կարդալ", + "blog.draft": "Սևագիր", + "blog.index": "Հետ դեպի ինդեքս", + "blog.meta": "Մետատվյալներ", + "blog.references": "Առնչվող հղումներ", + "clipboard.copy": "Պատճենել", + "clipboard.copied": "Պատճենված է", + "consent.accept": "Ընդունել", + "consent.manage": "Կառավարել կարգավորումները", + "consent.reject": "Մերժել", + "footer": "Էջատակ", + "footer.next": "Հաջորդը", + "footer.previous": "Նախորդը", + "header": "Գլխագիր", + "meta.comments": "Մեկնաբանությունները", + "meta.source": "Աղբյուր", + "nav": "Տեղորոշում", + "readtime.one": "Ընթերցում՝ 1 րոպե", + "readtime.other": "Ընթերցում՝ # րոպե", + "rss.created": "RSS հոսք", + "rss.updated": "Արդիացված բովանդակության RSS հոսք", + "search": "Որոնում", + "search.config.pipeline": " ", + "search.placeholder": "Որոնում", + "search.share": "Կիսվել", + "search.reset": "Ջնջել", + "search.result.initializer": "Որոնում", + "search.result.placeholder": "Մուտքագրեք որոնելու համար", + "search.result.none": "Արդյունքներ չկան", + "search.result.one": "1 արդյունք", + "search.result.other": "# արդյունք", + "search.result.more.one": "ևս 1-ը այս էջում", + "search.result.more.other": "ևս #-ը այս էջում", + "search.result.term.missing": "Բացակայում է", + "select.language": "Ընտրել լեզուն", + "select.version": "Ընտրել տարբերակը", + "source": "Դեպի պահոց", + "source.file.contributors": "Հեղինակողներ", + "source.file.date.created": "Ստեղծված է", + "source.file.date.updated": "Վերջին թարմացումը", + "tabs": "Ներդիրներ", + "toc": "Բովանդակություն", + "top": "Վերադառնալ սկիզբ" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/id.html b/src/templates/partials/languages/id.html new file mode 100644 index 00000000..c54229f0 --- /dev/null +++ b/src/templates/partials/languages/id.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Indonesian --> +{% macro t(key) %}{{ { + "language": "id", + "action.edit": "Ubah halaman ini", + "action.skip": "Lewati ke isi", + "action.view": "Lihat sumber halaman ini", + "announce.dismiss": "Jangan lihat ini lagi", + "blog.archive": "Arsip", + "blog.categories": "Kategori", + "blog.categories.in": "dalam", + "blog.continue": "Lanjut membaca", + "blog.draft": "Draf", + "blog.index": "Kembali ke indeks", + "blog.meta": "Metadata", + "blog.references": "Tautan yang berhubungan", + "clipboard.copy": "Salin ke clipboard", + "clipboard.copied": "Tersalin ke clipboard", + "consent.accept": "Terima", + "consent.manage": "Kelola pengaturan", + "consent.reject": "Tolak", + "footer": "Footer", + "footer.next": "Selanjutnya", + "footer.previous": "Sebelumnya", + "header": "Header", + "meta.comments": "Komentar", + "meta.source": "Sumber", + "nav": "Navigasi", + "readtime.one": "1 menit baca", + "readtime.other": "# menit baca", + "rss.created": "Umpan RSS", + "rss.updated": "Umpan RSS dari konten yang diperbarui", + "search": "Cari", + "search.config.pipeline": " ", + "search.placeholder": "Cari", + "search.share": "Bagikan", + "search.reset": "Kosongkan", + "search.result.initializer": "Mempersiapkan pencarian", + "search.result.placeholder": "Ketik untuk mulai pencarian", + "search.result.none": "Tidak ada dokumen yang sesuai", + "search.result.one": "1 dokumen ditemukan", + "search.result.other": "# dokumen ditemukan", + "search.result.more.one": "1 lagi di halaman ini", + "search.result.more.other": "# lagi di halaman ini", + "search.result.term.missing": "Tidak ada", + "select.language": "Pilih bahasa", + "select.version": "Pilih versi", + "source": "Ke repositori", + "source.file.contributors": "Kontributor", + "source.file.date.created": "Dibuat", + "source.file.date.updated": "Pembaruan terakhir", + "tabs": "Tab", + "toc": "Daftar isi", + "top": "Kembali ke atas" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/is.html b/src/templates/partials/languages/is.html new file mode 100644 index 00000000..5b9a47e8 --- /dev/null +++ b/src/templates/partials/languages/is.html @@ -0,0 +1,75 @@ +<!-- + 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. +--> + +<!-- Translations: Icelandic --> +{% macro t(key) %}{{ { + "language": "is", + "action.edit": "Breyta þessari síðu", + "action.skip": "Hoppa yfir í efnið", + "action.view": "Skoða frumgögn þessarar síðu", + "announce.dismiss": "Ekki sýna þetta aftur", + "blog.archive": "Safn", + "blog.categories": "Flokkar", + "blog.categories.in": "í", + "blog.continue": "Lesa meira", + "blog.draft": "Uppkast", + "blog.index": "Til baka í yfirlit", + "blog.meta": "Lýsigögn", + "blog.references": "Þessu tengt", + "clipboard.copy": "Afrita á klemmuspjald", + "clipboard.copied": "Afritað á klemmuspjald", + "consent.accept": "Samþykkja", + "consent.manage": "Breyta stillingum", + "consent.reject": "Hafna", + "footer": "Síðufótur", + "footer.next": "Næsta", + "footer.previous": "Fyrri", + "header": "Haus", + "meta.comments": "Umræður", + "meta.source": "Frumgögn", + "nav": "Valmynd", + "readtime.one": "1 mín lestur", + "readtime.other": "# mín lestur", + "rss.created": "RSS veita", + "rss.updated": "RSS veita fyrir uppfært innihald", + "search": "Leita", + "search.placeholder": "Leita", + "search.share": "Deila", + "search.reset": "Hreinsa", + "search.result.initializer": "Ræsi leitarvél", + "search.result.placeholder": "Byrjaðu að skrifa til að hefja leit", + "search.result.none": "Engar síður fundust", + "search.result.one": "1 síða fannst", + "search.result.other": "# síður fundust", + "search.result.more.one": "1 til viðbótar á þessari síðu", + "search.result.more.other": "# til viðbótar á þessari síðu", + "search.result.term.missing": "Vantar", + "select.language": "Veldu tungumál", + "select.version": "Veldu útgáfu", + "source": "Fara í gagnageymslu", + "source.file.contributors": "Meðhöfundar", + "source.file.date.created": "Búið til", + "source.file.date.updated": "Síðast uppfært", + "tabs": "Flipar", + "toc": "Efnisyfirlit", + "top": "Fara aftur efst" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/it.html b/src/templates/partials/languages/it.html new file mode 100644 index 00000000..77956ee7 --- /dev/null +++ b/src/templates/partials/languages/it.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Italian --> +{% macro t(key) %}{{ { + "language": "it", + "action.edit": "Modifica", + "action.skip": "Vai al contenuto", + "action.view": "Vedi il sorgente di questa pagina", + "announce.dismiss": "Non mostrare più", + "blog.archive": "Archivio", + "blog.categories": "Categorie", + "blog.categories.in": "in", + "blog.continue": "Continua a leggere", + "blog.draft": "Bozza", + "blog.index": "Torna all'indice", + "blog.meta": "Metadati", + "blog.references": "Collegamenti", + "clipboard.copy": "Copia", + "clipboard.copied": "Copiato", + "consent.accept": "Accetta", + "consent.manage": "Gestisci le opzioni", + "consent.reject": "Rifiuta", + "footer": "Piede", + "footer.next": "Successivo", + "footer.previous": "Precedente", + "header": "Intestazione", + "meta.comments": "Commenti", + "meta.source": "Sorgente", + "nav": "Navigazione", + "readtime.one": "1 minuto di lettura", + "readtime.other": "# minuti di lettura", + "rss.created": "Feed RSS", + "rss.updated": "Contenuto aggiornato del feed RSS", + "search": "Cerca", + "search.config.lang": "it", + "search.placeholder": "Cerca", + "search.share": "Condividi", + "search.reset": "Cancella", + "search.result.initializer": "Inizializza la ricerca", + "search.result.placeholder": "Scrivi per iniziare a cercare", + "search.result.none": "Nessun documento trovato", + "search.result.one": "1 documento trovato", + "search.result.other": "# documenti trovati", + "search.result.more.one": "1 altro in questa pagina", + "search.result.more.other": "# altri in questa pagina", + "search.result.term.missing": "Non presente", + "select.language": "Seleziona la lingua", + "select.version": "Seleziona la versione", + "source": "Apri repository", + "source.file.contributors": "Contributori", + "source.file.date.created": "Creata", + "source.file.date.updated": "Ultimo aggiornamento", + "tabs": "Tabs", + "toc": "Indice", + "top": "Torna su" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ja.html b/src/templates/partials/languages/ja.html new file mode 100644 index 00000000..b4b4279d --- /dev/null +++ b/src/templates/partials/languages/ja.html @@ -0,0 +1,78 @@ +<!-- + 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. +--> + +<!-- Translations: Japanese --> +{% macro t(key) %}{{ { + "language": "ja", + "action.edit": "編集", + "action.skip": "コンテンツにスキップ", + "action.view": "このページの原文を表示", + "announce.dismiss": "非表示にします", + "blog.archive": "過去の投稿", + "blog.categories": "カテゴリー", + "blog.categories.in": "", + "blog.continue": "続きを読む", + "blog.draft": "下書き", + "blog.index": "ブログトップへ戻る", + "blog.meta": "メタデータ", + "blog.references": "関連リンク", + "clipboard.copy": "クリップボードへコピー", + "clipboard.copied": "コピーしました", + "consent.accept": "同意", + "consent.manage": "サイトの設定", + "consent.reject": "拒否", + "footer": "フッター", + "footer.next": "次", + "footer.previous": "前", + "header": "ヘッダー", + "meta.comments": "コメント", + "meta.source": "ソース", + "nav": "ナビゲーション", + "readtime.one": "このページは約1分で読めます", + "readtime.other": "このページは約#分で読めます", + "rss.created": "新しいページのRSSフィード", + "rss.updated": "更新されたページのRSSフィード", + "search": "検索", + "search.config.lang": "ja", + "search.config.pipeline": "stemmer", + "search.config.separator": "[\\s\\- 、。,.]+", + "search.placeholder": "検索", + "search.share": "共有", + "search.reset": "クリア", + "search.result.initializer": "検索を初期化", + "search.result.placeholder": "検索キーワードを入力してください", + "search.result.none": "何も見つかりませんでした", + "search.result.one": "1件見つかりました", + "search.result.other": "#件見つかりました", + "search.result.more.one": "このページ内にもう1件見つかりました", + "search.result.more.other": "このページ内にあと#件見つかりました", + "search.result.term.missing": "検索に含まれない", + "select.language": "言語切り替え", + "select.version": "バージョン切り替え", + "source": "リポジトリへ", + "source.file.contributors": "投稿者", + "source.file.date.created": "作成日", + "source.file.date.updated": "最終更新日", + "tabs": "タブ", + "toc": "目次", + "top": "ページトップへ戻る" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ka.html b/src/templates/partials/languages/ka.html new file mode 100644 index 00000000..edfd2e02 --- /dev/null +++ b/src/templates/partials/languages/ka.html @@ -0,0 +1,49 @@ +<!-- + 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. +--> + +<!-- Translations: Georgian --> +{% macro t(key) %}{{ { + "language": "ka", + "action.edit": "გვერდის რედარქირება", + "action.skip": "კონტენტზე გადასვლა", + "clipboard.copy": "კოპირება", + "clipboard.copied": "კოპირებულია", + "footer.next": "შემდეგი", + "footer.previous": "წინა", + "meta.comments": "კომენტარები", + "meta.source": "წყარო", + "nav": "ნავიგაცია", + "search.config.pipeline": " ", + "search.placeholder": "ძებნა", + "search.reset": "გასუფთავება", + "search.result.placeholder": "ჩაწერე ძებნის დასაწყებად", + "search.result.none": "დოკუმენტი ვერ მოიძებნა", + "search.result.one": "მოიძებნა 1 დოკუმენტი", + "search.result.other": "მოიძებნა # დოკუმენტი", + "search.result.more.one": "კიდევ 1 ამ გვერდზე", + "search.result.more.other": "კიდევ # ამ გვერდზე", + "source": "საცავში გადასვლა", + "source.file.date.created": "შეიქმნა", + "source.file.date.updated": "ბოლო განახლება", + "tabs": "ტაბები", + "toc": "სარჩევი" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/kn.html b/src/templates/partials/languages/kn.html new file mode 100644 index 00000000..bd0ff722 --- /dev/null +++ b/src/templates/partials/languages/kn.html @@ -0,0 +1,75 @@ +<!-- + 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. +--> + +<!-- Translations: Kannada --> +{% macro t(key) %}{{ { + "language": "kn", + "action.edit": "ಈ ಪುಟವನ್ನು ತಿದ್ದುಪಡಿ ಮಾಡಿ", + "action.skip": "ವಿಷಯಕ್ಕೆ ತೆರಳಿ", + "action.view": "ಈ ಪುಟದ ಮೂಲವನ್ನು ವೀಕ್ಷಿಸಿ", + "announce.dismiss": "ಇದನ್ನು ಮತ್ತೊಮ್ಮೆ ತೋರಿಸಬೇಡಿ", + "blog.archive": "ಹಳೆಯ ಲೇಖನ", + "blog.categories": "ವರ್ಗಗಳು", + "blog.categories.in": "ರಲ್ಲಿ", + "blog.continue": "ಓದು ಮುಂದುವರೆಸಿ", + "blog.draft": "ಆರಂಭಿಕ ಬರವಣಿಗೆ", + "blog.index": "ಸೂಚ್ಯಂಕಕ್ಕೆ ಹಿಂತಿರುಗಿ", + "blog.meta": "ಮಾಹಿತಿಯ ಬಗ್ಗೆ ಮಾಹಿತಿ", + "blog.references": "ಸಂಬಂಧಿತ ಉಲ್ಲೇಖಗಳು", + "clipboard.copy": "ಇದನ್ನು ನಕಲಿಸಿ", + "clipboard.copied": "ಇದನ್ನು ನಕಲು ಮಾಡಿದೆ", + "consent.accept": "ನಾನು ಇದನ್ನು ಒಪ್ಪಿಕೊಳ್ಳುತ್ತೇನೆ", + "consent.manage": "ಸಂರಚನೆಯನ್ನು ನಿರ್ವಹಿಸಿ", + "consent.reject": "ನಾನು ಇದನ್ನು ತಿರಸ್ಕರಿಸುತ್ತೇನೆ", + "footer": "ಅಡಿಟಿಪ್ಪಣಿ", + "footer.next": "ಮುಂದಿನ ಸಂಚಿಕೆ", + "footer.previous": "ಹಿಂದಿನ ಸಂಚಿಕೆ", + "header": "ಮೇಲ್ಟಿಪ್ಪಣಿ", + "meta.comments": "ಪ್ರತಿಕ್ರಿಯೆಗಳು", + "meta.source": "ಮೂಲ", + "nav": "ಸಂಚರಣೆ", + "readtime.one": "ಓದಲು ೧ ನಿಮಿಷ ತೆಗೆದುಕೊಳ್ಳುತ್ತದೆ", + "readtime.other": "ಓದಲು # ನಿಮಿಷಗಳನ್ನು ತೆಗೆದುಕೊಳ್ಳುತ್ತದೆ", + "rss.created": "ಆರ್ಎಸ್ಎಸ್ ಸೇವೆ", + "rss.updated": "ಆರ್ಎಸ್ಎಸ್ ಸೇವೆಯಿಂದ ಇತ್ತೀಚಿನ ನವೀಕರಣ", + "search": "ಹುಡುಕಿ", + "search.placeholder": "ಹುಡುಕಿ", + "search.share": "ಹಂಚಿಕೊಳ್ಳಿ", + "search.reset": "ಅಳಿಸು", + "search.result.initializer": "ಹುಡುಕಾಟವನ್ನು ಪ್ರಾರಂಭಿಸಲಾಗುತ್ತಿದೆ", + "search.result.placeholder": "ಬರೆಯುವ ಮೂಲಕ ಹುಡುಕಲು ಪ್ರಾರಂಭಿಸಿ", + "search.result.none": "ಹೊಂದಾಣಿಕೆಯಾಗುವ ದಾಖಲೆಗಳಿಲ್ಲ", + "search.result.one": "೧ ಹೊಂದಾಣಿಕೆಯ ದಾಖಲೆಯಿದೆ", + "search.result.other": "# ಹೊಂದಾಣಿಕೆಯ ದಾಖಲೆಗಳಿವೆ", + "search.result.more.one": "ಈ ಪುಟದಲ್ಲಿ ಇನ್ನೂ ಒಂದು ಕಂಡುಬಂದಿದೆ", + "search.result.more.other": "ಈ ಪುಟದಲ್ಲಿ ಇನ್ನೂ # ಇವೆ", + "search.result.term.missing": "ಕಾಣೆಯಾಗಿದೆ", + "select.language": "ಭಾಷೆಯನ್ನು ಆಯ್ಕೆಮಾಡಿ", + "select.version": "ಆವೃತ್ತಿಯನ್ನು ಆಯ್ಕೆಮಾಡಿ", + "source": "ಭಂಡಾರಕ್ಕೆ ಹೋಗಿ", + "source.file.contributors": "ಕೊಡುಗೆದಾರರು", + "source.file.date.created": "ರಚಿಸಿದ ದಿನಾಂಕ", + "source.file.date.updated": "ಕೊನೆಯ ನವೀಕರಣ ದಿನಾಂಕ", + "tabs": "ವಿವಿಧ ಕಿಟಕಿಗಳು", + "toc": "ವಿಷಯಗಳ ಪಟ್ಟಿ", + "top": "ಮೇಲಕ್ಕೆ ಹಿಂತಿರುಗಿ" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ko.html b/src/templates/partials/languages/ko.html new file mode 100644 index 00000000..adadccb7 --- /dev/null +++ b/src/templates/partials/languages/ko.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Korean --> +{% macro t(key) %}{{ { + "language": "ko", + "action.edit": "이 페이지를 편집", + "action.skip": "콘텐츠로 이동", + "action.view": "페이지소스 보기", + "announce.dismiss": "다시 안보기", + "blog.archive": "아카이브", + "blog.categories": "카테고리", + "blog.categories.in": "카테고리", + "blog.continue": "계속 읽기", + "blog.draft": "임시 저장", + "blog.index": "Index로 돌아가기", + "blog.meta": "메타데이터", + "blog.references": "관련 링크", + "clipboard.copy": "클립보드로 복사", + "clipboard.copied": "클립보드에 복사됨", + "consent.accept": "동의 허락", + "consent.manage": "동의 허락 관리", + "consent.reject": "동의 거부", + "footer": "하단/푸터", + "footer.next": "다음", + "footer.previous": "이전", + "header": "상단/헤더", + "meta.comments": "댓글", + "meta.source": "출처", + "nav": "네비게이션", + "readtime.one": "읽는시간 1분", + "readtime.other": "읽는시간 #분", + "rss.created": "RSS 피드 생성완료", + "rss.updated": "RSS 피드 업데이트완료", + "search": "검색", + "search.config.pipeline": " ", + "search.placeholder": "검색", + "search.share": "공유", + "search.reset": "지우기", + "search.result.initializer": "검색 초기화", + "search.result.placeholder": "검색어를 입력하세요", + "search.result.none": "검색어와 일치하는 문서가 없습니다", + "search.result.one": "1개의 일치하는 문서", + "search.result.other": "#개의 일치하는 문서", + "search.result.more.one": "이 문서에서 1개의 검색 결과 더 보기", + "search.result.more.other": "이 문서에서 #개의 검색 결과 더 보기", + "search.result.term.missing": "포함되지 않은 검색어", + "select.language": "언어설정", + "select.version": "버전 선택", + "source": "저장소로 이동", + "source.file.contributors": "참여자들", + "source.file.date.created": "작성일", + "source.file.date.updated": "마지막 업데이트", + "tabs": "탭", + "toc": "목차", + "top": "맨위로" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ku-IQ.html b/src/templates/partials/languages/ku-IQ.html new file mode 100644 index 00000000..fe9dd1e7 --- /dev/null +++ b/src/templates/partials/languages/ku-IQ.html @@ -0,0 +1,64 @@ +<!-- + 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. +--> + +<!-- Translations: Kurdish (Soranî) --> +{% macro t(key) %}{{ { + "language": "ku", + "direction": "rtl", + "action.edit": "دەستکاری ئەم پەڕە بکە", + "action.skip": "ئەم ناوەڕۆکە بپەڕێنە", + "action.view": "سەرچاوەی ئەم لاپەڕەیە نیشان بدە", + "announce.dismiss": "دووبارە ئەمە پیشان مەدە", + "clipboard.copy": "لەبەرگتنەوە بۆ کلیپبۆرد", + "clipboard.copied": "لەبەرگیرایەوە بۆ کلیپ بۆرد", + "consent.accept": "ڕازیبوون", + "consent.manage": "بەڕیوەبردنی ڕیکخستنەکان", + "consent.reject": "ڕەتکردنەوە", + "footer": "ژێرپەڕە", + "footer.next": "دواتر", + "footer.previous": "پێشتر", + "header": "ناونیشانی بەڕه", + "meta.comments": "لێدوانەکان", + "meta.source": "سەرجاوە", + "nav": "ڕێنیشاندەر", + "search": "گەڕان", + "search.config.pipeline": " ", + "search.placeholder": "گەڕان", + "search.share": "گەڕان", + "search.reset": "سڕینەوە", + "search.result.initializer": "ئامادەکردنی گەڕان", + "search.result.placeholder": "بنووسە بۆ دەستپێکردن بە گەڕان", + "search.result.none": "هیچ بەڵگەنامەیەکی هاوتا نیە", + "search.result.one": "١ بەڵگەنامەی هاوتا", + "search.result.other": "بەڵگەنامەی هاوتا #", + "search.result.more.one": "١ دانەی تر لەسەر ئەم پەڕەیە", + "search.result.more.other": "دانەی تر لەسەر ئەم پەڕەیە #", + "search.result.term.missing": "ونبوو", + "select.language": "زمان دیاریبکە", + "select.version": "وەشان دیاریبکە", + "source": "بڕۆ بۆ کۆگا", + "source.file.date.created": "دروسکت کرا", + "source.file.date.updated": "دوایین نوێکردنەوە", + "tabs": "تابەکان", + "toc": "خشتەی ناوەڕۆکەکان", + "top": "گەڕانەوە بۆ سەرەوە" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/lb.html b/src/templates/partials/languages/lb.html new file mode 100644 index 00000000..f38c6568 --- /dev/null +++ b/src/templates/partials/languages/lb.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Luxembourgish --> +{% macro t(key) %}{{ { + "language": "lb", + "direction": "ltr", + "action.edit": "D'Säit beaarbechten", + "action.skip": "Zum Inhalt iwwersprangen", + "action.view": "Quellcode uweisen", + "announce.dismiss": "Net erëm uweisen", + "blog.archive": "Archiv", + "blog.categories": "Kategorien", + "blog.categories.in": "an", + "blog.continue": "Weider liesen", + "blog.draft": "Skizz", + "blog.index": "Zeréck zum Index", + "blog.meta": "Metadaten", + "blog.references": "Änlech Links", + "clipboard.copy": "Kopéieren", + "clipboard.copied": "Kopéiert", + "consent.accept": "Accept", + "consent.manage": "Astellungen beaarbechten", + "consent.reject": "Ofleenen", + "footer": "Footer", + "footer.next": "Weider", + "footer.previous": "Zeréck", + "header": "Header", + "meta.comments": "Kommentaren", + "meta.source": "Quell", + "nav": "Navigatioun", + "readtime.one": "1 min Liesedauer", + "readtime.other": "# min Liesedauer", + "rss.created": "RSS feed", + "rss.updated": "RSS feed vun aktualiséiertem Inhalt", + "search": "Sichen", + "search.placeholder": "Sichen", + "search.share": "Deelen", + "search.reset": "Läschen", + "search.result.initializer": "D'Sich gëtt initialiséiert", + "search.result.placeholder": "Schreif fir eppes ze sichen", + "search.result.none": "Keng zoutreffend Dokumenter", + "search.result.one": "1 zoutreffend Dokument", + "search.result.other": "# zoutreffend Dokumenter", + "search.result.more.one": "1 méi op dëser Säit", + "search.result.more.other": "# méi op dëser Säit", + "search.result.term.missing": "Feelend", + "select.language": "Sprooch auswielen", + "select.version": "Versioun auswielen", + "source": "Op den Repository goen", + "source.file.contributors": "Matwirkender", + "source.file.date.created": "Erstallt", + "source.file.date.updated": "Läscht update", + "tabs": "Tabs", + "toc": "Inhaltsverzeichnis", + "top": "Zeréck zum Ufank" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/lt.html b/src/templates/partials/languages/lt.html new file mode 100644 index 00000000..129505f5 --- /dev/null +++ b/src/templates/partials/languages/lt.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Lithuanian --> +{% macro t(key) %}{{ { + "language": "lt", + "action.edit": "Redaguoti šį puslapį", + "action.skip": "Pereiti prie turinio", + "action.view": "Žiūrėti puslapio šaltinius", + "announce.dismiss": "Daugiau neberodyti", + "blog.archive": "Archyvas", + "blog.categories": "Kategorijos", + "blog.categories.in": "į", + "blog.continue": "Skaityti toliau", + "blog.draft": "Ruošinys", + "blog.index": "Grįžti į indeksą", + "blog.meta": "Meta duomenys", + "blog.references": "Susieja saitai", + "clipboard.copy": "Kopijuoti į iškarpinę", + "clipboard.copied": "Nukopijuota į iškarpinę", + "consent.accept": "Sutikti", + "consent.manage": "Redaguoti nustatymus", + "consent.reject": "Atmesti", + "footer": "Poraštė", + "footer.next": "Sekantis", + "footer.previous": "Ankstesnis", + "header": "Antraštė", + "meta.comments": "Komentarai", + "meta.source": "Išeitinis kodas", + "nav": "Navigacija", + "readtime.one": "1 min skaitymo", + "readtime.other": "# min skaitymo", + "rss.created": "RSS šaltinis", + "rss.updated": "RSS šaltinis atnaujinimams", + "search": "Paieška", + "search.config.pipeline": " ", + "search.placeholder": "Paieška", + "search.share": "Dalintis", + "search.reset": "Išvalyti", + "search.result.initializer": "Paieškos inicijavimas", + "search.result.placeholder": "Įveskite norėdami pradėti paiešką", + "search.result.none": "Atitinkančių dokumentų nerasta", + "search.result.one": "1 atitinkantis dokumentas", + "search.result.other": "# atitinkantys dokumentai", + "search.result.more.one": "Dar 1 šiame puslapyje", + "search.result.more.other": "Dar # šiame puslapyje", + "search.result.term.missing": "Nerasta", + "select.language": "Pasirinkti kalbą", + "select.version": "Pasrinkti versiją", + "source": "Eiti į saugyklą", + "source.file.contributors": "Dalininkai", + "source.file.date.created": "Sukurta", + "source.file.date.updated": "Paskutinis atnaujinimas", + "tabs": "Skirtukai", + "toc": "Turinys", + "top": "Grįžti į viršų" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/lv.html b/src/templates/partials/languages/lv.html new file mode 100644 index 00000000..7bcd9ace --- /dev/null +++ b/src/templates/partials/languages/lv.html @@ -0,0 +1,55 @@ +<!-- + 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. +--> + +<!-- Translations: Latvian --> +{% macro t(key) %}{{ { + "language": "lv", + "action.edit": "Rediģēt šo lapu", + "action.skip": "Pāriet uz saturu", + "clipboard.copy": "Kopēt starpliktuvē", + "clipboard.copied": "Kopēts starpliktuvē", + "footer": "Kājene", + "footer.next": "Nākamais", + "footer.previous": "Iepriekšējais", + "header": "Galvene", + "meta.comments": "Komentārs", + "meta.source": "Avots", + "nav": "Navigācija", + "search.placeholder": "Meklēt", + "search.reset": "Notīrīt", + "search.result.initializer": "Notiek meklēšanas inicializācija", + "search.result.placeholder": "Ierakstiet, lai sāktu meklēšanu", + "search.result.none": "Nav atbilstošu dokumentu", + "search.result.one": "1 atbilstošs dokuments", + "search.result.other": "# atbilstoši dokumenti ", + "search.result.more.one": "1 šajā lapā", + "search.result.more.other": "# un vairāk šajā lapā", + "search.result.term.missing": "Trūkstošs", + "select.language": "Izvēlies valodu", + "select.version": "Izvēlies versiju", + "source": "Doties uz repozitoriju", + "source.file.date.created": "Izveidots", + "source.file.date.updated": "Pēdējoreiz atjaunots", + "tabs": "Cilnes", + "toc": "Satura rādītājs", + "top": "Atpakaļ uz augšu" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/mk.html b/src/templates/partials/languages/mk.html new file mode 100644 index 00000000..e3dc114c --- /dev/null +++ b/src/templates/partials/languages/mk.html @@ -0,0 +1,56 @@ +<!-- + 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. +--> + +<!-- Translations: Macedonian --> +{% macro t(key) %}{{ { + "language": "mk", + "action.edit": "Уредете ја оваа страница", + "action.skip": "Прескокнете до содржината", + "clipboard.copy": "Копирај во таблата", + "clipboard.copied": "Копирано", + "footer": "Подножје", + "footer.next": "Следно", + "footer.previous": "Претходно", + "header": "Заглавје", + "meta.comments": "Коментари", + "meta.source": "Извор", + "nav": "Наслов за навигација", + "search.config.lang": "ru", + "search.placeholder": "Пребарување", + "search.reset": "Чисти", + "search.result.initializer": "Иницијализирање на пребарувањето", + "search.result.placeholder": "Напишете за да започнете со пребарување", + "search.result.none": "Нема соодветни документи", + "search.result.one": "1 документ што се совпаѓа", + "search.result.other": "# соодветни документи", + "search.result.more.one": "Уште 1 на оваа страница", + "search.result.more.other": "Уште # на оваа страница", + "search.result.term.missing": "Недостасува", + "select.language": "Изберете јазик", + "select.version": "Изберете верзија", + "source": "Одете до складиштето", + "source.file.date.created": "Создаден", + "source.file.date.updated": "Последно ажурирање", + "tabs": "Јазичиња", + "toc": "Содржина", + "top": "Вратете се на почетокот" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/mn.html b/src/templates/partials/languages/mn.html new file mode 100644 index 00000000..de9002ab --- /dev/null +++ b/src/templates/partials/languages/mn.html @@ -0,0 +1,51 @@ +<!-- + 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. +--> + +<!-- Translations: Mongolian --> +{% macro t(key) %}{{ { + "language": "mn", + "action.edit": "Хуудас засварлах", + "action.skip": "Агуулгыг алгасах", + "clipboard.copy": "Хуулах", + "clipboard.copied": "Санах ойд хуулах", + "footer": "Хөл", + "footer.next": "Дараах", + "footer.previous": "Өмнөх", + "header": "Толгой", + "meta.comments": "Сэтгэгдэл", + "meta.source": "Эх үүсвэр", + "nav": "Чиглүүлэгч", + "search.config.lang": "ru", + "search.placeholder": "Хайлт", + "search.reset": "Цэвэрлэх", + "search.result.placeholder": "Хайлтын үгээ бичнэ үү", + "search.result.none": "Таарц илэрсэнгүй", + "search.result.one": "1 таарц илэрлээ", + "search.result.other": "# Тохирох баримт бичиг", + "search.result.more.one": "1 илүү хуудас байна", + "search.result.more.other": "# илүү хуудас байна", + "source": "Хадгалах сан руу очих", + "source.file.date.created": "Үүсгэсэн", + "source.file.date.updated": "Сүүлийн шинэчлэлт", + "tabs": "Табууд", + "toc": "Агуулга" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ms.html b/src/templates/partials/languages/ms.html new file mode 100644 index 00000000..57b70fc7 --- /dev/null +++ b/src/templates/partials/languages/ms.html @@ -0,0 +1,55 @@ +<!-- + 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. +--> + +<!-- Translations: Bahasa Malaysia --> +{% macro t(key) %}{{ { + "language": "ms", + "action.edit": "Edit halaman ini", + "action.skip": "Langkau tajuk talian", + "clipboard.copy": "Salin ke papan keratan", + "clipboard.copied": "Disalin ke papan keratan", + "footer": "Pengaki", + "footer.next" : "Seterusnya", + "footer.previous": "Sebelumnya", + "header": "Pengepala", + "meta.comments": "Komen", + "meta.source": "Sumber", + "nav": "Navigasi", + "search.placeholder": "Cari", + "search.reset": "Padam", + "search.result.initializer": "Siap carian", + "search.result.placeholder": "Taip untuk mula mencari", + "search.result.none": "Tiada dokumen yang sepadan", + "search.result.one": "1 dokumen yang sepadan", + "search.result.other": "# dokumen yang sepadan", + "search.result.more.one": "1 lagi di halaman ini", + "search.result.more.other": "# lagi di halaman ini", + "search.result.term.missing": "Hilang", + "select.language": "Pilih bahasa", + "select.version": "Pilih versi", + "source": "tajuk talian asal", + "source.file.date.created": "tarikh fil asal dicipta", + "source.file.date.updated": "Tarikh fil dikemas kini", + "tabs": "Tab", + "toc": "Jadual kandungan", + "top": "Kembali ke atas" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/my.html b/src/templates/partials/languages/my.html new file mode 100644 index 00000000..27ca3ad9 --- /dev/null +++ b/src/templates/partials/languages/my.html @@ -0,0 +1,49 @@ +<!-- + 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. +--> + +<!-- Translations: Burmese --> +{% macro t(key) %}{{ { + "language": "my", + "action.edit": "ဤ စာမျက်နှာကို ပြင်ရန်", + "action.skip": "မာတိကာ သို့ သွားရန်", + "clipboard.copy": "ကလစ်ဘုတ် သို့ ကူးယူရန်", + "clipboard.copied": "ကလစ်ဘုတ် သို့ ကူယူပြီး", + "footer": "အောက်ခြေ", + "footer.next": "ရှေ့သို့", + "footer.previous": "နောက်သို့", + "header": "ခေါင်းပိုင်း", + "meta.comments": "မှတ်ချက်များ", + "meta.source": "ရင်းမြစ်", + "nav": "လမ်းညွှန်", + "search.config.pipeline": " ", + "search.placeholder": "ရှာရန်", + "search.reset": "ရှင်းလင်း", + "search.result.placeholder": "ရှာဖွေခြင်းစရန် စာရိုက်ပါ", + "search.result.none": "တူညီသော စာရွက်စာတမ်းများ မရှိပါ", + "search.result.one": "စာရွက်စာတမ်း ၁ ခု တူညီသည်", + "search.result.other": "စာရွက်စာတမ်း # ခု တူညီသည်", + "source": "repository သို့ သွားရန်", + "source.file.date.created": "နေပြည်တော်", + "source.file.date.updated": "နောက်ဆုံး ထုတ်ပြန်ချက်", + "tabs": "တက်များ", + "toc": "ပါဝင်အကြောင်းအရာများ" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/nb.html b/src/templates/partials/languages/nb.html new file mode 100644 index 00000000..6be63531 --- /dev/null +++ b/src/templates/partials/languages/nb.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Norwegian Bokmål --> +{% macro t(key) %}{{ { + "language": "nb", + "action.edit": "Rediger denne siden", + "action.skip": "Gå til innhold", + "action.view": "Vis kildekoden til denne siden", + "announce.dismiss": "Ikke vis dette igjen", + "blog.archive": "Arkiv", + "blog.categories": "Kategorier", + "blog.categories.in": "i", + "blog.continue": "Fortsett å lese", + "blog.draft": "Kladd", + "blog.index": "Tilbake til oversikt", + "blog.meta": "Metadata", + "blog.references": "Relaterte lenker", + "clipboard.copy": "Kopier til utklippstavlen", + "clipboard.copied": "Kopiert til utklippstavlen", + "consent.accept": "Akseptert", + "consent.manage": "Innstillinger", + "consent.reject": "Reject", + "footer": "Footer", + "footer.next": "Neste", + "footer.previous": "Forrige", + "header": "Header", + "meta.comments": "Kommentarer", + "meta.source": "Kilde", + "nav": "Navigasjon", + "readtime.one": "lesteid: 1 min", + "readtime.other": "lesetid: # min", + "rss.created": "RSS feed", + "rss.updated": "Oppdatert RSS feed", + "search": "Søk", + "search.config.lang": "no", + "search.placeholder": "Søk", + "search.share": "Del", + "search.reset": "Nullstill", + "search.result.initializer": "Starter søk", + "search.result.placeholder": "Skriv søkeord", + "search.result.none": "Ingen treff", + "search.result.one": "1 treff", + "search.result.other": "# treff", + "search.result.more.one": "1 til på denne siden", + "search.result.more.other": "# flere på denne siden", + "search.result.term.missing": "Mangler", + "select.language": "Velg språk", + "select.version": "Velg versjon", + "source": "Gå til kilde", + "source.file.contributors": "Bidragsytere", + "source.file.date.created": "Opprettet", + "source.file.date.updated": "Sist oppdatert", + "tabs": "Faner", + "toc": "Innholdsliste", + "top": "Tilbake til toppen" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/nl.html b/src/templates/partials/languages/nl.html new file mode 100644 index 00000000..0000fe60 --- /dev/null +++ b/src/templates/partials/languages/nl.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Dutch --> +{% macro t(key) %}{{ { + "language": "nl", + "action.edit": "Wijzig deze pagina", + "action.skip": "Ga naar inhoud", + "action.view": "Bron van deze pagina bekijken", + "announce.dismiss": "Niet meer laten zien", + "blog.archive": "Archief", + "blog.categories": "Categorieën", + "blog.categories.in": "in", + "blog.continue": "Doorgaan met lezen", + "blog.draft": "Concept", + "blog.index": "Terug naar de inhoudsopgave", + "blog.meta": "Metadata", + "blog.references": "Gerelateerde links", + "clipboard.copy": "Kopiëren naar klembord", + "clipboard.copied": "Gekopieerd naar klembord", + "consent.accept": "Accepteren", + "consent.manage": "Instellingen", + "consent.reject": "Afwijzen", + "footer": "Footer", + "footer.next": "Volgende", + "footer.previous": "Vorige", + "header": "Header", + "meta.comments": "Reacties", + "meta.source": "Bron", + "nav": "Navigatie", + "readtime.one": "1 min leestijd", + "readtime.other": "# min leestijd", + "rss.created": "RSS feed", + "rss.updated": "RSS feed met geüpdatet inhoud", + "search": "Zoeken", + "search.config.lang": "nl", + "search.placeholder": "Zoeken", + "search.share": "Delen", + "search.reset": "Leegmaken", + "search.result.initializer": "Zoeken initialiseren", + "search.result.placeholder": "Typ om te beginnen met zoeken", + "search.result.none": "Geen overeenkomende documenten", + "search.result.one": "1 overeenkomende document", + "search.result.other": "# overeenkomende documenten", + "search.result.more.one": "1 extra overeenkomst op deze pagina", + "search.result.more.other": "# extra overeenkomsten op deze pagina", + "search.result.term.missing": "Ontbreekt", + "select.language": "Selecteer taal", + "select.version": "Selecteer versie", + "source": "Ga naar repository", + "source.file.contributors": "Bijdragers", + "source.file.date.created": "Gecreëerd", + "source.file.date.updated": "Laatst geüpdatet", + "tabs": "Tabs", + "toc": "Inhoudsopgave", + "top": "Terug naar boven" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/nn.html b/src/templates/partials/languages/nn.html new file mode 100644 index 00000000..9478bdbe --- /dev/null +++ b/src/templates/partials/languages/nn.html @@ -0,0 +1,62 @@ +<!-- + 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. +--> + +<!-- Translations: Norwegian Nynorsk --> +{% macro t(key) %}{{ { + "language": "nn", + "action.edit": "Rediger denne sida", + "action.skip": "Gå til innhald", + "announce.dismiss": "Ikkje vis dette att", + "clipboard.copy": "Kopier til utklippstavla", + "clipboard.copied": "Kopiert til utklippstavla", + "consent.accept": "Akseptert", + "consent.manage": "Innstillinger", + "consent.reject": "Reject", + "footer": "Footer", + "footer.next": "Neste", + "footer.previous": "Førre", + "header": "Header", + "meta.comments": "Kommentarar", + "meta.source": "Kjelde", + "nav": "Navigasjon", + "search": "Søk", + "search.config.lang": "no", + "search.placeholder": "Søk", + "search.share": "Del", + "search.reset": "Nullstill", + "search.result.initializer": "Startar søk", + "search.result.placeholder": "Skriv søkeord", + "search.result.none": "Ingen treff", + "search.result.one": "1 treff", + "search.result.other": "# treff", + "search.result.more.one": "1 til på denne sida", + "search.result.more.other": "# fleire på denne sida", + "search.result.term.missing": "Manglar", + "select.language": "Vel språk", + "select.version": "Vel versjon", + "source": "Gå til kjelde", + "source.file.date.created": "Oppretta", + "source.file.date.updated": "Sist oppdatert", + "tabs": "Faner", + "toc": "Innhaldsliste", + "top": "Tilbake til toppen" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/pl.html b/src/templates/partials/languages/pl.html new file mode 100644 index 00000000..7817633a --- /dev/null +++ b/src/templates/partials/languages/pl.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Polish --> +{% macro t(key) %}{{ { + "language": "pl", + "action.edit": "Edytuj tę stronę", + "action.skip": "Przejdź do treści", + "action.view": "Zobacz kod źródłowy tej strony", + "announce.dismiss": "Nie pokazuj tego ponownie", + "blog.archive": "Archiwum", + "blog.categories": "Kategorie", + "blog.categories.in": "", + "blog.continue": "Czytaj dalej", + "blog.draft": "Wersja robocza", + "blog.index": "Powrót do indeksu", + "blog.meta": "Metadane", + "blog.references": "Powiązane łącza", + "clipboard.copy": "Kopiuj do schowka", + "clipboard.copied": "Skopiowano do schowka", + "consent.accept": "Akceptuj", + "consent.manage": "Zarządzaj ustawieniami", + "consent.reject": "Odrzuć", + "footer": "Stopka", + "footer.next": "Następna strona", + "footer.previous": "Poprzednia strona", + "header": "Nagłówek", + "meta.comments": "Komentarze", + "meta.source": "Kod źródłowy", + "nav": "Nawigacja", + "readtime.one": "Czas czytania: 1 min", + "readtime.other": "Czas czytania: # min", + "rss.created": "Kanał RSS", + "rss.updated": "Kanał RSS zaktualizowanych treści", + "search": "Szukaj", + "search.config.pipeline": " ", + "search.placeholder": "Szukaj", + "search.share": "Udostępnij", + "search.reset": "Wyczyść", + "search.result.initializer": "Inicjowanie wyszukiwania", + "search.result.placeholder": "Zacznij pisać, aby szukać", + "search.result.none": "Brak wyników wyszukiwania", + "search.result.one": "Wyniki wyszukiwania: 1", + "search.result.other": "Wyniki wyszukiwania: #", + "search.result.more.one": "1 więcej na tej stronie", + "search.result.more.other": "# więcej na tej stronie", + "search.result.term.missing": "Brak", + "select.language": "Wybierz język", + "select.version": "Wybierz wersję", + "source": "Przejdź do repozytorium", + "source.file.contributors": "Kontrybutorzy", + "source.file.date.created": "Utworzony", + "source.file.date.updated": "Ostatnia aktualizacja", + "tabs": "Zakładki", + "toc": "Spis treści", + "top": "Powrót do góry" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/pt-BR.html b/src/templates/partials/languages/pt-BR.html new file mode 100644 index 00000000..d934a9ac --- /dev/null +++ b/src/templates/partials/languages/pt-BR.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Portuguese (Brasilian) --> +{% macro t(key) %}{{ { + "language": "pt", + "action.edit": "Editar esta página", + "action.skip": "Pular para conteúdo", + "action.view": "Exibir fonte desta página", + "announce.dismiss": "Não mostrar isso novamente", + "blog.archive": "Arquivo", + "blog.categories": "Categorias", + "blog.categories.in": "em", + "blog.continue": "Continuar leitura", + "blog.draft": "Rascunho", + "blog.index": "Voltar ao índice", + "blog.meta": "Metadados", + "blog.references": "Links relacionados", + "clipboard.copy": "Copiar para área de transferência", + "clipboard.copied": "Copiado para área de transferência", + "consent.accept": "Aceitar", + "consent.manage": "Gerenciar configurações", + "consent.reject": "Rejeitar", + "footer": "Rodapé", + "footer.next": "Próximo", + "footer.previous": "Anterior", + "header": "Cabeçalho", + "meta.comments": "Comentários", + "meta.source": "Origem", + "nav": "Navegação", + "readtime.one": "1 min de leitura", + "readtime.other": "# min de leitura", + "rss.created": "RSS feed", + "rss.updated": "RSS feed de conteúdo atualizado", + "search": "Pesquisar", + "search.config.lang": "pt", + "search.placeholder": "Buscar", + "search.share": "Compartilhar", + "search.reset": "Limpar", + "search.result.initializer": "Inicializando busca", + "search.result.placeholder": "Digite para iniciar a busca", + "search.result.none": "Nenhum documento encontrado", + "search.result.one": "1 documento encontrado", + "search.result.other": "# documentos encontrados", + "search.result.more.one": "mais 1 nesta página", + "search.result.more.other": "# mais nesta página", + "search.result.term.missing": "Ausente", + "select.language": "Selecione o idioma", + "select.version": "Selecione a versão", + "source": "Ir para repositório", + "source.file.contributors": "Contribuidores", + "source.file.date.created": "Criado em", + "source.file.date.updated": "Última atualização", + "tabs": "Abas", + "toc": "Índice", + "top": "Voltar para o topo" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/pt.html b/src/templates/partials/languages/pt.html new file mode 100644 index 00000000..e5dee1cb --- /dev/null +++ b/src/templates/partials/languages/pt.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Portuguese --> +{% macro t(key) %}{{ { + "language": "pt", + "action.edit": "Editar esta página", + "action.skip": "Ir para o conteúdo", + "action.view": "Ver fonte desta página", + "announce.dismiss": "Não mostrar novamente", + "blog.archive": "Arquivo", + "blog.categories": "Categorias", + "blog.categories.in": "em", + "blog.continue": "Continuar leitura", + "blog.draft": "Rascunho", + "blog.index": "Voltar ao índice", + "blog.meta": "Metadados", + "blog.references": "Ligações relacionadas", + "clipboard.copy": "Copiar para área de transferência", + "clipboard.copied": "Copiado para área de transferência", + "consent.accept": "Aceitar", + "consent.manage": "Gerir configurações", + "consent.reject": "Rejeitar", + "footer": "Rodapé", + "footer.next": "Próximo", + "footer.previous": "Anterior", + "header": "Cabeçalho", + "meta.comments": "Comentários", + "meta.source": "Fonte", + "nav": "Navegação", + "readtime.one": "1 min de leitura", + "readtime.other": "# min de leitura", + "rss.created": "canal RSS", + "rss.updated": "canal RSS com conteúdo atualizado", + "search": "Pesquisar", + "search.config.lang": "pt", + "search.placeholder": "Buscar", + "search.share": "Compartilhar", + "search.reset": "Limpar", + "search.result.initializer": "Inicializando a pesquisa", + "search.result.placeholder": "Digite para iniciar a busca", + "search.result.none": "Nenhum resultado encontrado", + "search.result.one": "1 resultado encontrado", + "search.result.other": "# resultados encontrados", + "search.result.more.one": "Mais 1 nesta página", + "search.result.more.other": "Mais # nesta página", + "search.result.term.missing": "Ausente", + "select.language": "Selecione o idioma", + "select.version": "Selecione a versão", + "source": "Ir ao repositório", + "source.file.contributors": "Colaboradores", + "source.file.date.created": "Criada", + "source.file.date.updated": "Última atualização", + "tabs": "Abas", + "toc": "Índice", + "top": "Voltar ao topo" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ro.html b/src/templates/partials/languages/ro.html new file mode 100644 index 00000000..7bea9afb --- /dev/null +++ b/src/templates/partials/languages/ro.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Romanian --> +{% macro t(key) %}{{ { + "language": "ro", + "action.edit": "Editeaza această pagină", + "action.skip": "Sari la conținut", + "action.view": "Vezi sursa acestei pagini", + "announce.dismiss": "Nu mai arăta asta", + "blog.archive": "Arhivează", + "blog.categories": "Categorii", + "blog.categories.in": "în", + "blog.continue": "Continuă să citești", + "blog.draft": "Ciornă", + "blog.index": "Înapoi la index", + "blog.meta": "Metadata", + "blog.references": "Link-uri relevante", + "clipboard.copy": "Copiază în clipboard", + "clipboard.copied": "Copiat în clipboard", + "consent.accept": "Accept", + "consent.manage": "Gestionați setările", + "consent.reject": "Refuz", + "footer": "Subsol", + "footer.next": "Următor", + "footer.previous": "Anterior", + "header": "Antet", + "meta.comments": "Comentarii", + "meta.source": "Sursă", + "nav": "Navigație", + "readtime.one": "1 minut de citit", + "readtime.other": "# minut de citit", + "rss.created": "Flux RSS", + "rss.updated": "Flux RSS cu conținut actualizat", + "search": "Caută", + "search.config.lang": "ro", + "search.placeholder": "Căutare", + "search.share": "Distribuie", + "search.reset": "Resetează", + "search.result.initializer": "Inițializare căutare", + "search.result.placeholder": "Tastează pentru a începe căutarea", + "search.result.none": "Nu a fost găsit niciun document", + "search.result.one": "1 document găsit", + "search.result.other": "# documente găsite", + "search.result.more.one": "Încă 1 pe această pagină", + "search.result.more.other": "Încă # pe această pagină", + "search.result.term.missing": "Lipsă", + "select.language": "Selectează limba", + "select.version": "Selectează versuine", + "source": "Accesează repository-ul", + "source.file.contributors": "Contribuitori", + "source.file.date.created": "Creată", + "source.file.date.updated": "Ultima actualizare", + "tabs": "File", + "toc": "Cuprins", + "top": "Înapoi sus" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ru.html b/src/templates/partials/languages/ru.html new file mode 100644 index 00000000..ddbd7b95 --- /dev/null +++ b/src/templates/partials/languages/ru.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Russian --> +{% macro t(key) %}{{ { + "language": "ru", + "action.edit": "Редактировать страницу", + "action.skip": "Перейти к содержанию", + "action.view": "Посмотреть исходный код страницы", + "announce.dismiss": "Больше не показывать", + "blog.archive": "Архив", + "blog.categories": "Категории", + "blog.categories.in": "В", + "blog.continue": "Читать", + "blog.draft": "Черновик", + "blog.index": "На главную", + "blog.meta": "Метаданные", + "blog.references": "Ссылки", + "clipboard.copy": "Копировать в буфер", + "clipboard.copied": "Скопировано в буфер", + "consent.accept": "Принять", + "consent.manage": "Управлять настройками", + "consent.reject": "Отклонить", + "footer": "Нижний колонтитул", + "footer.next": "Вперед", + "footer.previous": "Назад", + "header": "Верхний колонтитул", + "meta.comments": "Комментарии", + "meta.source": "Исходный код", + "nav": "Навигация", + "readtime.one": "Читать 1 минуту", + "readtime.other": "Читать # минут", + "rss.created": "RSS канал", + "rss.updated": "RSS канал с новым контентом", + "search": "Поиск", + "search.config.lang": "ru", + "search.placeholder": "Поиск", + "search.share": "Поделиться", + "search.reset": "Очистить", + "search.result.initializer": "Инициализация поиска", + "search.result.placeholder": "Начните печатать для поиска", + "search.result.none": "Совпадений не найдено", + "search.result.one": "Найдено 1 совпадение", + "search.result.other": "Найдено совпадений: #", + "search.result.more.one": "Ещё 1 на этой странице", + "search.result.more.other": "Ещё # на этой странице", + "search.result.term.missing": "Отсутствует", + "select.language": "Выберите язык", + "select.version": "Выберите версию", + "source": "Перейти к репозиторию", + "source.file.contributors": "Участники", + "source.file.date.created": "Дата создания", + "source.file.date.updated": "Последнее обновление", + "tabs": "Вкладки", + "toc": "Содержание", + "top": "К началу" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/sa.html b/src/templates/partials/languages/sa.html new file mode 100644 index 00000000..338e2b61 --- /dev/null +++ b/src/templates/partials/languages/sa.html @@ -0,0 +1,75 @@ +<!-- + 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. +--> + +<!-- Translations: Sanskrit --> +{% macro t(key) %}{{ { + "language": "sa", + "action.edit": "एतत् पृष्ठं सम्पादयतु", + "action.skip": "सामग्रीं त्यजन्तु", + "action.view": "अस्य पृष्ठस्य स्रोतः पश्यन्तु", + "announce.dismiss": "एतत् पुनः न दर्शयतु", + "blog.archive": "लेखागार", + "blog.categories": "श्रेणियाँ", + "blog.categories.in": "इत्यस्मिन्", + "blog.continue": "पठनं निरन्तरं कुर्वन्तु", + "blog.draft": "प्रारूप", + "blog.index": "अनुक्रमणिकां प्रति पुनः आगच्छन्तु", + "blog.meta": "परिदत्तांश", + "blog.references": "सन्दर्भाः", + "clipboard.copy": "एतत् प्रतिलिख्यताम्", + "clipboard.copied": "प्रतिलिपितः भवति", + "consent.accept": "अहं तत् स्वीकुर्वन् अस्मि", + "consent.manage": "वविन्यासं प्रबन्धयन्तु", + "consent.reject": "अहं तत् निराकरोमि", + "footer": "पादलेखः", + "footer.next": "अग्रिमः", + "footer.previous": "पूर्वकृत", + "header": "शीर्षकम्", + "meta.comments": "विचाराः", + "meta.source": "स्रोतः", + "nav": "मार्गदर्शनम्", + "readtime.one": "१ निमेषं पठितुं", + "readtime.other": "# निमेषं पठितुं", + "rss.created": "आरएसएस सेवा", + "rss.updated": "आरएसएस सेवातः नवीनतमं अद्यतनम्", + "search": "अन्वेषण", + "search.placeholder": "अन्वेषण", + "search.share": "भजतु", + "search.reset": "तत् स्वच्छं कुर्वन्तु", + "search.result.initializer": "अन्वेषणस्य आरम्भः", + "search.result.placeholder": "अन्वेषणं आरभ्य लिखन्तु", + "search.result.none": "अभिलेखाः नास्ति", + "search.result.one": "१ अभिलेखः अस्ति", + "search.result.other": "# अभिलेखाः सन्ति", + "search.result.more.one": "अस्मिन् पृष्ठे १ अन्यः अस्ति", + "search.result.more.other": "अस्मिन् पृष्ठे # अन्ये सन्ति", + "search.result.term.missing": "शून्य", + "select.language": "भाषां चिनोतु", + "select.version": "संस्करणं चिनोतु", + "source": "भण्डारं गच्छन्तु", + "source.file.contributors": "अंशदाता", + "source.file.date.created": "ननिर्माणस्य तिथिः", + "source.file.date.updated": "परिवर्तनस्य तिथिः", + "tabs": "पट्टाः", + "toc": "सामग्रीसारणी", + "top": "पुनः उपरिभागं प्रति गच्छन्तु" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/sh.html b/src/templates/partials/languages/sh.html new file mode 100644 index 00000000..42a0d902 --- /dev/null +++ b/src/templates/partials/languages/sh.html @@ -0,0 +1,70 @@ +<!-- + 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. +--> + +<!-- Translations: Serbo-Croatian --> +{% macro t(key) %}{{ { + "language": "sh", + "action.edit": "Ažuriraj stranicu", + "action.skip": "Idi na tekst", + "action.view": "Pogledaj izvorni kod ove stranice", + "announce.dismiss": "Nemoj mi ponovo pokazati ovo", + "blog.archive": "Arhiva", + "blog.categories": "Kategorije", + "blog.categories.in": "u", + "blog.continue": "Nastavi sa čitanjem", + "blog.meta": "Metapodaci", + "blog.references": "Povezani linkovi", + "clipboard.copy": "Kopiraj u klipbord", + "clipboard.copied": "Iskopirano u klipbord", + "consent.accept": "Prihvati", + "consent.manage": "Promeni podešavanja", + "consent.reject": "Odbij", + "footer": "Podnožje", + "footer.next": "Sledeće", + "footer.previous": "Prethodno", + "header": "Zaglavlje", + "meta.comments": "Komentari", + "meta.source": "Izvor", + "nav": "Navigacija", + "readtime.one": "1 minut čitanja", + "readtime.other": "# minuta čitanja", + "search": "Pretraga", + "search.placeholder": "Pretraga", + "search.share": "Deljenje", + "search.reset": "Očisti", + "search.result.initializer": "Inicijalizujem pretragu", + "search.result.placeholder": "Unesite pojam pretrage", + "search.result.none": "Ništa nije pronađeno", + "search.result.one": "1 rezultat pretrage", + "search.result.other": "# rezultata pretrage", + "search.result.more.one": "još 1 na ovoj strani", + "search.result.more.other": "još # na ovoj strani", + "search.result.term.missing": "Nedostaje", + "select.language": "Izaberi jezik", + "select.version": "Izaberi verziju", + "source": "Idi u repozitorijum", + "source.file.date.created": "Kreiran", + "source.file.date.updated": "Ažuriran", + "tabs": "Tabovi", + "toc": "Sadržaj", + "top": "Nazad na vrh" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/si.html b/src/templates/partials/languages/si.html new file mode 100644 index 00000000..eb41309e --- /dev/null +++ b/src/templates/partials/languages/si.html @@ -0,0 +1,51 @@ +<!-- + 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. +--> + +<!-- Translations: Sinhalese --> +{% macro t(key) %}{{ { + "language": "si", + "action.edit": "පිටුව සංස්කරණය", + "action.skip": "අන්තර්ගතය වෙත යන්න", + "clipboard.copy": "කොපි කරන්න", + "clipboard.copied": "කොපි කළා", + "footer": "පාදම", + "footer.next": "මීළඟ", + "footer.previous": "පසුගිය", + "header": "ශීර්ෂය", + "meta.comments": "ප්රතිචාර", + "meta.source": "මූලාශ්රය", + "nav": "යාත්රණය", + "search.config.pipeline": " ", + "search.placeholder": "සොයන්න", + "search.reset": "මකන්න", + "search.result.placeholder": "සෙවීමට ටයිප් කරන්න", + "search.result.none": "කිසිවක් හමු නොවුණි", + "search.result.one": "1 ගැලපෙන ගොනුවක්", + "search.result.other": "ගැලපෙන ගොනු # ක්", + "search.result.more.one": "තව 1 ප්රතිඵලයක්", + "search.result.more.other": "තව ප්රතිඵල # ක්", + "source": "රිපොසිටරියට යන්න", + "source.file.date.created": "ٺاھيو ويو", + "source.file.date.updated": "අවසන් යාවත්කාලීන වීම", + "tabs": "ටැබ්ස්", + "toc": "පටුන" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/sk.html b/src/templates/partials/languages/sk.html new file mode 100644 index 00000000..701a5a53 --- /dev/null +++ b/src/templates/partials/languages/sk.html @@ -0,0 +1,43 @@ +<!-- + 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. +--> + +<!-- Translations: Slovak --> +{% macro t(key) %}{{ { + "language": "sk", + "action.edit": "Upraviť túto stránku", + "action.skip": "Preskočiť na obsah", + "clipboard.copy": "Kopírovať do schránky", + "clipboard.copied": "Skopírované do schránky", + "footer.next": "Ďalej", + "footer.previous": "Späť", + "meta.comments": "Komentáre", + "meta.source": "Zdroj", + "search.placeholder": "Hľadať", + "search.result.placeholder": "Pre vyhľadávanie začni písať", + "search.result.none": "Žiadne vyhovujúce dokumenty", + "search.result.one": "Vyhovujúci dokument: 1", + "search.result.other": "Vyhovujúce dokumenty: #", + "source": "Zobraziť repozitár", + "source.file.date.created": "Vytvorené", + "source.file.date.updated": "Posledná aktualizácia", + "toc": "Obsah" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/sl.html b/src/templates/partials/languages/sl.html new file mode 100644 index 00000000..a01f31d9 --- /dev/null +++ b/src/templates/partials/languages/sl.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Slovenian --> +{% macro t(key) %}{{ { + "language": "sl", + "action.edit": "Uredi stran", + "action.skip": "Skoči na vsebino", + "action.view": "Prikaži izvorno stran", + "announce.dismiss": "Ne prikaži več", + "blog.archive": "Arhiv", + "blog.categories": "Kategorije", + "blog.categories.in": "v", + "blog.continue": "Nadaljuj z branjem", + "blog.draft": "Osnutek", + "blog.index": "Nazaj na kazalo", + "blog.meta": "Metapodatki", + "blog.references": "Sorodne povezave", + "clipboard.copy": "Kopiraj v odložišče", + "clipboard.copied": "Kopirano v odložišče", + "consent.accept": "Sprejmi", + "consent.manage": "Uredi nastavitve", + "consent.reject": "Zavrni", + "footer": "Glava", + "footer.next": "Naslednja stran", + "footer.previous": "Prejšnja stran", + "header": "Noga", + "meta.comments": "Komentarji", + "meta.source": "Izvorna koda", + "nav": "Navigacija", + "readtime.one": "Čas branja: 1 min", + "readtime.other": "Čas branja: # min", + "rss.created": "RSS vir", + "rss.updated": "RSS vir posodobljene vsebine", + "search": "Iskanje", + "search.config.lang": "sl", + "search.placeholder": "Išči", + "search.share": "Deli", + "search.reset": "Počisti", + "search.result.initializer": "Inicializacija iskanja", + "search.result.placeholder": "Vpiši iskalni niz", + "search.result.none": "Ni zadetkov", + "search.result.one": "1 zadetek", + "search.result.other": "# zadetkov", + "search.result.more.one": "Še 1 na tej strani", + "search.result.more.other": "Še # na tej strani", + "search.result.term.missing": "Manjka", + "select.language": "Izberi jezik", + "select.version": "Izberi različico", + "source": "Pojdi na repozitorij", + "source.file.contributors": "Soavtorji", + "source.file.date.created": "Ustvarjeno", + "source.file.date.updated": "Zadnja posodobitev", + "tabs": "Zavihki", + "toc": "Kazalo", + "top": "Nazaj na vrh" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/sr.html b/src/templates/partials/languages/sr.html new file mode 100644 index 00000000..275ea126 --- /dev/null +++ b/src/templates/partials/languages/sr.html @@ -0,0 +1,57 @@ +<!-- + 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. +--> + +<!-- Translations: Serbian --> +{% macro t(key) %}{{ { + "language": "sr", + "action.edit": "Ажурирај страницу", + "action.skip": "Иди на текст", + "clipboard.copy": "Копирај у клипборд", + "clipboard.copied": "Ископирано у клипборд", + "footer": "Подножје", + "footer.next": "Следеће", + "footer.previous": "Претходно", + "header": "Заглавље", + "meta.comments": "Коментари", + "meta.source": "Извор", + "nav": "Навигација", + "search": "Претрага", + "search.placeholder": "Претрага", + "search.share": "Дељење", + "search.reset": "Очисти", + "search.result.initializer": "Иницијализујем претрагу", + "search.result.placeholder": "Унесите појам претраге", + "search.result.none": "Ништа није пронађено", + "search.result.one": "1 резултат претраге", + "search.result.other": "# резултата претраге", + "search.result.more.one": "још 1 на овој страни", + "search.result.more.other": "још # на овој страни", + "search.result.term.missing": "Недостаје", + "select.language": "Изабери језик", + "select.version": "Изабери верзију", + "source": "Иди у репозиторијум", + "source.file.date.created": "Креиран", + "source.file.date.updated": "Ажуриран", + "tabs": "Табови", + "toc": "Садржај", + "top": "Назад на врх" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/sv.html b/src/templates/partials/languages/sv.html new file mode 100644 index 00000000..52f151d2 --- /dev/null +++ b/src/templates/partials/languages/sv.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Swedish --> +{% macro t(key) %}{{ { + "language": "sv", + "action.edit": "Redigera sidan", + "action.skip": "Gå till innehållet", + "action.view": "Visa källkoden för denna sida", + "announce.dismiss": "Visa inte igen", + "blog.archive": "Arkivera", + "blog.categories": "Kategorier", + "blog.categories.in": "i", + "blog.continue": "Fortsätt läsa", + "blog.draft": "Utkast", + "blog.index": "Tillbaka till index", + "blog.meta": "Metadata", + "blog.references": "Relaterade länkar", + "clipboard.copy": "Kopiera till urklipp", + "clipboard.copied": "Kopierat till urklipp", + "consent.accept": "Acceptera", + "consent.manage": "Hantera inställningar", + "consent.reject": "Acceptera inte", + "footer": "Sidfot", + "footer.next": "Nästa", + "footer.previous": "Föregående", + "header": "Sidhuvud", + "meta.comments": "Kommentarer", + "meta.source": "Källa", + "nav": "Navigation", + "readtime.one": "1 min lästid", + "readtime.other": "# min lästid", + "rss.created": "RSS-flöde", + "rss.updated": "RSS-flöde av uppdaterat innehåll", + "search": "Sök", + "search.config.lang": "sv", + "search.placeholder": "Sök", + "search.share": "Dela", + "search.reset": "Rensa", + "search.result.initializer": "Initialiserar sök", + "search.result.placeholder": "Skriv sökord", + "search.result.none": "Inga sökresultat", + "search.result.one": "1 sökresultat", + "search.result.other": "# sökresultat", + "search.result.more.one": "1 till på denna sida", + "search.result.more.other": "# till på denna sida", + "search.result.term.missing": "Saknas", + "select.language": "Välj språk", + "select.version": "Välj version", + "source": "Gå till datakatalog", + "source.file.contributors": "Författare", + "source.file.date.created": "Skapad", + "source.file.date.updated": "Senast uppdaterad", + "tabs": "Flikar", + "toc": "Innehållsförteckning", + "top": "Tillbaka till toppen" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/te.html b/src/templates/partials/languages/te.html new file mode 100644 index 00000000..7529a47c --- /dev/null +++ b/src/templates/partials/languages/te.html @@ -0,0 +1,75 @@ +<!-- + 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. +--> + +<!-- Translations: Telugu --> +{% macro t(key) %}{{ { + "language": "te", + "action.edit": "ఈ పేజీలో దిద్దుబాట్లు చేయండి", + "action.skip": "సమాచారానికి వెళ్లండి", + "action.view": "నేను ఈ పేజీ యొక్క మూలాన్ని చూడాలనుకుంటున్నాను", + "announce.dismiss": "దీన్ని మళ్లీ చూపవద్దు", + "blog.archive": "పాత వ్యాసం", + "blog.categories": "వర్గాలు", + "blog.categories.in": "లో", + "blog.continue": "చదవడం కొనసాగించండి", + "blog.draft": "ప్రారంభ రచన", + "blog.index": "సూచికకు తిరిగి వెళ్ళు", + "blog.meta": "సమాచారం గురించి సమాచారం", + "blog.references": "సంబంధిత సూచనలు", + "clipboard.copy": "దీనిని అనుకరించు", + "clipboard.copied": "దీనిని అతికించు", + "consent.accept": "నేను దీనిని అంగీకరిస్తున్నాను", + "consent.manage": "ఆకృతీకరణను నిర్వహించండి", + "consent.reject": "నేను దీనిని తిరస్కరిస్తున్నాను", + "footer": "అడిటిప్పణి", + "footer.next": "తదుపరి భాగం", + "footer.previous": "మునుపటి భాగం", + "header": "శీర్షిక విభాగం", + "meta.comments": "అభిప్రాయాలు", + "meta.source": "మూలం", + "nav": "మార్గదర్శక పట్టీ", + "readtime.one": "చదవడానికి ఒక నిమిషం పడుతుంది", + "readtime.other": "చదవడానికి # నిమిషాలు పడుతుంది", + "rss.created": "ఆర్ఎస్ఎస్ సేవ", + "rss.updated": "ఆర్ఎస్ఎస్ సేవ నుండి తాజా నవీకరణ", + "search": "వెతకండి", + "search.placeholder": "వెతకండి", + "search.share": "పంచుకోండి", + "search.reset": "తుడిచివేయు", + "search.result.initializer": "శోధనను ప్రారంభిస్తోంది", + "search.result.placeholder": "రాయడం ద్వారా వెతకడం ప్రారంభించండి", + "search.result.none": "సరిపోలే పత్రాలు లేవు", + "search.result.one": "ఒక సరిపోలే పత్రం", + "search.result.other": "# సరిపోలే పత్రాలు", + "search.result.more.one": "ఈ పేజీలో మరొకటి", + "search.result.more.other": "ఈ పేజీలో ఇంకా # ఉన్నాయి", + "search.result.term.missing": "తప్పిపోయింది", + "select.language": "భాషను ఎంచుకోండి", + "select.version": "సంస్కరణను ఎంచుకోండి", + "source": "భండారానికి వెళ్ళండి", + "source.file.contributors": "సహకారులు", + "source.file.date.created": "సృష్టించబడింది", + "source.file.date.updated": "చివరి నవీకరణ", + "tabs": "వివిధ కిటికీలు", + "toc": "విషయ సూచిక", + "top": "పైకి తిరిగి వెళ్ళు" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/th.html b/src/templates/partials/languages/th.html new file mode 100644 index 00000000..c8104fc1 --- /dev/null +++ b/src/templates/partials/languages/th.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Thai --> +{% macro t(key) %}{{ { + "language": "th", + "action.edit": "แก้ไขหน้านี้", + "action.skip": "ข้ามไปที่เนื้อหา", + "action.view": "ดูแหล่งที่มาของหน้านี้", + "announce.dismiss": "อย่าแสดงสิ่งนี้อีก", + "blog.archive": "คลังเก็บเอกสาร", + "blog.categories": "หมวดหมู่", + "blog.categories.in": "ใย", + "blog.continue": "อ่านต่อไป", + "blog.draft": "ฉบับร่าง", + "blog.index": "กลับไปยังหน้าแรก", + "blog.meta": "คำอธิบายข้อมูล", + "blog.references": "ลิงก์ที่เกี่ยวข้อง", + "clipboard.copy": "คัดลอก", + "clipboard.copied": "คัดลอกแล้ว", + "consent.accept": "ยอมรับ", + "consent.manage": "จัดการการตั้งค่า", + "consent.reject": "ปฏิเสธ", + "footer": "ส่วนท้าย", + "footer.next": "ต่อไป", + "footer.previous": "ก่อนหน้า", + "header": "หัวข้อ", + "meta.comments": "ความคิดเห็น", + "meta.source": "แหล่งที่มา", + "nav": "ตัวนำทาง", + "readtime.one": "อ่าน 1 นาที", + "readtime.other": "อ่าน # นาที", + "rss.created": "ฟีด RSS", + "rss.updated": "ฟีด RSS ของเนื้อหาที่อัปเดต", + "search": "ค้นหา", + "search.config.lang": "th", + "search.placeholder": "ค้นหา", + "search.share": "แบ่งปัน", + "search.reset": "ล้าง", + "search.result.initializer": "กำลังเริ่มต้นการค้นหา", + "search.result.placeholder": "พิมพ์เพื่อเริ่มค้นหา", + "search.result.none": "ไม่พบเอกสารที่ตรงกัน", + "search.result.one": "พบเอกสารที่ตรงกัน", + "search.result.other": "พบ # เอกสารที่ตรงกัน", + "search.result.more.one": "อีกหนึ่งในหน้านี้", + "search.result.more.other": "# เพิ่มเติมในหน้านี้", + "search.result.term.missing": "ไม่พบ", + "select.language": "เลือกภาษา", + "select.version": "เลือกเวอร์ชัน", + "source": "ไปที่พื้นที่เก็บข้อมูล", + "source.file.contributors": "ผู้มีส่วนร่วม", + "source.file.date.created": "สร้าง", + "source.file.date.updated": "สร้าง", + "tabs": "แท็บ", + "toc": "สารบัญ", + "top": "กลับไปด้านบนสุด" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/tl.html b/src/templates/partials/languages/tl.html new file mode 100644 index 00000000..00c22c99 --- /dev/null +++ b/src/templates/partials/languages/tl.html @@ -0,0 +1,57 @@ +<!-- + 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. +--> + +<!-- Translations: Tagalog --> +{% macro t(key) %}{{ { + "language": "tl", + "action.edit": "I-edit ang pahinang ito", + "action.skip": "I-skip tungo sa nilalaman", + "clipboard.copy": "Kopyahin sa clipboard", + "clipboard.copied": "Nakopya mula sa clipboard", + "footer": "Lagdang Pangwakas", + "footer.next": "Susunod", + "footer.previous": "Nakaraan", + "header": "Pamuhatan", + "meta.comments": "Mga Komento", + "meta.source": "Pinagmulan", + "nav": "Nabigasyon", + "search": "Hanapin", + "search.placeholder": "Hanapin", + "search.share": "Ibahagi", + "search.reset": "Tanggalin", + "search.result.initializer": "Sinisimulan ang paghahanap", + "search.result.placeholder": "Mag-type upang simulan ang paghahanap", + "search.result.none": "Walang nahanap na dokumento", + "search.result.one": "1 magkatugmang dokumento", + "search.result.other": "# magkatugmang mga dokumento", + "search.result.more.one": "1 meron sa pahina na ito", + "search.result.more.other": "# meron sa pahina na ito", + "search.result.term.missing": "Nawawala", + "select.language": "Pumili ng lenguwahe", + "select.version": "Pumili ng bersyon", + "source": "Pumunta sa repository", + "source.file.date.created": "Nagawa", + "source.file.date.updated": "Huling update", + "tabs": "Mga tala", + "toc": "Talaan ng nilalaman", + "top": "Bumalik sa taas" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/tr.html b/src/templates/partials/languages/tr.html new file mode 100644 index 00000000..860f8ed7 --- /dev/null +++ b/src/templates/partials/languages/tr.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Turkish --> +{% macro t(key) %}{{ { + "language": "tr", + "action.edit": "Düzenle", + "action.skip": "Ana içeriğe geç", + "action.view": "Sayfanın kaynağını görüntüle", + "announce.dismiss": "Bir daha gösterme", + "blog.archive": "Arşiv", + "blog.categories": "Kategoriler", + "blog.categories.in": "in", + "blog.continue": "Okumaya devam et", + "blog.draft": "Taslak", + "blog.index": "Dizine geri dön", + "blog.meta": "Metadata", + "blog.references": "İlgili bağlantılar", + "clipboard.copy": "Kopyala", + "clipboard.copied": "Kopyalandı", + "consent.accept": "Kabul et", + "consent.manage": "Ayarları yönet", + "consent.reject": "Reddet", + "footer": "Altbilgi", + "footer.next": "Sonraki", + "footer.previous": "Önceki", + "header": "Başlık", + "meta.comments": "Yorumlar", + "meta.source": "Kaynak", + "nav": "Navigasyon", + "readtime.one": "1 dakika okuma", + "readtime.other": "# dakika okuma", + "rss.created": "RSS beslemesi", + "rss.updated": "Güncellenmiş içeriğin RSS beslemesi", + "search": "Ara", + "search.config.lang": "tr", + "search.placeholder": "Ara", + "search.share": "Paylaş", + "search.reset": "Temizle", + "search.result.initializer": "Arama başlatılıyor", + "search.result.placeholder": "Aramaya başlamak için yazın", + "search.result.none": "Eşleşen doküman bulunamadı", + "search.result.one": "1 doküman bulundu", + "search.result.other": "# doküman bulundu", + "search.result.more.one": "Bu sayfada 1 tane daha", + "search.result.more.other": "Bu sayfada # tane daha", + "search.result.term.missing": "Eksik", + "select.language": "Dil seç", + "select.version": "Versiyon seç", + "source": "Depoya git", + "source.file.contributors": "Katkıda bulunanlar", + "source.file.date.created": "Oluşturuldu", + "source.file.date.updated": "Son Güncelleme", + "tabs": "Sekmeler", + "toc": "İçindekiler", + "top": "Başa dön" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/uk.html b/src/templates/partials/languages/uk.html new file mode 100644 index 00000000..ca5c709c --- /dev/null +++ b/src/templates/partials/languages/uk.html @@ -0,0 +1,75 @@ +<!-- + 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. +--> + +<!-- Translations: Ukrainian --> +{% macro t(key) %}{{ { + "language": "uk", + "action.edit": "Редагувати сторінку", + "action.skip": "Перейти до змісту", + "action.view": "Переглянути вихідний код сторінки", + "announce.dismiss": "Більше не показувати", + "blog.archive": "Архівувати", + "blog.categories": "Категорії", + "blog.categories.in": "в", + "blog.continue": "Читати далі", + "blog.draft": "Чернетка", + "blog.index": "Повернутись на головну", + "blog.meta": "Метадані", + "blog.references": "Пов'язані посилання", + "clipboard.copy": "Скопіювати в буфер", + "clipboard.copied": "Скопійовано в буфер", + "consent.accept": "Прийняти", + "consent.manage": "Керувати налаштуваннями", + "consent.reject": "Відхилити", + "footer": "Футер", + "footer.next": "Вперед", + "footer.previous": "Назад", + "header": "Хедер", + "meta.comments": "Коментарі", + "meta.source": "Вихідний код", + "nav": "Навігація", + "readtime.one": "Час на прочитання: 1 хвилина", + "readtime.other": "Час на прочитання: # хвилин", + "rss.created": "RSS стрічка", + "rss.updated": "RSS стрічка оновленого контенту", + "search": "Шукати", + "search.placeholder": "Пошук", + "search.share": "Поділитись", + "search.reset": "Очистити", + "search.result.initializer": "Пошук розпочато", + "search.result.placeholder": "Розпочніть писати для пошуку", + "search.result.none": "Збігів не знайдено", + "search.result.one": "Знайдено 1 збіг", + "search.result.other": "Знайдено # збігів", + "search.result.more.one": "Ще 1 збіг на цій сторінці", + "search.result.more.other": "Ще # збігів на цій сторінці", + "search.result.term.missing": "Не знайдено запиту", + "select.language": "Обрати мову", + "select.version": "Обрати версію", + "source": "Перейти до вихідного коду", + "source.file.contributors": "Контриб'ютори", + "source.file.date.created": "Створено", + "source.file.date.updated": "Востаннє оновлено", + "tabs": "Вкладки", + "toc": "Зміст", + "top": "Повернутись нагору" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ur.html b/src/templates/partials/languages/ur.html new file mode 100644 index 00000000..14a50588 --- /dev/null +++ b/src/templates/partials/languages/ur.html @@ -0,0 +1,77 @@ +<!-- + 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. +--> + +<!-- Translations: Urdu --> +{% macro t(key) %}{{ { + "language": "ur", + "direction": "rtl", + "action.edit": "اس صفحے میں ترمیم کریں", + "action.skip": "براہِ راست مواد پر جائیں", + "action.view": "اس صفحہ کا ماخذ دیکھیں", + "announce.dismiss": "اسے دوبارہ مت دکھائیں", + "blog.archive": "محفوظ شدہ", + "blog.categories": "اقسام", + "blog.categories.in": "میں", + "blog.continue": "پڑھنا جاری رکھیے", + "blog.draft": "ڈرافٹ", + "blog.index": "واپس انڈیکس پر جائیں", + "blog.meta": "میٹا ڈیٹا", + "blog.references": "متعلقہ لنکس", + "clipboard.copy": "کلِپ بورڈ میں نقل کریں", + "clipboard.copied": "کلِپ بورڈ میں نقل کر دیا گیا", + "consent.accept": "قبول کریں", + "consent.manage": "سیٹینگ بدلیں", + "consent.reject": "رد کرنا", + "footer": "ذیلی تحریر", + "footer.next": "اگلا", + "footer.previous": "پچھلا", + "header": "سر تحریر", + "meta.comments": "تبصرے", + "meta.source": "ذریعہ", + "nav": "رہنمائی", + "readtime.one": "1 منٹ لگے گا", + "readtime.other": "# منٹ لگیں گے", + "rss.created": "RSS فیڈ", + "rss.updated": "تازہ ترین مواد کی RSS فیڈ", + "search": "تلاش", + "search.config.pipeline": " ", + "search.placeholder": "تلاش کریں", + "search.share": "اشتراک کریں", + "search.reset": "صاف کریں", + "search.result.initializer": "تلاش کا آغاز ہو رہا ہے", + "search.result.placeholder": "تلاش شروع کرنے کے لئے ٹائپ کریں", + "search.result.none": "کوئی ملتی جلتی دستاویزات نہیں", + "search.result.one": "۱ ملتی جلتی دستاویز", + "search.result.other": "# ملتی جلتی دستاویزات", + "search.result.more.one": "اِس صفحے پر مزید ۱", + "search.result.more.other": "اِس صفحے پر مزید #", + "search.result.term.missing": "گمشدہ", + "select.language": "زبان کا انتخاب کریں", + "select.version": "ورژن کا انتخاب کریں", + "source": "ریپازٹری پر جائیں", + "source.file.contributors": "تعاون کار", + "source.file.date.created": "تخلیق", + "source.file.date.updated": "آخری بار تجدید", + "tabs": "ٹیبز", + "toc": "فہرست", + "top": "واپس اوپر جائیں" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/uz.html b/src/templates/partials/languages/uz.html new file mode 100644 index 00000000..d86f4db2 --- /dev/null +++ b/src/templates/partials/languages/uz.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Uzbek --> +{% macro t(key) %}{{ { + "language": "uz", + "action.edit": "Ushbu sahifani tahrirlash", + "action.skip": "Tarkibga o'tish", + "action.view": "Ushbu sahifaning manbasini ko'rish", + "announce.dismiss": "Buni boshqa ko'rsatma", + "blog.archive": "Arxiv", + "blog.categories": "Kategoriyalar", + "blog.categories.in": "ichida", + "blog.continue": "O'qishni davom ettiring", + "blog.draft": "Qoralama", + "blog.index": "Indeks sahifasiga qaytish", + "blog.meta": "Metama'lumot", + "blog.references": "Bog'liq havolalar", + "clipboard.copy": "Buferga nusxalash", + "clipboard.copied": "Buferga nusxalandi", + "consent.accept": "Qabul qilish", + "consent.manage": "Sozlamalarni boshqarish", + "consent.reject": "Rad etish", + "footer": "Pastgi qism", + "footer.next": "Keyingi sahifa", + "footer.previous": "Oldingi sahifa", + "header": "Sarlavha", + "meta.comments": "Izohlar", + "meta.source": "Manba", + "nav": "Navigatsiya", + "readtime.one": "1 daqiqa o'qish", + "readtime.other": "# daqiqa o'qish", + "rss.created": "RSS tasmasi", + "rss.updated": "Yangilangan kontentning RSS tasmasi", + "search": "Qidirish", + "search.config.lang": "tr", + "search.placeholder": "Qidirish", + "search.share": "Ulashish", + "search.reset": "Tozalash", + "search.result.initializer": "Qidiruv ishga tushirilmoqda", + "search.result.placeholder": "Qidiruvni boshlash uchun kiriting", + "search.result.none": "Mos natijalar yo'q", + "search.result.one": "1 ta mos natija", + "search.result.other": "# ta mos keladigan natijalar", + "search.result.more.one": "Ushbu sahifada yana 1 ta natija", + "search.result.more.other": "Bu sahifada yana # ta natija", + "search.result.term.missing": "To'ldirilmagan", + "select.language": "Tilni tanlang", + "select.version": "Versiyani tanlang", + "source": "Repozitoriyga o'tish", + "source.file.contributors": "Hissa qo'shuvchilar", + "source.file.date.created": "Yaratildi", + "source.file.date.updated": "Oxirgi yangilanish", + "tabs": "Yorliqlar", + "toc": "Mundarija", + "top": "Yuqoriga qaytish" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/vi.html b/src/templates/partials/languages/vi.html new file mode 100644 index 00000000..b63a8d82 --- /dev/null +++ b/src/templates/partials/languages/vi.html @@ -0,0 +1,76 @@ +<!-- + 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. +--> + +<!-- Translations: Vietnamese --> +{% macro t(key) %}{{ { + "language": "vi", + "action.edit": "Chỉnh sửa", + "action.skip": "Bỏ qua", + "action.view": "Xem mã nguồn của trang", + "announce.dismiss": "Không hiển thị lại", + "blog.archive": "Lưu trữ", + "blog.categories": "Mục", + "blog.categories.in": "Trong", + "blog.continue": "Tiếp tục đọc", + "blog.draft": "Bản nháp", + "blog.index": "Quay lại", + "blog.meta": "Metadata", + "blog.references": "Các liên kết liên quan", + "clipboard.copy": "Sao chép vào bộ nhớ tạm", + "clipboard.copied": "Đã sao chép", + "consent.accept": "Đồng ý", + "consent.manage": "Cài đặt", + "consent.reject": "Từ chối", + "footer": "Chân trang", + "footer.next": "Sau", + "footer.previous": "Trước", + "header": "Đầu trang", + "meta.comments": "Bình luận", + "meta.source": "Mã nguồn", + "nav": "Thanh điều hướng", + "readtime.one": "1 phút đọc", + "readtime.other": "# phút đọc", + "rss.created": "RSS feed", + "rss.updated": "RSS feed of updated content", + "search": "Tìm kiếm", + "search.config.lang": "vi", + "search.placeholder": "Tìm kiếm", + "search.share": "Chia sẻ", + "search.reset": "Xoá", + "search.result.initializer": "Initializing search", + "search.result.placeholder": "Nhập để bắt đầu tìm kiếm", + "search.result.none": "Không tìm thấy tài liệu liên quan", + "search.result.one": "1 tài liệu liên quan", + "search.result.other": "# tài liệu liên quan", + "search.result.more.one": "1 từ khác trong trang", + "search.result.more.other": "# từ khác trong trang", + "search.result.term.missing": "Không", + "select.language": "Chọn ngôn ngữ", + "select.version": "Chọn phiên bản", + "source": "Xem mã nguồn", + "source.file.contributors": "Contributors", + "source.file.date.created": "Tạo", + "source.file.date.updated": "Cập nhật lần cuối", + "tabs": "Tabs", + "toc": "Mục lục", + "top": "Trở lại mục lục" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/zh-Hant.html b/src/templates/partials/languages/zh-Hant.html new file mode 100644 index 00000000..578fc82a --- /dev/null +++ b/src/templates/partials/languages/zh-Hant.html @@ -0,0 +1,77 @@ +<!-- + 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. +--> + +<!-- Translations: Chinese (Traditional) --> +{% macro t(key) %}{{ { + "language": "zh-Hant", + "action.edit": "編輯此頁", + "action.skip": "跳轉至", + "action.view": "查看源代碼", + "announce.dismiss": "不再顯示此訊息", + "blog.archive": "存檔", + "blog.categories": "分類", + "blog.categories.in": "分類在", + "blog.continue": "繼續閲讀", + "blog.draft": "草稿", + "blog.index": "回到首頁", + "blog.meta": "元數據", + "blog.references": "相關鏈接", + "clipboard.copy": "拷貝", + "clipboard.copied": "已拷貝", + "consent.accept": "接受", + "consent.manage": "管理設置", + "consent.reject": "拒絕", + "footer": "頁脚", + "footer.next": "下一頁", + "footer.previous": "上一頁", + "header": "頁首", + "meta.comments": "評論", + "meta.source": "來源", + "search.config.pipeline": "stemmer", + "search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+", + "nav": "導航", + "readtime.one": "需要 1 分鐘閲讀", + "readtime.other": "需要 # 分鐘閲讀", + "rss.created": "簡易資訊聚合", + "rss.updated": "更新之部分的簡易資訊聚合", + "search": "搜尋", + "search.placeholder": "搜尋", + "search.share": "分享", + "search.reset": "清空", + "search.result.initializer": "正在初始化搜尋引擎", + "search.result.placeholder": "鍵入以開始檢索", + "search.result.none": "沒有找到符合條件的結果", + "search.result.one": "找到 1 个符合條件的結果", + "search.result.other": "找到 # 個符合條件的結果", + "search.result.more.one": "此頁尚有 1 個符合的項目", + "search.result.more.other": "此頁尚有 # 個符合的項目", + "search.result.term.missing": "缺失", + "select.language": "選擇語言", + "select.version": "選擇版本", + "source": "前往倉庫", + "source.file.contributors": "貢獻者", + "source.file.date.created": "建立日期", + "source.file.date.updated": "最後更新", + "tabs": "標籤頁", + "toc": "目錄", + "top": "回到頂部" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/zh-TW.html b/src/templates/partials/languages/zh-TW.html new file mode 100644 index 00000000..405538f8 --- /dev/null +++ b/src/templates/partials/languages/zh-TW.html @@ -0,0 +1,77 @@ +<!-- + 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. +--> + +<!-- Translations: Chinese (Taiwanese) --> +{% macro t(key) %}{{ { + "language": "zh-TW", + "action.edit": "編輯此頁", + "action.skip": "跳轉到", + "action.view": "查看此頁原始碼", + "announce.dismiss": "不再顯示此訊息", + "blog.archive": "封存", + "blog.categories": "分類", + "blog.categories.in": "於", + "blog.continue": "繼續閱讀", + "blog.draft": "草稿", + "blog.index": "回到主頁", + "blog.meta": "元數據", + "blog.references": "相關連結", + "clipboard.copy": "複製", + "clipboard.copied": "已複製", + "consent.accept": "同意", + "consent.manage": "管理設定", + "consent.reject": "拒絕", + "footer": "頁腳", + "footer.next": "下一頁", + "footer.previous": "上一頁", + "header": "頁首", + "meta.comments": "留言", + "meta.source": "來源", + "nav": "導覽列", + "readtime.one": "需要 1 分鐘閱讀時間", + "readtime.other": "需要 # 分鐘閱讀時間", + "rss.created": "RSS 訂閱", + "rss.updated": "RSS 訂閱內容已更新", + "search": "搜尋", + "search.config.pipeline": "stemmer", + "search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+", + "search.placeholder": "搜尋", + "search.share": "分享", + "search.reset": "清除", + "search.result.initializer": "正在初始化搜尋引擎", + "search.result.placeholder": "打字進行搜尋", + "search.result.none": "沒有符合的項目", + "search.result.one": "找到 1 個符合的項目", + "search.result.other": "找到 # 個符合的項目", + "search.result.more.one": "此頁尚有 1 個符合的項目", + "search.result.more.other": "此頁尚有 # 個符合的項目", + "search.result.term.missing": "缺少字詞", + "select.language": "選擇語言", + "select.version": "選擇版本", + "source": "前往倉庫", + "source.file.contributors": "貢獻者", + "source.file.date.created": "建立日期", + "source.file.date.updated": "最後更新", + "tabs": "標籤", + "toc": "目錄", + "top": "回到頂端" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/zh.html b/src/templates/partials/languages/zh.html new file mode 100644 index 00000000..49f233a4 --- /dev/null +++ b/src/templates/partials/languages/zh.html @@ -0,0 +1,77 @@ +<!-- + 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. +--> + +<!-- Translations: Chinese (Simplified) --> +{% macro t(key) %}{{ { + "language": "zh", + "action.edit": "编辑此页", + "action.skip": "跳转至", + "action.view": "查看本页的源代码", + "announce.dismiss": "不再显示此消息", + "blog.archive": "归档", + "blog.categories": "分类", + "blog.categories.in": "分类于", + "blog.continue": "继续阅读", + "blog.draft": "草稿", + "blog.index": "回到主页", + "blog.meta": "元数据", + "blog.references": "相关链接", + "clipboard.copy": "复制", + "clipboard.copied": "已复制", + "consent.accept": "同意", + "consent.manage": "管理设定", + "consent.reject": "拒绝", + "footer": "页脚", + "footer.next": "下一页", + "footer.previous": "上一页", + "header": "页眉", + "meta.comments": "评论", + "meta.source": "来源", + "nav": "导航栏", + "readtime.one": "需要 1 分钟阅读时间", + "readtime.other": "需要 # 分钟阅读时间", + "rss.created": "RSS 订阅", + "rss.updated": "已更新内容的 RSS 订阅", + "search": "查找", + "search.config.pipeline": "stemmer", + "search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+", + "search.placeholder": "搜索", + "search.share": "分享", + "search.reset": "清空当前内容", + "search.result.initializer": "正在初始化搜索引擎", + "search.result.placeholder": "键入以开始搜索", + "search.result.none": "没有找到符合条件的结果", + "search.result.one": "找到 1 个符合条件的结果", + "search.result.other": "# 个符合条件的结果", + "search.result.more.one": "在该页上还有 1 个符合条件的结果", + "search.result.more.other": "在该页上还有 # 个符合条件的结果", + "search.result.term.missing": "缺少", + "select.language": "选择当前语言", + "select.version": "选择当前版本", + "source": "前往仓库", + "source.file.contributors": "贡献者", + "source.file.date.created": "创建日期", + "source.file.date.updated": "最后更新", + "tabs": "标签", + "toc": "目录", + "top": "回到页面顶部" +}[key] }}{% endmacro %} diff --git a/src/templates/partials/logo.html b/src/templates/partials/logo.html new file mode 100644 index 00000000..05832c71 --- /dev/null +++ b/src/templates/partials/logo.html @@ -0,0 +1,29 @@ +<!-- + 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. +--> + +<!-- Logo --> +{% if config.theme.logo %} + <img src="{{ config.theme.logo | url }}" alt="logo" /> +{% else %} + {% set icon = config.theme.icon.logo or "material/library" %} + {% include ".icons/" ~ icon ~ ".svg" %} +{% endif %} diff --git a/src/templates/partials/nav-item.html b/src/templates/partials/nav-item.html new file mode 100644 index 00000000..24d74a1a --- /dev/null +++ b/src/templates/partials/nav-item.html @@ -0,0 +1,249 @@ +<!-- + 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. +--> + +<!-- Render navigation link status --> +{% macro render_status(nav_item, type) %} + {% set class = "md-status md-status--" ~ type %} + + <!-- Render icon with title (or tooltip), if given --> + {% if config.extra.status and config.extra.status[type] %} + <span + class="{{ class }}" + title="{{ config.extra.status[type] }}" + > + </span> + + <!-- Render icon only --> + {% else %} + <span class="{{ class }}"></span> + {% endif %} +{% endmacro %} + +<!-- Render navigation link content --> +{% macro render_content(nav_item, ref = nav_item) %} + + <!-- Navigation link icon --> + {% if nav_item.is_page and nav_item.meta.icon %} + {% include ".icons/" ~ nav_item.meta.icon ~ ".svg" %} + {% endif %} + + <!-- Navigation link title --> + <span class="md-ellipsis"> + {{ ref.title }} + </span> + + <!-- Navigation link status --> + {% if nav_item.is_page and nav_item.meta.status %} + {{ render_status(nav_item, nav_item.meta.status) }} + {% endif %} +{% endmacro %} + +<!-- Render navigation item (pruned) --> +{% macro render_pruned(nav_item, ref = nav_item) %} + {% set first = nav_item.children | first %} + + <!-- Recurse, if the first item has further nested items --> + {% if first and first.children %} + {{ render_pruned(first, ref) }} + + <!-- Navigation link --> + {% else %} + <a href="{{ first.url | url }}" class="md-nav__link"> + {{ render_content(ref) }} + + <!-- Only render toggle if there's at least one nested item --> + {% if nav_item.children | length > 0 %} + <span class="md-nav__icon md-icon"></span> + {% endif %} + </a> + {% endif %} +{% endmacro %} + +<!-- Render navigation item --> +{% macro render(nav_item, path, level) %} + + <!-- Determine classes --> + {% set class = "md-nav__item" %} + {% if nav_item.active %} + {% set class = class ~ " md-nav__item--active" %} + {% endif %} + + <!-- Navigation item with nested items --> + {% if nav_item.children %} + + <!-- Determine all nested items that are index pages --> + {% set indexes = [] %} + {% if "navigation.indexes" in features %} + {% for nav_item in nav_item.children %} + {% if nav_item.is_index and not index is defined %} + {% set _ = indexes.append(nav_item) %} + {% endif %} + {% endfor %} + {% endif %} + + <!-- Determine whether to render item as a section --> + {% set tabs = "navigation.tabs" in features %} + {% set sections = "navigation.sections" in features %} + {% if tabs and level == 1 or sections and tabs >= level - 1 %} + {% set class = class ~ " md-nav__item--section" %} + {% set is_section = true %} + + <!-- Determine whether to prune inactive item --> + {% elif not nav_item.active and "navigation.prune" in features %} + {% set class = class ~ " md-nav__item--pruned" %} + {% set is_pruned = true %} + {% endif %} + + <!-- Nested navigation item --> + <li class="{{ class }} md-nav__item--nested"> + {% if not is_pruned %} + {% set checked = "checked" if nav_item.active %} + + <!-- Determine checked and indeterminate state --> + {% set is_expanded = "navigation.expand" in features %} + {% if is_expanded and not checked %} + {% set indeterminate = "md-toggle--indeterminate" %} + {% endif %} + + <!-- Active checkbox expands items contained within nested section --> + <input + class="md-nav__toggle md-toggle {{ indeterminate }}" + type="checkbox" + id="{{ path }}" + {{ checked }} + /> + + <!-- Toggle to expand nested items --> + {% if not indexes %} + {% set tabindex = "0" if not is_section %} + <label + class="md-nav__link" + for="{{ path }}" + id="{{ path }}_label" + tabindex="{{ tabindex }}" + > + {{ render_content(nav_item) }} + <span class="md-nav__icon md-icon"></span> + </label> + + <!-- Toggle to expand nested items with link to index page --> + {% else %} + {% set index = indexes | first %} + {% set class = "md-nav__link--active" if index == page %} + <div class="md-nav__link md-nav__container"> + <a + href="{{ index.url | url }}" + class="md-nav__link {{ class }}" + > + {{ render_content(index, nav_item) }} + </a> + + <!-- Only render toggle if there's at least one more page --> + {% if nav_item.children | length > 1 %} + {% set tabindex = "0" if not is_section %} + <label + class="md-nav__link {{ class }}" + for="{{ path }}" + id="{{ path }}_label" + tabindex="{{ tabindex }}" + > + <span class="md-nav__icon md-icon"></span> + </label> + {% endif %} + </div> + {% endif %} + + <!-- Nested navigation --> + <nav + class="md-nav" + data-md-level="{{ level }}" + aria-labelledby="{{ path }}_label" + aria-expanded="{{ nav_item.active | tojson }}" + > + <label class="md-nav__title" for="{{ path }}"> + <span class="md-nav__icon md-icon"></span> + {{ nav_item.title }} + </label> + <ul class="md-nav__list" data-md-scrollfix> + + <!-- Nested navigation item --> + {% for nav_item in nav_item.children %} + {% if not indexes or nav_item != indexes | first %} + {{ render(nav_item, path ~ "_" ~ loop.index, level + 1) }} + {% endif %} + {% endfor %} + </ul> + </nav> + + <!-- Pruned navigation item --> + {% else %} + {{ render_pruned(nav_item) }} + {% endif %} + </li> + + <!-- Currently active page --> + {% elif nav_item == page %} + <li class="{{ class }}"> + {% set toc = page.toc %} + + <!-- State toggle --> + <input + class="md-nav__toggle md-toggle" + type="checkbox" + id="__toc" + /> + + <!-- Hack: see partials/toc.html for more information --> + {% set first = toc | first %} + {% if first and first.level == 1 %} + {% set toc = first.children %} + {% endif %} + + <!-- Navigation link to table of contents --> + {% if toc %} + <label class="md-nav__link md-nav__link--active" for="__toc"> + {{ render_content(nav_item) }} + <span class="md-nav__icon md-icon"></span> + </label> + {% endif %} + <a + href="{{ nav_item.url | url }}" + class="md-nav__link md-nav__link--active" + > + {{ render_content(nav_item) }} + </a> + + <!-- Table of contents --> + {% if toc %} + {% include "partials/toc.html" %} + {% endif %} + </li> + + <!-- Navigation item --> + {% else %} + <li class="{{ class }}"> + <a href="{{ nav_item.url | url }}" class="md-nav__link"> + {{ render_content(nav_item) }} + </a> + </li> + {% endif %} +{% endmacro %} diff --git a/src/templates/partials/nav.html b/src/templates/partials/nav.html new file mode 100644 index 00000000..c41fe694 --- /dev/null +++ b/src/templates/partials/nav.html @@ -0,0 +1,69 @@ +<!-- + 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 "partials/nav-item.html" as item with context %} + +<!-- Determine classes --> +{% set class = "md-nav md-nav--primary" %} +{% if "navigation.tabs" in features %} + {% set class = class ~ " md-nav--lifted" %} +{% endif %} +{% if "toc.integrate" in features %} + {% set class = class ~ " md-nav--integrated" %} +{% endif %} + +<!-- Navigation --> +<nav + class="{{ class }}" + aria-label="{{ lang.t('nav') }}" + data-md-level="0" +> + + <!-- Site title --> + <label class="md-nav__title" for="__drawer"> + <a + href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}" + title="{{ config.site_name | e }}" + class="md-nav__button md-logo" + aria-label="{{ config.site_name }}" + data-md-component="logo" + > + {% include "partials/logo.html" %} + </a> + {{ config.site_name }} + </label> + + <!-- Repository information --> + {% if config.repo_url %} + <div class="md-nav__source"> + {% include "partials/source.html" %} + </div> + {% endif %} + + <!-- Navigation list --> + <ul class="md-nav__list" data-md-scrollfix> + {% for nav_item in nav %} + {% set path = "__nav_" ~ loop.index %} + {{ item.render(nav_item, path, 1) }} + {% endfor %} + </ul> +</nav> diff --git a/src/templates/partials/pagination.html b/src/templates/partials/pagination.html new file mode 100644 index 00000000..046ecbe9 --- /dev/null +++ b/src/templates/partials/pagination.html @@ -0,0 +1,42 @@ +<!-- + 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. +--> + +<!-- Pagination icons --> +{% import ".icons/material/chevron-double-left.svg" as icon_first %} +{% import ".icons/material/chevron-left.svg" as icon_previous %} +{% import ".icons/material/chevron-right.svg" as icon_next %} +{% import ".icons/material/chevron-double-right.svg" as icon_last %} + +<!-- Pagination --> +<nav class="md-pagination"> + {{ + pagination({ + "link_attr": { "class": "md-pagination__link" }, + "curpage_attr": { "class": "md-pagination__current" }, + "dotdot_attr": { "class": "md-pagination__dots" }, + "symbol_first": icon_first, + "symbol_previous": icon_previous, + "symbol_next": icon_next, + "symbol_last": icon_last + }) + }} +</nav> diff --git a/src/templates/partials/palette.html b/src/templates/partials/palette.html new file mode 100644 index 00000000..ccb8db0a --- /dev/null +++ b/src/templates/partials/palette.html @@ -0,0 +1,55 @@ +<!-- + 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. +--> + +<!-- Color palette toggle --> +<form class="md-header__option" data-md-component="palette"> + {% for option in config.theme.palette %} + {% set scheme = option.scheme | d("default", true) %} + {% set primary = option.primary | d("indigo", true) %} + {% set accent = option.accent | d("indigo", true) %} + <input + class="md-option" + data-md-color-media="{{ option.media }}" + data-md-color-scheme="{{ scheme | replace(' ', '-') }}" + data-md-color-primary="{{ primary | replace(' ', '-') }}" + data-md-color-accent="{{ accent | replace(' ', '-') }}" + {% if option.toggle %} + aria-label="{{ option.toggle.name }}" + {% else %} + aria-hidden="true" + {% endif %} + type="radio" + name="__palette" + id="__palette_{{ loop.index }}" + /> + {% if option.toggle %} + <label + class="md-header__button md-icon" + title="{{ option.toggle.name }}" + for="__palette_{{ loop.index0 or loop.length }}" + hidden + > + {% include ".icons/" ~ option.toggle.icon ~ ".svg" %} + </label> + {% endif %} + {% endfor %} +</form> diff --git a/src/templates/partials/post.html b/src/templates/partials/post.html new file mode 100644 index 00000000..c7233051 --- /dev/null +++ b/src/templates/partials/post.html @@ -0,0 +1,99 @@ +<!-- + 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. +--> + +<!-- Post excerpt --> +<article class="md-post md-post--excerpt"> + <header class="md-post__header"> + + <!-- Post authors --> + {% if post.authors %} + <nav class="md-post__authors md-typeset"> + {% for author in post.authors %} + <span class="md-author"> + <img src="{{ author.avatar }}" alt="{{ author.name }}" /> + </span> + {% endfor %} + </nav> + {% endif %} + + <!-- Post metadata --> + <div class="md-post__meta md-meta"> + <ul class="md-meta__list"> + + <!-- Post date --> + <li class="md-meta__item"> + <time datetime="{{ post.config.date.created }}"> + {{- post.config.date.created | date -}} + </time> + {#- Collapse whitespace -#} + </li> + + <!-- Post categories --> + {% if post.categories %} + <li class="md-meta__item"> + {{ lang.t("blog.categories.in") }} + {% for category in post.categories %} + <a + href="{{ category.url | url }}" + class="md-meta__link" + > + {{- category.title -}} + </a> + {%- if loop.revindex > 1 %}, {% endif -%} + {% endfor -%} + </li> + {% endif %} + + <!-- Post readtime --> + {% if post.config.readtime %} + {% set time = post.config.readtime %} + <li class="md-meta__item"> + {% if time == 1 %} + {{ lang.t("readtime.one") }} + {% else %} + {{ lang.t("readtime.other") | replace("#", time) }} + {% endif %} + </li> + {% endif %} + </ul> + + <!-- Draft marker --> + {% if post.config.draft %} + <span class="md-draft"> + {{ lang.t("blog.draft") }} + </span> + {% endif %} + </div> + </header> + + <!-- Post content --> + <div class="md-post__content md-typeset"> + {{ post.content }} + + <!-- Continue reading link --> + <nav class="md-post__action"> + <a href="{{ post.url | url }}"> + {{ lang.t("blog.continue") }} + </a> + </nav> + </div> +</article> diff --git a/src/templates/partials/progress.html b/src/templates/partials/progress.html new file mode 100644 index 00000000..f5d13d10 --- /dev/null +++ b/src/templates/partials/progress.html @@ -0,0 +1,24 @@ +<!-- + 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. +--> + +<!-- Progress indicator --> +<div class="md-progress" data-md-component="progress" role="progressbar"></div> diff --git a/src/templates/partials/search.html b/src/templates/partials/search.html new file mode 100644 index 00000000..1854a7d3 --- /dev/null +++ b/src/templates/partials/search.html @@ -0,0 +1,109 @@ +<!-- + 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. +--> + +<!-- Search interface --> +<div class="md-search" data-md-component="search" role="dialog"> + <label class="md-search__overlay" for="__search"></label> + <div class="md-search__inner" role="search"> + <form class="md-search__form" name="search"> + + <!-- Search input --> + <input + type="text" + class="md-search__input" + name="query" + aria-label="{{ lang.t('search.placeholder') }}" + placeholder="{{ lang.t('search.placeholder') }}" + autocapitalize="off" + autocorrect="off" + autocomplete="off" + spellcheck="false" + data-md-component="search-query" + required + /> + + <!-- Button to open search --> + <label class="md-search__icon md-icon" for="__search"> + {% set icon = config.theme.icon.search or "material/magnify" %} + {% include ".icons/" ~ icon ~ ".svg" %} + {% set icon = config.theme.icon.previous or "material/arrow-left" %} + {% include ".icons/" ~ icon ~ ".svg" %} + </label> + + <!-- Search options --> + <nav + class="md-search__options" + aria-label="{{ lang.t('search') }}" + > + + <!-- Button to share search --> + {% if "search.share" in features %} + <a + href="javascript:void(0)" + class="md-search__icon md-icon" + title="{{ lang.t('search.share') }}" + aria-label="{{ lang.t('search.share') }}" + data-clipboard + data-clipboard-text="" + data-md-component="search-share" + tabindex="-1" + > + {% set icon = config.theme.icon.share or "material/share-variant" %} + {% include ".icons/" ~ icon ~ ".svg" %} + </a> + {% endif %} + + <!-- Button to reset search --> + <button + type="reset" + class="md-search__icon md-icon" + title="{{ lang.t('search.reset') }}" + aria-label="{{ lang.t('search.reset') }}" + tabindex="-1" + > + {% set icon = config.theme.icon.close or "material/close" %} + {% include ".icons/" ~ icon ~ ".svg" %} + </button> + </nav> + + <!-- Search suggestions --> + {% if "search.suggest" in features %} + <div + class="md-search__suggest" + data-md-component="search-suggest" + ></div> + {% endif %} + </form> + <div class="md-search__output"> + <div class="md-search__scrollwrap" data-md-scrollfix> + + <!-- Search results --> + <div class="md-search-result" data-md-component="search-result"> + <div class="md-search-result__meta"> + {{ lang.t("search.result.initializer") }} + </div> + <ol class="md-search-result__list" role="presentation"></ol> + </div> + </div> + </div> + </div> +</div> diff --git a/src/templates/partials/social.html b/src/templates/partials/social.html new file mode 100644 index 00000000..5d2c4017 --- /dev/null +++ b/src/templates/partials/social.html @@ -0,0 +1,48 @@ +<!-- + 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. +--> + +<!-- Social links --> +<div class="md-social"> + {% for social in config.extra.social %} + + <!-- Automatically set rel=me for Mastodon --> + {% set rel = "noopener" %} + {% if "mastodon" in social.icon %} + {% set rel = rel ~ " me" %} + {% endif %} + + <!-- Compute title and render link --> + {% set title = social.name %} + {% if not title and "//" in social.link %} + {% set _, url = social.link.split("//") %} + {% set title = url.split("/")[0] %} + {% endif %} + <a + href="{{ social.link }}" + target="_blank" rel="{{ rel }}" + title="{{ title | e }}" + class="md-social__link" + > + {% include ".icons/" ~ social.icon ~ ".svg" %} + </a> + {% endfor %} +</div> diff --git a/src/templates/partials/source-file.html b/src/templates/partials/source-file.html new file mode 100644 index 00000000..928e35de --- /dev/null +++ b/src/templates/partials/source-file.html @@ -0,0 +1,44 @@ +<!-- + 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. +--> + +<!-- Source file information --> +<hr /> +<div class="md-source-file"> + <small> + + <!-- mkdocs-git-revision-date-localized-plugin --> + {% if page.meta.git_revision_date_localized %} + {{ lang.t("source.file.date.updated") }}: + {{ page.meta.git_revision_date_localized }} + {% if page.meta.git_creation_date_localized %} + <br /> + {{ lang.t("source.file.date.created") }}: + {{ page.meta.git_creation_date_localized }} + {% endif %} + + <!-- mkdocs-git-revision-date-plugin --> + {% elif page.meta.revision_date %} + {{ lang.t("source.file.date.updated") }}: + {{ page.meta.revision_date }} + {% endif %} + </small> +</div> diff --git a/src/templates/partials/source.html b/src/templates/partials/source.html new file mode 100644 index 00000000..f4aac3e6 --- /dev/null +++ b/src/templates/partials/source.html @@ -0,0 +1,37 @@ +<!-- + 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. +--> + +<!-- Repository information --> +<a + href="{{ config.repo_url }}" + title="{{ lang.t('source') }}" + class="md-source" + data-md-component="source" +> + <div class="md-source__icon md-icon"> + {% set icon = config.theme.icon.repo or "fontawesome/brands/git-alt" %} + {% include ".icons/" ~ icon ~ ".svg" %} + </div> + <div class="md-source__repository"> + {{ config.repo_name }} + </div> +</a> diff --git a/src/templates/partials/tabs-item.html b/src/templates/partials/tabs-item.html new file mode 100644 index 00000000..7a12a742 --- /dev/null +++ b/src/templates/partials/tabs-item.html @@ -0,0 +1,71 @@ +<!-- + 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. +--> + +<!-- Render navigation link content --> +{% macro render_content(nav_item, ref = nav_item) %} + + <!-- Navigation link icon --> + {% if nav_item == ref or "navigation.indexes" in features %} + {% if nav_item.is_index and nav_item.meta.icon %} + {% include ".icons/" ~ nav_item.meta.icon ~ ".svg" %} + {% endif %} + {% endif %} + + <!-- Navigation link title --> + {{ ref.title }} +{% endmacro %} + +<!-- Render navigation item --> +{% macro render(nav_item, ref = nav_item) %} + + <!-- Determine classes --> + {% set class = "md-tabs__item" %} + {% if ref.active %} + {% set class = class ~ " md-tabs__item--active" %} + {% endif %} + + <!-- Navigation item with nested items --> + {% if nav_item.children %} + {% set first = nav_item.children | first %} + + <!-- Recurse, if the first item has further nested items --> + {% if first.children %} + {{ render(first, ref) }} + + <!-- Nested navigation item --> + {% else %} + <li class="{{ class }}"> + <a href="{{ first.url | url }}" class="md-tabs__link"> + {{ render_content(first, ref) }} + </a> + </li> + {% endif %} + + <!-- Navigation item --> + {% else %} + <li class="{{ class }}"> + <a href="{{ nav_item.url | url }}" class="md-tabs__link"> + {{ render_content(nav_item) }} + </a> + </li> + {% endif %} +{% endmacro %} diff --git a/src/templates/partials/tabs.html b/src/templates/partials/tabs.html new file mode 100644 index 00000000..0ea590cf --- /dev/null +++ b/src/templates/partials/tabs.html @@ -0,0 +1,38 @@ +<!-- + 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 "partials/tabs-item.html" as item with context %} + +<!-- Navigation tabs --> +<nav + class="md-tabs" + aria-label="{{ lang.t('tabs') }}" + data-md-component="tabs" +> + <div class="md-grid"> + <ul class="md-tabs__list"> + {% for nav_item in nav %} + {{ item.render(nav_item) }} + {% endfor %} + </ul> + </div> +</nav> diff --git a/src/templates/partials/tags.html b/src/templates/partials/tags.html new file mode 100644 index 00000000..b3dea295 --- /dev/null +++ b/src/templates/partials/tags.html @@ -0,0 +1,52 @@ +<!-- + 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. +--> + +<!-- Determine whether to show tags --> +{% if page.meta and page.meta.hide %} + {% set hidden = "hidden" if "tags" in page.meta.hide %} +{% endif %} + +<!-- Tags --> +<nav class="md-tags" {{ hidden }}> + {% for tag in tags %} + {% set icon = "" %} + {% if config.extra.tags %} + {% set icon = " md-tag-icon" %} + {% if tag.type %} + {% set icon = icon ~ " md-tag--" ~ tag.type %} + {% endif %} + {% endif %} + + <!-- Render tag with link --> + {% if tag.url %} + <a href="{{ tag.url | url }}" class="md-tag{{ icon }}"> + {{- tag.name -}} + </a> + + <!-- Render tag without link --> + {% else %} + <span class="md-tag{{ icon }}"> + {{- tag.name -}} + </span> + {% endif %} + {% endfor %} +</nav> diff --git a/src/templates/partials/toc-item.html b/src/templates/partials/toc-item.html new file mode 100644 index 00000000..1af82c56 --- /dev/null +++ b/src/templates/partials/toc-item.html @@ -0,0 +1,39 @@ +<!-- + 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. +--> + +<!-- Table of contents item --> +<li class="md-nav__item"> + <a href="{{ toc_item.url }}" class="md-nav__link"> + {{ toc_item.title }} + </a> + + <!-- Table of contents list --> + {% if toc_item.children %} + <nav class="md-nav" aria-label="{{ toc_item.title | striptags }}"> + <ul class="md-nav__list"> + {% for toc_item in toc_item.children %} + {% include "partials/toc-item.html" %} + {% endfor %} + </ul> + </nav> + {% endif %} +</li> diff --git a/src/templates/partials/toc.html b/src/templates/partials/toc.html new file mode 100644 index 00000000..cb50b257 --- /dev/null +++ b/src/templates/partials/toc.html @@ -0,0 +1,56 @@ +<!-- + 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. +--> + +<!-- Determine title --> +{% set title = lang.t("toc") %} +{% if config.mdx_configs.toc and config.mdx_configs.toc.title %} + {% set title = config.mdx_configs.toc.title %} +{% endif %} + +<!-- Table of contents --> +<nav class="md-nav md-nav--secondary" aria-label="{{ title }}"> + {% set toc = page.toc %} + + <!-- + Check whether the content starts with a level 1 headline. If it does, the + top-level anchor must be skipped, since it would be redundant to the link + to the current page that is located just above the anchor. Therefore we + directly continue with the children of the anchor. + --> + {% set first = toc | first %} + {% if first and first.level == 1 %} + {% set toc = first.children %} + {% endif %} + + <!-- Table of contents title and list --> + {% if toc %} + <label class="md-nav__title" for="__toc"> + <span class="md-nav__icon md-icon"></span> + {{ title }} + </label> + <ul class="md-nav__list" data-md-component="toc" data-md-scrollfix> + {% for toc_item in toc %} + {% include "partials/toc-item.html" %} + {% endfor %} + </ul> + {% endif %} +</nav> diff --git a/src/templates/partials/top.html b/src/templates/partials/top.html new file mode 100644 index 00000000..737e6248 --- /dev/null +++ b/src/templates/partials/top.html @@ -0,0 +1,28 @@ +<!-- + 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. +--> + +<!-- Progress indicator --> +<button type="button" class="md-top md-icon" data-md-component="top" hidden> + {% set icon = config.theme.icon.top or "material/arrow-up" %} + {% include ".icons/" ~ icon ~ ".svg" %} + {{ lang.t("top") }} +</button> diff --git a/src/templates/redirect.html b/src/templates/redirect.html new file mode 100644 index 00000000..80869c4f --- /dev/null +++ b/src/templates/redirect.html @@ -0,0 +1,41 @@ +<!-- + 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. +--> + +<!doctype html> +<html> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width,initial-scale=1" /> + <title>{{ config.site_name }}</title> + <noscript> + <meta http-equiv="refresh" content="0;url={{ page.meta.location }}" /> + </noscript> + <script> + window.location.replace([ + "{{ page.meta.location }}", + window.location.search, + window.location.hash + ].join("")) + </script> + </head> + <body></body> +</html> |
