diff options
| author | 2023-12-15 09:11:47 +0800 | |
|---|---|---|
| committer | 2023-12-15 09:11:47 +0800 | |
| commit | be8de118db913711eb72ae5187d26e54a0055727 (patch) | |
| tree | 96cd6c012dafa3f4015e54edef90df5eaaab0ddb /docs/src/overrides | |
| parent | 9b2d27ba1d91a0d5531bc9c0d52c3887a2dfb2aa (diff) | |
| download | infini-be8de118db913711eb72ae5187d26e54a0055727.tar.gz infini-be8de118db913711eb72ae5187d26e54a0055727.zip | |
refactor(docs): optmst `docs` dir & `deps`
Diffstat (limited to 'docs/src/overrides')
24 files changed, 2522 insertions, 0 deletions
diff --git a/docs/src/overrides/assets/javascripts/components/_/index.ts b/docs/src/overrides/assets/javascripts/components/_/index.ts new file mode 100644 index 00000000..3cb4c18e --- /dev/null +++ b/docs/src/overrides/assets/javascripts/components/_/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 { getElement, getElements } from "~/browser" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Component type + */ +export type ComponentType = + | "iconsearch" /* Icon search */ + | "iconsearch-query" /* Icon search input */ + | "iconsearch-result" /* Icon search results */ + | "sponsorship" /* Sponsorship */ + | "sponsorship-count" /* Sponsorship count */ + | "sponsorship-total" /* Sponsorship total */ + +/** + * 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 { + "iconsearch": HTMLElement /* Icon search */ + "iconsearch-query": HTMLInputElement /* Icon search input */ + "iconsearch-result": HTMLElement /* Icon search results */ + "sponsorship": HTMLElement /* Sponsorship */ + "sponsorship-count": HTMLElement /* Sponsorship count */ + "sponsorship-total": HTMLElement /* Sponsorship total */ +} + +/* ---------------------------------------------------------------------------- + * 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-mdx-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-mdx-component=${type}]`, node) +} diff --git a/docs/src/overrides/assets/javascripts/components/iconsearch/_/index.ts b/docs/src/overrides/assets/javascripts/components/iconsearch/_/index.ts new file mode 100644 index 00000000..f509a6f9 --- /dev/null +++ b/docs/src/overrides/assets/javascripts/components/iconsearch/_/index.ts @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { configuration } from "~/_" +import { requestJSON } from "~/browser" + +import { Component, getComponentElement } from "../../_" +import { + IconSearchQuery, + mountIconSearchQuery +} from "../query" +import { + IconSearchResult, + mountIconSearchResult +} from "../result" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Icon category + */ +export interface IconCategory { + base: string /* Category base URL */ + data: Record<string, string> /* Category data */ +} + +/** + * Icon search index + */ +export interface IconSearchIndex { + icons: IconCategory /* Icons */ + emojis: IconCategory /* Emojis */ +} + +/* ------------------------------------------------------------------------- */ + +/** + * Icon search + */ +export type IconSearch = + | IconSearchQuery + | IconSearchResult + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Mount icon search + * + * @param el - Icon search element + * + * @returns Icon search component observable + */ +export function mountIconSearch( + el: HTMLElement +): Observable<Component<IconSearch>> { + const config = configuration() + const index$ = requestJSON<IconSearchIndex>( + new URL("assets/javascripts/iconsearch_index.json", config.base) + ) + + /* Retrieve query and result components */ + const query = getComponentElement("iconsearch-query", el) + const result = getComponentElement("iconsearch-result", el) + + /* Create and return component */ + const query$ = mountIconSearchQuery(query) + const result$ = mountIconSearchResult(result, { index$, query$ }) + return merge(query$, result$) +} diff --git a/docs/src/overrides/assets/javascripts/components/iconsearch/index.ts b/docs/src/overrides/assets/javascripts/components/iconsearch/index.ts new file mode 100644 index 00000000..9d856774 --- /dev/null +++ b/docs/src/overrides/assets/javascripts/components/iconsearch/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 "./query" +export * from "./result" diff --git a/docs/src/overrides/assets/javascripts/components/iconsearch/query/index.ts b/docs/src/overrides/assets/javascripts/components/iconsearch/query/index.ts new file mode 100644 index 00000000..03a3daad --- /dev/null +++ b/docs/src/overrides/assets/javascripts/components/iconsearch/query/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. + */ + +import { + Observable, + combineLatest, + delay, + distinctUntilChanged, + filter, + fromEvent, + map, + merge, + startWith, + withLatestFrom +} from "rxjs" + +import { watchElementFocus } from "~/browser" + +import { Component } from "../../_" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Icon search query + */ +export interface IconSearchQuery { + value: string /* Query value */ + focus: boolean /* Query focus */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Mount icon search query + * + * @param el - Icon search query element + * + * @returns Icon search query component observable + */ +export function mountIconSearchQuery( + el: HTMLInputElement +): Observable<Component<IconSearchQuery, HTMLInputElement>> { + + /* Intercept focus and input events */ + const focus$ = watchElementFocus(el) + const value$ = merge( + fromEvent(el, "keyup"), + fromEvent(el, "focus").pipe(delay(1)) + ) + .pipe( + map(() => el.value), + startWith(el.value), + distinctUntilChanged(), + ) + + /* Log search on blur */ + focus$ + .pipe( + filter(active => !active), + withLatestFrom(value$) + ) + .subscribe(([, value]) => { + const path = document.location.pathname + if (typeof ga === "function" && value.length) + ga("send", "pageview", `${path}?q=[icon]+${value}`) + }) + + /* Combine into single observable */ + return combineLatest([value$, focus$]) + .pipe( + map(([value, focus]) => ({ ref: el, value, focus })), + ) +} diff --git a/docs/src/overrides/assets/javascripts/components/iconsearch/result/index.ts b/docs/src/overrides/assets/javascripts/components/iconsearch/result/index.ts new file mode 100644 index 00000000..2b9d97fb --- /dev/null +++ b/docs/src/overrides/assets/javascripts/components/iconsearch/result/index.ts @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { filter as search } from "fuzzaldrin-plus" +import { + Observable, + Subject, + bufferCount, + combineLatest, + distinctUntilKeyChanged, + filter, + finalize, + map, + merge, + of, + switchMap, + tap, + withLatestFrom, + zipWith +} from "rxjs" + +import { + getElement, + watchElementBoundary +} from "~/browser" +import { round } from "~/utilities" + +import { Icon, renderIconSearchResult } from "_/templates" + +import { Component } from "../../_" +import { IconSearchIndex } from "../_" +import { IconSearchQuery } from "../query" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Icon search result + */ +export interface IconSearchResult { + data: Icon[] /* Search result data */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Watch options + */ +interface WatchOptions { + index$: Observable<IconSearchIndex> /* Search index observable */ + query$: Observable<IconSearchQuery> /* Search query observable */ +} + +/** + * Mount options + */ +interface MountOptions { + index$: Observable<IconSearchIndex> /* Search index observable */ + query$: Observable<IconSearchQuery> /* Search query observable */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch icon search result + * + * @param el - Icon search result element + * @param options - Options + * + * @returns Icon search result observable + */ +export function watchIconSearchResult( + el: HTMLElement, { index$, query$ }: WatchOptions +): Observable<IconSearchResult> { + switch (el.getAttribute("data-mdx-mode")) { + + case "file": + return combineLatest([ + query$.pipe(distinctUntilKeyChanged("value")), + index$ + .pipe( + map(({ icons }) => Object.values(icons.data) + .map(icon => icon.replace(/\.svg$/, "")) + ) + ) + ]) + .pipe( + map(([{ value }, data]) => search(data, value)), + switchMap(files => index$.pipe( + map(({ icons }) => ({ + data: files.map<Icon>(shortcode => { + return { + shortcode, + url: [ + icons.base, + shortcode, + ".svg" + ].join("") + } + }) + })) + )) + ) + + default: + return combineLatest([ + query$.pipe(distinctUntilKeyChanged("value")), + index$ + .pipe( + map(({ icons, emojis }) => [ + ...Object.keys(icons.data), + ...Object.keys(emojis.data) + ]) + ) + ]) + .pipe( + map(([{ value }, data]) => search(data, value)), + switchMap(shortcodes => index$.pipe( + map(({ icons, emojis }) => ({ + data: shortcodes.map<Icon>(shortcode => { + const category = + shortcode in icons.data + ? icons + : emojis + return { + shortcode, + url: [ + category.base, + category.data[shortcode] + ].join("") + } + }) + })) + )) + ) + } +} + +/** + * Mount icon search result + * + * @param el - Icon search result element + * @param options - Options + * + * @returns Icon search result component observable + */ +export function mountIconSearchResult( + el: HTMLElement, { index$, query$ }: MountOptions +): Observable<Component<IconSearchResult, HTMLElement>> { + const push$ = new Subject<IconSearchResult>() + const boundary$ = watchElementBoundary(el) + .pipe( + filter(Boolean) + ) + + /* Update search result metadata */ + const meta = getElement(":scope > :first-child", el) + push$ + .pipe( + withLatestFrom(query$) + ) + .subscribe(([{ data }, { value }]) => { + if (value) { + switch (data.length) { + + /* No results */ + case 0: + meta.textContent = "No matches" + break + + /* One result */ + case 1: + meta.textContent = "1 match" + break + + /* Multiple result */ + default: + meta.textContent = `${round(data.length)} matches` + } + } else { + meta.textContent = "Type to start searching" + } + }) + + /* Update icon search result list */ + const file = el.getAttribute("data-mdx-mode") === "file" + const list = getElement(":scope > :last-child", el) + push$ + .pipe( + tap(() => list.innerHTML = ""), + switchMap(({ data }) => merge( + of(...data.slice(0, 10)), + of(...data.slice(10)) + .pipe( + bufferCount(10), + zipWith(boundary$), + switchMap(([chunk]) => chunk) + ) + )), + withLatestFrom(query$) + ) + .subscribe(([result, { value }]) => list.appendChild( + renderIconSearchResult(result, value, file) + )) + + /* Create and return component */ + return watchIconSearchResult(el, { query$, index$ }) + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })) + ) +} diff --git a/docs/src/overrides/assets/javascripts/components/index.ts b/docs/src/overrides/assets/javascripts/components/index.ts new file mode 100644 index 00000000..ec6c9dce --- /dev/null +++ b/docs/src/overrides/assets/javascripts/components/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 "./iconsearch" +export * from "./sponsorship" diff --git a/docs/src/overrides/assets/javascripts/components/sponsorship/index.ts b/docs/src/overrides/assets/javascripts/components/sponsorship/index.ts new file mode 100644 index 00000000..711f423a --- /dev/null +++ b/docs/src/overrides/assets/javascripts/components/sponsorship/index.ts @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 } from "rxjs" + +import { getElement, requestJSON } from "~/browser" + +import { renderPrivateSponsor, renderPublicSponsor } from "_/templates" + +import { Component, getComponentElement } from "../_" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Sponsor type + */ +export type SponsorType = + | "user" /* Sponsor is a user */ + | "organization" /* Sponsor is an organization */ + +/** + * Sponsor visibility + */ +export type SponsorVisibility = + | "public" /* Sponsor is a user */ + | "private" /* Sponsor is an organization */ + +/* ------------------------------------------------------------------------- */ + +/** + * Sponsor user + */ +export interface SponsorUser { + type: SponsorType /* Sponsor type */ + name: string /* Sponsor login name */ + image: string /* Sponsor image URL */ + url: string /* Sponsor URL */ +} + +/* ------------------------------------------------------------------------- */ + +/** + * Public sponsor + */ +export interface PublicSponsor { + type: "public" /* Sponsor visibility */ + user: SponsorUser /* Sponsor user */ +} + +/** + * Private sponsor + */ +export interface PrivateSponsor { + type: "private" /* Sponsor visibility */ +} + +/* ------------------------------------------------------------------------- */ + +/** + * Sponsor + */ +export type Sponsor = + | PublicSponsor + | PrivateSponsor + +/* ------------------------------------------------------------------------- */ + +/** + * Sponsorship + */ +export interface Sponsorship { + sponsors: Sponsor[] /* Sponsors */ + total: number /* Total amount */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Mount sponsorship + * + * @param el - Sponsorship element + * + * @returns Sponsorship component observable + */ +export function mountSponsorship( + el: HTMLElement +): Observable<Component<Sponsorship>> { + const sponsorship$ = requestJSON<Sponsorship>( + "https://3if8u9o552.execute-api.us-east-1.amazonaws.com/_/" + ) + + /* Retrieve adjacent components */ + const count = getComponentElement("sponsorship-count") + const total = getComponentElement("sponsorship-total") + + /* Render sponsorship */ + sponsorship$.subscribe(sponsorship => { + el.removeAttribute("hidden") + + /* Render public sponsors with avatar and links */ + const list = getElement(":scope > :first-child", el) + for (const sponsor of sponsorship.sponsors) + if (sponsor.type === "public") + list.appendChild(renderPublicSponsor(sponsor.user)) + + /* Render combined private sponsors */ + list.appendChild(renderPrivateSponsor( + sponsorship.sponsors.filter(({ type }) => ( + type === "private" + )).length + )) + + /* Render sponsorship count and total */ + count.innerText = `${sponsorship.sponsors.length}` + total.innerText = `$ ${sponsorship.total + .toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, ",") + } a month` + }) + + // /* Create and return component */ + return sponsorship$ + .pipe( + map(state => ({ ref: el, ...state })) + ) +} diff --git a/docs/src/overrides/assets/javascripts/custom.ts b/docs/src/overrides/assets/javascripts/custom.ts new file mode 100644 index 00000000..7c3c3847 --- /dev/null +++ b/docs/src/overrides/assets/javascripts/custom.ts @@ -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. + */ + +import { merge, switchMap } from "rxjs" + +import { + getComponentElements, + mountIconSearch, + mountSponsorship +} from "./components" +import { setupAnalytics } from "./integrations" + +/* ---------------------------------------------------------------------------- + * Application + * ------------------------------------------------------------------------- */ + +/* Set up extra analytics events */ +setupAnalytics() + +/* Set up extra component observables */ +const component$ = document$ + .pipe( + switchMap(() => merge( + + /* Icon search */ + ...getComponentElements("iconsearch") + .map(el => mountIconSearch(el)), + + /* Sponsorship */ + ...getComponentElements("sponsorship") + .map(el => mountSponsorship(el)) + )) + ) + +/* Subscribe to all components */ +component$.subscribe() diff --git a/docs/src/overrides/assets/javascripts/integrations/analytics/index.ts b/docs/src/overrides/assets/javascripts/integrations/analytics/index.ts new file mode 100644 index 00000000..658add2a --- /dev/null +++ b/docs/src/overrides/assets/javascripts/integrations/analytics/index.ts @@ -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 { fromEvent } from "rxjs" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Set up extra analytics events + */ +export function setupAnalytics(): void { + const { origin } = new URL(location.href) + fromEvent(document.body, "click") + .subscribe(ev => { + if (ev.target instanceof HTMLElement) { + const el = ev.target.closest("a") + if (el && el.origin !== origin) + ga("send", "event", "outbound", "click", el.href) + } + }) +} diff --git a/docs/src/overrides/assets/javascripts/integrations/index.ts b/docs/src/overrides/assets/javascripts/integrations/index.ts new file mode 100644 index 00000000..9179f2a2 --- /dev/null +++ b/docs/src/overrides/assets/javascripts/integrations/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 "./analytics" diff --git a/docs/src/overrides/assets/javascripts/templates/iconsearch/index.tsx b/docs/src/overrides/assets/javascripts/templates/iconsearch/index.tsx new file mode 100644 index 00000000..13cafa6d --- /dev/null +++ b/docs/src/overrides/assets/javascripts/templates/iconsearch/index.tsx @@ -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 { wrap } from "fuzzaldrin-plus" + +import { translation } from "~/_" +import { h } from "~/utilities" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Icon + */ +export interface Icon { + shortcode: string /* Icon shortcode */ + url: string /* Icon URL */ +} + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Highlight an icon search result + * + * @param icon - Icon + * @param query - Search query + * + * @returns Highlighted result + */ +function highlight(icon: Icon, query: string): string { + return wrap(icon.shortcode, query, { + wrap: { + tagOpen: "<b>", + tagClose: "</b>" + } + }) +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Render an icon search result + * + * @param icon - Icon + * @param query - Search query + * @param file - Render as file + * + * @returns Element + */ +export function renderIconSearchResult( + icon: Icon, query: string, file?: boolean +): HTMLElement { + return ( + <li class="mdx-iconsearch-result__item"> + <span class="twemoji"> + <img src={icon.url} /> + </span> + <button + class="md-clipboard--inline" + title={translation("clipboard.copy")} + data-clipboard-text={file ? icon.shortcode : `:${icon.shortcode}:`} + > + <code>{ + file + ? highlight(icon, query) + : `:${highlight(icon, query)}:` + }</code> + </button> + </li> + ) +} diff --git a/docs/src/overrides/assets/javascripts/templates/index.ts b/docs/src/overrides/assets/javascripts/templates/index.ts new file mode 100644 index 00000000..02376b3d --- /dev/null +++ b/docs/src/overrides/assets/javascripts/templates/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 "./iconsearch" +export * from "./sponsorship" diff --git a/docs/src/overrides/assets/javascripts/templates/sponsorship/index.tsx b/docs/src/overrides/assets/javascripts/templates/sponsorship/index.tsx new file mode 100644 index 00000000..7891c2e0 --- /dev/null +++ b/docs/src/overrides/assets/javascripts/templates/sponsorship/index.tsx @@ -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 { h } from "~/utilities" + +import { SponsorUser } from "_/components" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Render public sponsor + * + * @param user - Sponsor user + * + * @returns Element + */ +export function renderPublicSponsor( + user: SponsorUser +): HTMLElement { + const title = `@${user.name}` + return ( + <a href={user.url} title={title} class="mdx-sponsorship__item"> + <img src={user.image} /> + </a> + ) +} + +/** + * Render private sponsor + * + * @param count - Number of private sponsors + * + * @returns Element + */ +export function renderPrivateSponsor( + count: number +): HTMLElement { + return ( + <a + href="https://github.com/sponsors/squidfunk?metadata_origin=docs" + class="mdx-sponsorship__item mdx-sponsorship__item--private" + > + +{count} + </a> + ) +} diff --git a/docs/src/overrides/assets/stylesheets/custom.scss b/docs/src/overrides/assets/stylesheets/custom.scss new file mode 100644 index 00000000..8235e7d0 --- /dev/null +++ b/docs/src/overrides/assets/stylesheets/custom.scss @@ -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 +//// + +// ---------------------------------------------------------------------------- +// Dependencies +// ---------------------------------------------------------------------------- + +@import "material-color"; +@import "material-shadows"; + +// ---------------------------------------------------------------------------- +// Local imports +// ---------------------------------------------------------------------------- + +@import "utilities/break"; +@import "utilities/convert"; + +@import "config"; + +@import "custom/typeset"; + +@import "custom/layout/banner"; +@import "custom/layout/hero"; +@import "custom/layout/iconsearch"; +@import "custom/layout/sponsorship"; diff --git a/docs/src/overrides/assets/stylesheets/custom/_typeset.scss b/docs/src/overrides/assets/stylesheets/custom/_typeset.scss new file mode 100644 index 00000000..bef30073 --- /dev/null +++ b/docs/src/overrides/assets/stylesheets/custom/_typeset.scss @@ -0,0 +1,294 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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 +// ---------------------------------------------------------------------------- + +// Pumping heart animation +@keyframes heart { + 0%, + 40%, + 80%, + 100% { + transform: scale(1); + } + + 20%, + 60% { + transform: scale(1.15); + } +} + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Twitter icon + .twitter { + color: #00acee; + } + + // Mastodon icon - it's not the exact brand color, because that doesn't work + // well on dark backgrounds, so we lightened it up a bit. + .mastodon { + color: #897ff8; + } + + // Insiders video + .mdx-video { + width: auto; + + // Insiders video container + &__inner { + position: relative; + width: 100%; + height: 0; + padding-bottom: 56.138%; + } + + // Insiders video iframe + iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + border: none; + } + } + + // Pumping heart + .mdx-heart { + animation: heart 1000ms infinite; + } + + // Insiders color (for links, etc.) // remove + .mdx-insiders { + color: $clr-pink-500; + } + + // BETA ##################################################################### + + // Badge + .mdx-badge { + font-size: 0.85em; + + // Badge with heart + &--heart { + --md-typeset-a-color: hsla(#{hex2hsl($clr-pink-500)}, 1); + --md-accent-fg-color: hsla(#{hex2hsl($clr-pink-a200)}, 1); + --md-accent-fg-color--transparent: hsla(#{hex2hsl($clr-pink-500)}, 0.1); + + // Animate icon + .twemoji { + animation: heart 1000ms infinite; + } + } + + // Badge moved to the right + &--right { + float: right; + margin-left: 0.35em; + } + + // Badge icon + &__icon { + padding: px2rem(4px); + background: var(--md-accent-fg-color--transparent); + border-start-start-radius: px2rem(2px); + border-end-start-radius: px2rem(2px); + + // If icon is alone, round corners + &:last-child { + border-radius: px2rem(2px); + } + } + + // Badge text + &__text { + padding: px2rem(4px) px2rem(6px); + border-start-end-radius: px2rem(2px); + border-end-end-radius: px2rem(2px); + box-shadow: 0 0 0 1px inset var(--md-accent-fg-color--transparent); + } + } + + // BETA ##################################################################### + + // Switch buttons + .mdx-switch button { + cursor: pointer; + transition: opacity 250ms; + + // Button on focus/hover + &:is(:focus, :hover) { + opacity: 0.75; + } + + // Code block + > code { + display: block; + color: var(--md-primary-bg-color); + background-color: var(--md-primary-fg-color); + } + } + + // Two-column layout + .mdx-columns { + + // Column + ol, + ul { + columns: 2; + + // [mobile portrait -]: Reset columns on mobile + @include break-to-device(mobile portrait) { + columns: initial; + } + } + + // Column item + li { + break-inside: avoid; + } + } + + // Language list + .mdx-flags { + margin: 2em auto; + + // Language list + ol { + list-style: none; + + // Language list item + li { + margin-bottom: 1em; + } + } + + // Language item + &__item { + display: flex; + gap: px2rem(12px); + } + + // Language content + &__content { + display: flex; + flex: 1; + flex-direction: column; + + // Language name + span { + display: inline-flex; + align-items: baseline; + justify-content: space-between; + } + + // Language link + > span:nth-child(2) { + font-size: 80%; + } + + // Language code + code { + float: right; + } + } + } + + // Social card + .mdx-social { + position: relative; + height: min(#{px2rem(540px)}, 80vw); + + // Social card image on hover + &:hover .mdx-social__image { + background-color: rgba(228, 228, 228, 0.05); + } + + // Social card layer + &__layer { + position: absolute; + margin-top: px2rem(80px); + transition: 250ms cubic-bezier(0.7, 0, 0.3, 1); + transform-style: preserve-3d; + + // Social card layer on hover + &:hover { + + // Social card label + .mdx-social__label { + opacity: 1; + } + + // Social card image + .mdx-social__image { + background-color: rgba(127, 127, 127, 0.99); + } + + // Hide top layers + ~ .mdx-social__layer { + opacity: 0; + } + } + } + + // Social card image + &__image { + box-shadow: + px2rem(-5px) px2rem(5px) px2rem(10px) + rgba(0, 0, 0, 0.05); + transition: all 250ms; + transform: rotate(-40deg) skew(15deg, 15deg) scale(0.7); + + // Actual image + img { + display: block; + } + } + + // Social card label + &__label { + position: absolute; + display: block; + padding: px2rem(4px) px2rem(8px); + color: var(--md-default-bg-color); + background-color: var(--md-default-fg-color--light); + opacity: 0; + transition: all 250ms; + } + + // Transform on hover + @for $i from 6 through 0 { + &:hover .mdx-social__layer:nth-child(#{$i}) { + transform: translateY(#{($i - 3) * -10}px); + } + } + } +} diff --git a/docs/src/overrides/assets/stylesheets/custom/layout/_banner.scss b/docs/src/overrides/assets/stylesheets/custom/layout/_banner.scss new file mode 100644 index 00000000..b67d7fff --- /dev/null +++ b/docs/src/overrides/assets/stylesheets/custom/layout/_banner.scss @@ -0,0 +1,66 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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 { + color: var(--md-footer-fg-color--lighter); + + // Don't wrap name of blog article + strong { + color: var(--md-footer-fg-color); + white-space: nowrap; + } + + a { + color: var(--md-footer-fg-color); + + &:focus, + &:hover { + color: currentcolor; + + .twemoji { + background-color: var(--md-footer-fg-color); + box-shadow: none; + } + } + } + + .twemoji { + display: inline-block; + width: px2rem(24px); + height: px2rem(24px); + padding: px2rem(5px); + vertical-align: bottom; + border-radius: 100%; + box-shadow: 0 0 0 px2rem(1px) currentcolor inset; + transition: all 250ms; + + svg { + display: block; + max-height: initial; + } + } +} diff --git a/docs/src/overrides/assets/stylesheets/custom/layout/_hero.scss b/docs/src/overrides/assets/stylesheets/custom/layout/_hero.scss new file mode 100644 index 00000000..428cd37e --- /dev/null +++ b/docs/src/overrides/assets/stylesheets/custom/layout/_hero.scss @@ -0,0 +1,123 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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 +// ---------------------------------------------------------------------------- + +// Landing page container +.mdx-container { + padding-top: px2rem(20px); + background: + url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1123 258'><path d='M1124,2c0,0 0,256 0,256l-1125,0l0,-48c0,0 16,5 55,5c116,0 197,-92 325,-92c121,0 114,46 254,46c140,0 214,-167 572,-166Z' style='fill: hsla(0, 0%, 100%, 1)' /></svg>") no-repeat bottom, + linear-gradient( + to bottom, + var(--md-primary-fg-color), + hsla(280, 67%, 55%, 1) 99%, + var(--md-default-bg-color) 99% + ); + + // Adjust background for slate theme + [data-md-color-scheme="slate"] & { + background: + url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1123 258'><path d='M1124,2c0,0 0,256 0,256l-1125,0l0,-48c0,0 16,5 55,5c116,0 197,-92 325,-92c121,0 114,46 254,46c140,0 214,-167 572,-166Z' style='fill: hsla(230, 15%, 14%, 1)' /></svg>") no-repeat bottom, + linear-gradient( + to bottom, + var(--md-primary-fg-color), + hsla(230, 15%, 25%, 1) 99%, + var(--md-default-bg-color) 99% + ); + } +} + +// Landing page hero +.mdx-hero { + margin: 0 px2rem(16px); + color: var(--md-primary-bg-color); + + // Hero headline + h1 { + margin-bottom: px2rem(20px); + font-weight: 700; + color: currentcolor; + + // [mobile portrait -]: Larger hero headline + @include break-to-device(mobile portrait) { + font-size: px2rem(28px); + } + } + + // Hero content + &__content { + padding-bottom: px2rem(120px); + } + + // [tablet landscape +]: Columnar display + @include break-from-device(tablet landscape) { + display: flex; + align-items: stretch; + + // Adjust spacing and set dimensions + &__content { + max-width: px2rem(380px); + padding-bottom: 14vw; + margin-top: px2rem(70px); + } + + // Hero image + &__image { + order: 1; + width: px2rem(760px); + transform: translateX(#{px2rem(80px)}); + } + } + + // [screen +]: Columnar display and adjusted spacing + @include break-from-device(screen) { + + // Hero image + &__image { + transform: translateX(#{px2rem(160px)}); + } + } + + // Button + .md-button { + margin-top: px2rem(10px); + margin-right: px2rem(10px); + color: var(--md-primary-bg-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); + } + + // Primary button + &--primary { + color: hsla(280, 37%, 48%, 1); + background-color: var(--md-primary-bg-color); + border-color: var(--md-primary-bg-color); + } + } +} diff --git a/docs/src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss b/docs/src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss new file mode 100644 index 00000000..651c4135 --- /dev/null +++ b/docs/src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss @@ -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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Icon search + .mdx-iconsearch { + position: relative; + background-color: var(--md-default-bg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z1); + transition: box-shadow 125ms; + + // Icon search on focus/hover + &:is(:focus-within, :hover) { + box-shadow: var(--md-shadow-z2); + } + + // Icon search input + .md-input { + background: var(--md-default-bg-color); + box-shadow: none; + + // Slate theme, i.e. dark mode + [data-md-color-scheme="slate"] & { + background: var(--md-code-bg-color); + } + } + } + + // Icon search result + .mdx-iconsearch-result { + max-height: 50vh; + overflow-y: auto; + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + touch-action: pan-y; + scrollbar-width: thin; + scrollbar-color: var(--md-default-fg-color--lighter) transparent; + + // Icon search result inside tooltip + .md-tooltip & { + max-height: px2rem(205px); + } + + // 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); + } + } + + // Icon search result metadata + &__meta { + position: absolute; + top: px2rem(8px); + right: px2rem(12px); + font-size: px2rem(12.8px); + color: var(--md-default-fg-color--lighter); + } + + // Icon search result list + &__list { + padding: 0; + margin: 0; + // Hack: necessary because of increased specificity due to the PostCSS + // plugin which prefixes this with `[dir=...]` selectors. + margin-inline-start: 0; + list-style: none; + } + + // Icon search result item + &__item { + padding: px2rem(4px) px2rem(12px); + margin: 0; + // Hack: necessary because of increased specificity due to the PostCSS + // plugin which prefixes this with `[dir=...]` selectors. + margin-inline-start: 0; + border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest); + + // Omit border on last child + &:last-child { + border-bottom: none; + } + + // Item content + > * { + margin-right: px2rem(12px); + } + + // Set icon dimensions to fit + img { + width: px2rem(18px); + height: px2rem(18px); + + // Slate theme, i.e. dark mode + [data-md-color-scheme="slate"] &[src*="squidfunk"] { + filter: invert(1); /* stylelint-disable-line */ + } + } + } + } +} diff --git a/docs/src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss b/docs/src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss new file mode 100644 index 00000000..e2b16570 --- /dev/null +++ b/docs/src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss @@ -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 +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Premium sponsors + .mdx-premium { + + // Paragraphs + p { + margin: 2em 0; + text-align: center; + } + + // Premium sponsor image + img { + height: px2rem(65px); + } + + // Premium sponsor list + p:last-child { + display: flex; + flex-wrap: wrap; + justify-content: center; + + // Premium sponsor link + > a { + display: block; + flex-shrink: 0; + } + } + } + + // Sponsorship + .mdx-sponsorship { + + // Sponsorship list + &__list { + margin: 2em 0; + + // Clearfix, because we can't use overflow: auto + &::after { + display: block; + clear: both; + content: ""; + } + } + + // Sponsorship item + &__item { + display: block; + float: inline-start; + width: px2rem(32px); + height: px2rem(32px); + margin: px2rem(4px); + overflow: hidden; + border-radius: 100%; + transition: + color 125ms, + transform 125ms; + transform: scale(1); + + // Sponsor item on focus/hover + &:is(:focus, :hover) { + transform: scale(1.1); + + // Sponsor avatar + img { + filter: grayscale(0%); + } + } + + // Private sponsor + &--private { + 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); + } + + // Sponsor avatar + img { + display: block; + width: 100%; + height: auto; + filter: grayscale(100%) opacity(75%); + transition: filter 125ms; + } + } + } + + // Sponsorship button + .mdx-sponsorship-button { + font-weight: 400; + } + + // Sponsorship count and total + .mdx-sponsorship-count, + .mdx-sponsorship-total { + font-weight: 700; + } +} diff --git a/docs/src/overrides/home.html b/docs/src/overrides/home.html new file mode 100644 index 00000000..3f54ca82 --- /dev/null +++ b/docs/src/overrides/home.html @@ -0,0 +1,106 @@ +<!-- + Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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" %} + +<!-- Render hero under tabs --> +{% block tabs %} + {{ super() }} + + <!-- Additional styles for landing page --> + <style> + + /* Application header should be static for the landing page */ + .md-header { + position: initial; + } + + /* Remove spacing, as we cannot hide it completely */ + .md-main__inner { + margin: 0; + } + + /* Hide main content for now */ + .md-content { + display: none; + } + + /* Hide table of contents */ + @media screen and (min-width: 60em) { + .md-sidebar--secondary { + display: none; + } + } + + /* Hide navigation */ + @media screen and (min-width: 76.25em) { + .md-sidebar--primary { + display: none; + } + } + </style> + + <!-- Hero for landing page --> + <section class="mdx-container"> + <div class="md-grid md-typeset"> + <div class="mdx-hero"> + + <!-- Hero image --> + <div class="mdx-hero__image"> + <img + src="assets/images/illustration.png" + alt="" + width="1659" + height="1200" + draggable="false" + > + </div> + + <!-- Hero content --> + <div class="mdx-hero__content"> + <h1>Technical documentation that just works</h1> + <p>{{ config.site_description }}. Set up in 5 minutes.</p> + <a + href="{{ page.next_page.url | url }}" + title="{{ page.next_page.title | e }}" + class="md-button md-button--primary" + > + Quick start + </a> + <a + href="{{ 'insiders/' | url }}" + title="Material for MkDocs Insiders" + class="md-button" + > + Get Insiders + </a> + </div> + </div> + </div> + </section> +{% endblock %} + +<!-- Content --> +{% block content %}{% endblock %} + +<!-- Application footer --> +{% block footer %}{% endblock %} diff --git a/docs/src/overrides/hooks/shortcodes.py b/docs/src/overrides/hooks/shortcodes.py new file mode 100644 index 00000000..5b02e3cf --- /dev/null +++ b/docs/src/overrides/hooks/shortcodes.py @@ -0,0 +1,283 @@ +# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from __future__ import annotations + +import posixpath +import re + +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.structure.files import File, Files +from mkdocs.structure.pages import Page +from re import Match + +# ----------------------------------------------------------------------------- +# Hooks +# ----------------------------------------------------------------------------- + +# @todo +def on_page_markdown( + markdown: str, *, page: Page, config: MkDocsConfig, files: Files +): + + # Replace callback + def replace(match: Match): + type, args = match.groups() + args = args.strip() + if type == "version": + if args.startswith("insiders-"): + return _badge_for_version_insiders(args, page, files) + else: + return _badge_for_version(args, page, files) + elif type == "sponsors": return _badge_for_sponsors(page, files) + elif type == "flag": return flag(args, page, files) + elif type == "option": return option(args) + elif type == "setting": return setting(args) + elif type == "feature": return _badge_for_feature(args, page, files) + elif type == "plugin": return _badge_for_plugin(args, page, files) + elif type == "extension": return _badge_for_extension(args, page, files) + elif type == "utility": return _badge_for_utility(args, page, files) + elif type == "example": return _badge_for_example(args, page, files) + elif type == "default": + if args == "none": return _badge_for_default_none(page, files) + elif args == "computed": return _badge_for_default_computed(page, files) + else: return _badge_for_default(args, page, files) + + # Otherwise, raise an error + raise RuntimeError(f"Unknown shortcode: {type}") + + # Find and replace all external asset URLs in current page + return re.sub( + r"<!-- md:(\w+)(.*?) -->", + replace, markdown, flags = re.I | re.M + ) + +# ----------------------------------------------------------------------------- +# Helper functions +# ----------------------------------------------------------------------------- + +# Create a flag of a specific type +def flag(args: str, page: Page, files: Files): + type, *_ = args.split(" ", 1) + if type == "experimental": return _badge_for_experimental(page, files) + elif type == "required": return _badge_for_required(page, files) + elif type == "customization": return _badge_for_customization(page, files) + elif type == "metadata": return _badge_for_metadata(page, files) + elif type == "multiple": return _badge_for_multiple(page, files) + raise RuntimeError(f"Unknown type: {type}") + +# Create a linkable option +def option(type: str): + _, *_, name = re.split(r"[.:]", type) + return f"[`{name}`](#+{type}){{ #+{type} }}\n\n" + +# Create a linkable setting - @todo append them to the bottom of the page +def setting(type: str): + _, *_, name = re.split(r"[.*]", type) + return f"`{name}` {{ #{type} }}\n\n[{type}]: #{type}\n\n" + +# ----------------------------------------------------------------------------- + +# Resolve path of file relative to given page - the posixpath always includes +# one additional level of `..` which we need to remove +def _resolve_path(path: str, page: Page, files: Files): + path, anchor, *_ = f"{path}#".split("#") + path = _resolve(files.get_file_from_path(path), page) + return "#".join([path, anchor]) if anchor else path + +# Resolve path of file relative to given page - the posixpath always includes +# one additional level of `..` which we need to remove +def _resolve(file: File, page: Page): + path = posixpath.relpath(file.src_uri, page.file.src_uri) + return posixpath.sep.join(path.split(posixpath.sep)[1:]) + +# ----------------------------------------------------------------------------- + +# Create badge +def _badge(icon: str, text: str = "", type: str = ""): + classes = f"mdx-badge mdx-badge--{type}" if type else "mdx-badge" + return "".join([ + f"<span class=\"{classes}\">", + *([f"<span class=\"mdx-badge__icon\">{icon}</span>"] if icon else []), + *([f"<span class=\"mdx-badge__text\">{text}</span>"] if text else []), + f"</span>", + ]) + +# Create sponsors badge +def _badge_for_sponsors(page: Page, files: Files): + icon = "material-heart" + href = _resolve_path("insiders/index.md", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Sponsors only')", + type = "heart" + ) + +# Create badge for version +def _badge_for_version(text: str, page: Page, files: Files): + spec = text + path = f"changelog/index.md#{spec}" + + # Return badge + icon = "material-tag-outline" + href = _resolve_path("conventions.md#version", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Minimum version')", + text = f"[{text}]({_resolve_path(path, page, files)})" if spec else "" + ) + +# Create badge for version of Insiders +def _badge_for_version_insiders(text: str, page: Page, files: Files): + spec = text.replace("insiders-", "") + path = f"insiders/changelog/index.md#{spec}" + + # Return badge + icon = "material-tag-heart-outline" + href = _resolve_path("conventions.md#version-insiders", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Minimum version')", + text = f"[{text}]({_resolve_path(path, page, files)})" if spec else "" + ) + +# Create badge for feature +def _badge_for_feature(text: str, page: Page, files: Files): + icon = "material-toggle-switch" + href = _resolve_path("conventions.md#feature", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Optional feature')", + text = text + ) + +# Create badge for plugin +def _badge_for_plugin(text: str, page: Page, files: Files): + icon = "material-floppy" + href = _resolve_path("conventions.md#plugin", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Plugin')", + text = text + ) + +# Create badge for extension +def _badge_for_extension(text: str, page: Page, files: Files): + icon = "material-language-markdown" + href = _resolve_path("conventions.md#extension", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Markdown extension')", + text = text + ) + +# Create badge for utility +def _badge_for_utility(text: str, page: Page, files: Files): + icon = "material-package-variant" + href = _resolve_path("conventions.md#utility", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Third-party utility')", + text = text + ) + +# Create badge for example +def _badge_for_example(text: str, page: Page, files: Files): + return "\n".join([ + _badge_for_example_download(text, page, files), + _badge_for_example_view(text, page, files) + ]) + +# Create badge for example view +def _badge_for_example_view(text: str, page: Page, files: Files): + icon = "material-folder-eye" + href = f"https://mkdocs-material.github.io/examples/{text}/" + return _badge( + icon = f"[:{icon}:]({href} 'View example')", + type = "right" + ) + +# Create badge for example download +def _badge_for_example_download(text: str, page: Page, files: Files): + icon = "material-folder-download" + href = f"https://mkdocs-material.github.io/examples/{text}.zip" + return _badge( + icon = f"[:{icon}:]({href} 'Download example')", + text = f"[`.zip`]({href})", + type = "right" + ) + +# Create badge for default value +def _badge_for_default(text: str, page: Page, files: Files): + icon = "material-water" + href = _resolve_path("conventions.md#default", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Default value')", + text = text + ) + +# Create badge for empty default value +def _badge_for_default_none(page: Page, files: Files): + icon = "material-water-outline" + href = _resolve_path("conventions.md#default", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Default value is empty')" + ) + +# Create badge for computed default value +def _badge_for_default_computed(page: Page, files: Files): + icon = "material-water-check" + href = _resolve_path("conventions.md#default", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Default value is computed')" + ) + +# Create badge for metadata property flag +def _badge_for_metadata(page: Page, files: Files): + icon = "material-list-box-outline" + href = _resolve_path("conventions.md#metadata", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Metadata property')" + ) + +# Create badge for required value flag +def _badge_for_required(page: Page, files: Files): + icon = "material-alert" + href = _resolve_path("conventions.md#required", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Required value')" + ) + +# Create badge for customization flag +def _badge_for_customization(page: Page, files: Files): + icon = "material-brush-variant" + href = _resolve_path("conventions.md#customization", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Customization')" + ) + +# Create badge for multiple instance flag +def _badge_for_multiple(page: Page, files: Files): + icon = "material-inbox-multiple" + href = _resolve_path("conventions.md#multiple-instances", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Multiple instances')" + ) + +# Create badge for experimental flag +def _badge_for_experimental(page: Page, files: Files): + icon = "material-flask-outline" + href = _resolve_path("conventions.md#experimental", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Experimental')" + ) diff --git a/docs/src/overrides/hooks/translations.html b/docs/src/overrides/hooks/translations.html new file mode 100644 index 00000000..ab41c77d --- /dev/null +++ b/docs/src/overrides/hooks/translations.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. +--> + +<!-- Render translation language --> +{% macro render_language(language) %} + <div class="mdx-flags__item" markdown> + :flag_{{ language.flag }}:{ .lg .middle } + <span class="mdx-flags__content"> + <span> + <strong>{{ language.name }}</strong> + <code>{{ language.code }}</code> + </span> + {% if language.miss %} + <span> + <a href="{{ language.link }}"> + {{ language.miss | length }} translations missing + </a> + </span> + {% else %} + <small>Complete</small> + {% endif %} + </span> + </div> +{% endmacro %} + +<!-- Render translations --> +{% macro render(translations, start = 1) %} + <div class="mdx-columns mdx-flags" markdown> + <ol markdown> + {% for language in translations %} + <li markdown>{{ render_language(language) }}</li> + {% endfor %} + </ol> + </div> +{% endmacro %} diff --git a/docs/src/overrides/hooks/translations.py b/docs/src/overrides/hooks/translations.py new file mode 100644 index 00000000..661fd18e --- /dev/null +++ b/docs/src/overrides/hooks/translations.py @@ -0,0 +1,193 @@ +# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION 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 os +import re + +from glob import iglob +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.structure.pages import Page +from urllib.parse import urlencode, urlparse + +# ----------------------------------------------------------------------------- +# Hooks +# ----------------------------------------------------------------------------- + +# Determine missing translations and render language overview in the setup +# guide, including links to provide missing translations. +def on_page_markdown(markdown: str, *, page: Page, config: MkDocsConfig, files): + issue_url = "https://github.com/squidfunk/mkdocs-material/issues/new" + if page.file.src_uri != "setup/changing-the-language.md": + return + + # Collect all existing languages + names: dict[str, str] = {} + known: dict[str, dict[str, str]] = {} + for path in iglob("src/templates/partials/languages/*.html"): + with open(path, "r", encoding = "utf-8") as f: + data = f.read() + + # Extract language code and name + name, = re.findall(r"<!-- Translations: (.+) -->", data) + code, _ = os.path.splitext(os.path.basename(path)) + + # Map names and available translations + names[code] = name + known[code] = dict(re.findall( + r"^ \"([^\"]+)\": \"([^\"]*)\"(?:,|$)?", data, + re.MULTILINE + )) + + # Remove technical stuff + for key in [ + "direction", + "search.config.pipeline", + "search.config.lang", + "search.config.separator" + ]: + if key in known[code]: + del known[code][key] + + # Traverse all languages and compute missing translations + languages = [] + reference = set(known["en"]) + for code, name in names.items(): + miss = reference - set(known[code]) + + # Check each translations + translations: list[str] = [] + for key, value in known["en"].items(): + if key in known[code]: + translations.append( + f" \"{key}\": \"{known[code][key]}\"" + ) + else: + translations.append( + f" \"{key}\": \"{value} ⬅️\"" + ) + + # Assemble GitHub issue URL + link = urlparse(issue_url) + link = link._replace(query = urlencode({ + "template": "04-add-translations.yml", + "title": f"Update {name} translations", + "translations": "\n".join([ + "{% macro t(key) %}{{ {", + ",\n".join(translations), + "}[key] }}{% endmacro %}" + ]), + "country-flag": f":flag_{countries[code]}:" + })) + + # Add translation + languages.append({ + "flag": countries[code], + "code": code, + "name": name, + "link": link.geturl(), + "miss": miss + }) + + # Load template and render translations + env = config.theme.get_env() + template = env.get_template( "hooks/translations.html") + translations = template.module.render( + sorted(languages, key = lambda language: language["name"]) + ) + + # Replace translation marker + return markdown.replace( + "<!-- hooks/translations.py -->", "\n".join( + [line.lstrip() for line in translations.split("\n") + ] + )) + +# ----------------------------------------------------------------------------- +# Data +# ----------------------------------------------------------------------------- + +# Map ISO 639-1 (languages) to ISO 3166 (countries) +countries = dict({ + "af": "za", + "ar": "ae", + "be": "by", + "bg": "bg", + "bn": "bd", + "ca": "es", + "cs": "cz", + "da": "dk", + "de": "de", + "el": "gr", + "en": "us", + "eo": "eu", + "es": "es", + "et": "ee", + "eu": "es", + "fa": "ir", + "fi": "fi", + "fr": "fr", + "gl": "es", + "he": "il", + "hi": "in", + "hr": "hr", + "hu": "hu", + "hy": "am", + "id": "id", + "is": "is", + "it": "it", + "ja": "jp", + "ka": "ge", + "kn": "in", + "ko": "kr", + "ku-IQ": "iq", + "lb": "lu", + "lt": "lt", + "lv": "lv", + "mk": "mk", + "mn": "mn", + "ms": "my", + "my": "mm", + "nb": "no", + "nl": "nl", + "nn": "no", + "pl": "pl", + "pt-BR": "br", + "pt": "pt", + "ro": "ro", + "ru": "ru", + "sa": "in", + "sh": "rs", + "si": "lk", + "sk": "sk", + "sl": "si", + "sr": "rs", + "sv": "se", + "te": "in", + "th": "th", + "tl": "ph", + "tr": "tr", + "uk": "ua", + "ur": "pk", + "uz": "uz", + "vi": "vn", + "zh": "cn", + "zh-Hant": "cn", + "zh-TW": "tw" +}) diff --git a/docs/src/overrides/main.html b/docs/src/overrides/main.html new file mode 100644 index 00000000..39b68b5a --- /dev/null +++ b/docs/src/overrides/main.html @@ -0,0 +1,59 @@ +<!-- + Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION 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" %} + +<!-- Custom front matter --> +{% block extrahead %} + + <!-- Extra style sheets (can't be set in mkdocs.yml due to content hash) --> + <link + rel="stylesheet" + href="{{ 'assets/stylesheets/custom.css' | url }}" + /> +{% endblock %} + +<!-- Announcement bar --> +{% block announce %} + For updates follow <strong>@squidfunk</strong> on + <a rel="me" href="https://fosstodon.org/@squidfunk"> + <span class="twemoji mastodon"> + {% include ".icons/fontawesome/brands/mastodon.svg" %} + </span> + <strong>Fosstodon</strong> + </a> + and + <a href="https://twitter.com/squidfunk"> + <span class="twemoji twitter"> + {% include ".icons/fontawesome/brands/twitter.svg" %} + </span> + <strong>Twitter</strong> + </a> +{% endblock %} + +<!-- Theme-related JavaScript --> +{% block scripts %} + {{ super() }} + + <!-- Extra JavaScript (can't be set in mkdocs.yml due to content hash) --> + <script src="{{ 'assets/javascripts/custom.js' | url }}"></script> +{% endblock %} |
