1
0
mirror of https://github.com/squidfunk/mkdocs-material.git synced 2024-11-27 17:00:54 +01:00

Merge of Insiders features tied to 'Ghost Pepper' funding goal

This commit is contained in:
squidfunk 2021-11-13 11:39:10 +01:00
parent 41785f1c57
commit 887b7115fc
58 changed files with 834 additions and 243 deletions

View File

@ -34,11 +34,7 @@ trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
# Makefiles
# Python
[*.py]
indent_style = space
indent_size = 4
# Makefiles
[Makefile]
indent_style = tab
indent_size = 8

1
.gitignore vendored
View File

@ -43,6 +43,7 @@
*.tsbuildinfo
.cache
.eslintcache
__pycache__
# -----------------------------------------------------------------------------
# General

View File

@ -80,6 +80,7 @@
"selector-class-pattern": null,
"selector-combinator-space-before": null,
"selector-descendant-combinator-no-non-space": null,
"selector-id-pattern": null,
"selector-max-empty-lines": 0,
"selector-max-id": 0,
"selector-no-qualifying-type": null,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -34,7 +34,7 @@
{% endif %}
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.75e88914.min.css' | url }}">
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.9e98b581.min.css' | url }}">
{% if config.theme.palette %}
{% set palette = config.theme.palette %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.9204c3b2.min.css' | url }}">
@ -62,6 +62,7 @@
{% for path in config["extra_css"] %}
<link rel="stylesheet" href="{{ path | url }}">
{% endfor %}
{% include "partials/javascripts/base.html" %}
{% block analytics %}
{% include "partials/integrations/analytics.html" %}
{% endblock %}
@ -81,7 +82,6 @@
<body dir="{{ direction }}">
{% endif %}
{% set features = config.theme.features or [] %}
{% include "partials/javascripts/base.html" %}
{% if not config.theme.palette is mapping %}
{% include "partials/javascripts/palette.html" %}
{% endif %}
@ -105,6 +105,18 @@
</aside>
{% endif %}
</div>
{% if config.extra.version %}
<div data-md-component="outdated" hidden>
<aside class="md-banner md-banner--warning">
{% if self.outdated() %}
<div class="md-banner__inner md-grid md-typeset">
{% block outdated %}{% endblock %}
</div>
{% include "partials/javascripts/outdated.html" %}
{% endif %}
</aside>
</div>
{% endif %}
{% block header %}
{% include "partials/header.html" %}
{% endblock %}
@ -157,13 +169,12 @@
<h1>{{ page.title | d(config.site_name, true)}}</h1>
{% endif %}
{{ page.content }}
{% if page and page.meta %}
{% if page.meta.git_revision_date_localized or
{% if page and page.meta and (
page.meta.git_revision_date_localized or
page.meta.revision_date
%}
) %}
{% include "partials/source-file.html" %}
{% endif %}
{% endif %}
{% endblock %}
{% block disqus %}
{% include "partials/integrations/disqus.html" %}
@ -217,7 +228,7 @@
</script>
{% endblock %}
{% block scripts %}
<script src="{{ 'assets/javascripts/bundle.ccba565e.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.6273739e.min.js' | url }}"></script>
{% for path in config["extra_javascript"] %}
<script src="{{ path | url }}"></script>
{% endfor %}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@
-#}
{% extends "base.html" %}
{% block extrahead %}
<link rel="stylesheet" href="{{ 'overrides/assets/stylesheets/main.bf3dc0a9.min.css' | url }}">
<link rel="stylesheet" href="{{ 'overrides/assets/stylesheets/main.a9ec9cc0.min.css' | url }}">
{% endblock %}
{% block announce %}
<a href="https://twitter.com/squidfunk">
@ -16,5 +16,5 @@
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{ 'overrides/assets/javascripts/bundle.525231ca.min.js' | url }}"></script>
<script src="{{ 'overrides/assets/javascripts/bundle.35fbbc46.min.js' | url }}"></script>
{% endblock %}

View File

@ -1,4 +1,4 @@
{#-
This file was automatically generated - do not edit
-#}
<script>function __prefix(e){return new URL("{{ base_url }}",location).pathname+"."+e}function __get(e,t=localStorage){return JSON.parse(t.getItem(__prefix(e)))}</script>
<script>function __md_scope(e,t,_){return new URL(_||(t===localStorage?"{{ config.extra.scope | d(base_url) }}":"{{ base_url }}"),location).pathname+"."+e}function __md_get(e,t=localStorage,_){return JSON.parse(t.getItem(__md_scope(e,t,_)))}function __md_set(e,t,_=localStorage,o){try{_.setItem(__md_scope(e,_,o),JSON.stringify(t))}catch(e){}}</script>

View File

@ -1,4 +1,4 @@
{#-
This file was automatically generated - do not edit
-#}
<script>var palette=__get("__palette");if(null!==palette&&"object"==typeof palette.color)for(var key in palette.color)document.body.setAttribute("data-md-color-"+key,palette.color[key])</script>
<script>var palette=__md_get("__palette");if(palette&&"object"==typeof palette.color)for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)</script>

View File

@ -0,0 +1,4 @@
{#-
This file was automatically generated - do not edit
-#}
<script>var el=document.querySelector("[data-md-component=outdated]"),outdated=__md_get("__outdated",sessionStorage);!0===outdated&&el&&(el.hidden=!1)</script>

View File

@ -2,17 +2,20 @@
This file was automatically generated - do not edit
-#}
{% import "partials/language.html" as lang with context %}
{% set label = lang.t("source.file.date.updated") %}
<hr>
<div class="md-source-date">
<small>
{% if page.meta.git_revision_date_localized %}
{{ label }}: {{ 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 }}
<br>
{{ lang.t("source.file.date.created") }}:
{{ page.meta.git_creation_date_localized }}
{% endif %}
{% elif page.meta.revision_date %}
{{ label }}: {{ page.meta.revision_date }}
{{ lang.t("source.file.date.updated") }}:
{{ page.meta.revision_date }}
{% endif %}
</small>
</div>

View File

@ -30,6 +30,7 @@ import { getElementOrThrow, getLocation } from "~/browser"
* Feature flag
*/
export type Flag =
| "content.code.annotate" /* Code annotations */
| "header.autohide" /* Hide header */
| "navigation.expand" /* Automatic expansion */
| "navigation.instant" /* Instant loading */
@ -38,6 +39,7 @@ export type Flag =
| "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 */
@ -76,6 +78,7 @@ export type Translations = Record<Translation, string>
*/
export interface Versioning {
provider: "mike" /* Version provider */
default?: string /* Default version */
}
/**

View File

@ -24,7 +24,8 @@ import {
NEVER,
Observable,
fromEvent,
fromEventPattern
fromEventPattern,
merge
} from "rxjs"
import {
mapTo,
@ -59,14 +60,14 @@ export function watchMedia(query: string): Observable<boolean> {
}
/**
* Watch print mode, cross-browser
* Watch print mode
*
* @returns Print mode observable
* @returns Print observable
*/
export function watchPrint(): Observable<void> {
return fromEvent(window, "beforeprint")
.pipe(
mapTo(undefined)
export function watchPrint(): Observable<boolean> {
return merge(
fromEvent(window, "beforeprint").pipe(mapTo(true)),
fromEvent(window, "afterprint").pipe(mapTo(false))
)
}

View File

@ -249,6 +249,6 @@ window.keyboard$ = keyboard$ /* Keyboard observable */
window.viewport$ = viewport$ /* Viewport observable */
window.tablet$ = tablet$ /* Tablet observable */
window.screen$ = screen$ /* Screen observable */
window.print$ = print$ /* Print mode observable */
window.print$ = print$ /* Print observable */
window.alert$ = alert$ /* Alert subject */
window.component$ = component$ /* Component observable */

View File

@ -38,6 +38,7 @@ export type ComponentType =
| "header-title" /* Header title */
| "header-topic" /* Header topic */
| "main" /* Main area */
| "outdated" /* Version warning */
| "palette" /* Color palette */
| "search" /* Search */
| "search-query" /* Search input */
@ -81,6 +82,7 @@ interface ComponentTypeMap {
"header-title": HTMLElement /* Header title */
"header-topic": HTMLElement /* Header topic */
"main": HTMLElement /* Main area */
"outdated": HTMLElement /* Version warning */
"palette": HTMLElement /* Color palette */
"search": HTMLElement /* Search */
"search-query": HTMLInputElement /* Search input */

View File

@ -53,7 +53,7 @@ export type Content =
interface MountOptions {
target$: Observable<HTMLElement> /* Location target observable */
viewport$: Observable<Viewport> /* Viewport observable */
print$: Observable<void> /* Print mode observable */
print$: Observable<boolean> /* Print observable */
}
/* ----------------------------------------------------------------------------
@ -78,7 +78,7 @@ export function mountContent(
/* Code blocks */
...getElements("pre > code", el)
.map(child => mountCodeBlock(child, { viewport$ })),
.map(child => mountCodeBlock(child, { viewport$, print$ })),
/* Data tables */
...getElements("table:not([class])", el)

View File

@ -30,23 +30,33 @@ import {
of
} from "rxjs"
import {
combineLatestWith,
distinctUntilKeyChanged,
finalize,
map,
mergeWith,
switchMap,
take,
takeWhile,
tap,
withLatestFrom
} from "rxjs/operators"
import { feature } from "~/_"
import { resetFocusable, setFocusable } from "~/actions"
import {
Viewport,
getElement,
getElementContentSize,
getElementOrThrow,
getElementSize,
getElements,
watchMedia
} from "~/browser"
import { renderClipboardButton } from "~/templates"
import {
renderAnnotation,
renderClipboardButton
} from "~/templates"
import { Component } from "../../_"
@ -59,6 +69,7 @@ import { Component } from "../../_"
*/
export interface CodeBlock {
scroll: boolean /* Code block overflows */
annotations?: HTMLElement[] /* Code block annotations */
}
/* ----------------------------------------------------------------------------
@ -70,6 +81,7 @@ export interface CodeBlock {
*/
interface WatchOptions {
viewport$: Observable<Viewport> /* Viewport observable */
print$: Observable<boolean> /* Print observable */
}
/**
@ -77,6 +89,7 @@ interface WatchOptions {
*/
interface MountOptions {
viewport$: Observable<Viewport> /* Viewport observable */
print$: Observable<boolean> /* Print observable */
}
/* ----------------------------------------------------------------------------
@ -104,7 +117,7 @@ let index = 0
* @returns Code block observable
*/
export function watchCodeBlock(
el: HTMLElement, { viewport$ }: WatchOptions
el: HTMLElement, { viewport$, print$ }: WatchOptions
): Observable<CodeBlock> {
const container$ = of(el)
.pipe(
@ -112,7 +125,7 @@ export function watchCodeBlock(
const container = child.closest("[data-tabs]")
if (container instanceof HTMLElement) {
return merge(
...getElements("input", container)
...getElements(":scope > input", container)
.map(input => fromEvent(input, "change"))
)
}
@ -120,17 +133,76 @@ export function watchCodeBlock(
})
)
/* Transform annotations */
const annotations: HTMLElement[] = []
const container =
el.closest(".highlighttable") ||
el.closest(".highlight")
if (container) {
const list = container.nextElementSibling
if (list instanceof HTMLOListElement && (
container.classList.contains("annotate") ||
feature("content.code.annotate")
)) {
const items = Array.from(list.children)
list.remove()
/* Replace comments with annotations */
for (const comment of getElements(".c, .c1, .cm", el)) {
/* Split comment at annotations */ // TODO: refactor when revisiting annotations
let match: RegExpExecArray | null
let text = comment.firstChild as Text
do {
match = /\((\d+)\)/.exec(text.textContent!)
if (match && match.index) {
const bubble = text.splitText(match.index)
text = bubble.splitText(match[0].length) // complete match length
const [, j = -1] = match
const content = items[+j - 1]
if (typeof content !== "undefined") {
const annotation = renderAnnotation(+j, content.childNodes)
bubble.replaceWith(annotation)
annotations.push(annotation)
}
}
} while (match)
}
/* Move elements back on print */ // TODO: refactor memleak (instant loading)
print$.subscribe(active => {
if (active) {
container.insertAdjacentElement("afterend", list)
for (const annotation of annotations) {
const id = parseInt(annotation.getAttribute("data-index")!, 10)
const typeset = getElement(":scope .md-typeset", annotation)!
items[id - 1].append(...Array.from(typeset.childNodes))
}
} else {
list.remove()
for (const annotation of annotations) {
const id = parseInt(annotation.getAttribute("data-index")!, 10)
const nodes = items[id - 1].childNodes
getElementOrThrow(":scope .md-typeset", annotation)
.append(...Array.from(nodes))
}
}
})
}
}
/* Check overflow on resize and tab change */
return merge(
viewport$.pipe(distinctUntilKeyChanged("size")),
container$
)
return viewport$
.pipe(
distinctUntilKeyChanged("size"),
mergeWith(container$),
map(() => {
const visible = getElementSize(el)
const content = getElementContentSize(el)
return {
scroll: content.width > visible.width
scroll: content.width > visible.width,
...annotations.length && { annotations }
}
}),
distinctUntilKeyChanged("scroll")
@ -163,10 +235,34 @@ export function mountCodeBlock(
resetFocusable(el)
})
/* Compute annotation position */
internal$
.pipe(
take(1),
takeWhile(({ annotations }) => !!annotations?.length),
map(({ annotations }) => annotations!
.map(annotation => getElementOrThrow(".md-tooltip", annotation))
),
combineLatestWith(viewport$
.pipe(
distinctUntilKeyChanged("size")
)
)
)
.subscribe(([tooltips, { size }]) => {
for (const tooltip of tooltips) {
const { x, width } = tooltip.getBoundingClientRect()
if (x + width > size.width)
tooltip.classList.add("md-tooltip--end")
else
tooltip.classList.remove("md-tooltip--end")
}
})
/* Render button for Clipboard.js integration */
if (ClipboardJS.isSupported()) {
const parent = el.closest("pre")!
parent.id = `__code_${index++}`
parent.id = `__code_${++index}`
parent.insertBefore(
renderClipboardButton(parent.id),
el

View File

@ -20,13 +20,12 @@
* IN THE SOFTWARE.
*/
import { Observable, Subject } from "rxjs"
import { Observable, Subject, merge } from "rxjs"
import {
filter,
finalize,
map,
mapTo,
mergeWith,
tap
} from "rxjs/operators"
@ -40,6 +39,7 @@ import { Component } from "../../_"
* Details
*/
export interface Details {
action: "open" | "close" /* Action */
scroll?: boolean /* Scroll into view */
}
@ -52,7 +52,7 @@ export interface Details {
*/
interface WatchOptions {
target$: Observable<HTMLElement> /* Location target observable */
print$: Observable<void> /* Print mode observable */
print$: Observable<boolean> /* Print observable */
}
/**
@ -60,7 +60,7 @@ interface WatchOptions {
*/
interface MountOptions {
target$: Observable<HTMLElement> /* Location target observable */
print$: Observable<void> /* Print mode observable */
print$: Observable<boolean> /* Print observable */
}
/* ----------------------------------------------------------------------------
@ -78,12 +78,26 @@ interface MountOptions {
export function watchDetails(
el: HTMLDetailsElement, { target$, print$ }: WatchOptions
): Observable<Details> {
return target$
let open = false
return merge(
/* Open and focus details on location target */
target$
.pipe(
map(target => target.closest("details:not([open])")!),
filter(details => el === details),
mapTo({ scroll: true }),
mergeWith(print$.pipe(mapTo({})))
mapTo<Details>({ action: "open", scroll: true })
),
/* 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)
)
)
}
@ -102,8 +116,11 @@ export function mountDetails(
el: HTMLDetailsElement, options: MountOptions
): Observable<Component<Details>> {
const internal$ = new Subject<Details>()
internal$.subscribe(({ scroll }) => {
internal$.subscribe(({ action, scroll }) => {
if (action === "open")
el.setAttribute("open", "")
else
el.removeAttribute("open")
if (scroll)
el.scrollIntoView()
})
@ -113,6 +130,6 @@ export function mountDetails(
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
mapTo({ ref: el })
map(state => ({ ref: el, ...state }))
)
}

View File

@ -75,8 +75,7 @@ export interface Palette {
export function watchPalette(
inputs: HTMLInputElement[]
): Observable<Palette> {
const data = localStorage.getItem(__prefix("__palette"))!
const current = JSON.parse(data) || {
const current = __md_get<Palette>("__palette") || {
index: inputs.findIndex(input => (
matchMedia(input.getAttribute("data-md-color-media")!).matches
))
@ -104,7 +103,7 @@ export function watchPalette(
/* Persist preference in local storage */
palette$.subscribe(palette => {
localStorage.setItem(__prefix("__palette"), JSON.stringify(palette))
__md_set("__palette", palette)
})
/* Return palette */

View File

@ -74,22 +74,14 @@ export function watchSource(
el: HTMLAnchorElement
): Observable<Source> {
return fetch$ ||= defer(() => {
const data = sessionStorage.getItem(__prefix("__source"))
if (data) {
return of<SourceFacts>(JSON.parse(data))
} else {
const value$ = fetchSourceFacts(el.href)
value$.subscribe(value => {
try {
sessionStorage.setItem(__prefix("__source"), JSON.stringify(value))
} catch (err) {
/* Uncritical, just swallow */
}
})
/* Return value */
return value$
}
const cached = __md_get<SourceFacts>("__source", sessionStorage)
if (cached)
return of(cached)
else
return fetchSourceFacts(el.href)
.pipe(
tap(facts => __md_set("__source", facts, sessionStorage))
)
})
.pipe(
catchError(() => NEVER),

View File

@ -24,7 +24,9 @@ import {
Observable,
Subject,
animationFrameScheduler,
combineLatest
combineLatest,
defer,
of
} from "rxjs"
import {
bufferCount,
@ -39,6 +41,7 @@ import {
tap
} from "rxjs/operators"
import { feature } from "~/_"
import {
resetAnchorActive,
resetAnchorState,
@ -49,6 +52,7 @@ import {
Viewport,
getElement,
getElements,
getLocation,
watchElementSize
} from "~/browser"
@ -106,15 +110,18 @@ interface MountOptions {
*
* Note that the current anchor is the last item of the `prev` anchor list.
*
* @param anchors - Anchor elements
* @param el - Table of contents element
* @param options - Options
*
* @returns Table of contents observable
*/
export function watchTableOfContents(
anchors: HTMLAnchorElement[], { viewport$, header$ }: WatchOptions
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 = getElement(`[id="${id}"]`)
@ -134,9 +141,9 @@ export function watchTableOfContents(
distinctUntilKeyChanged("height"),
/* Build index to map anchor paths to vertical offsets */
map(() => {
switchMap(body => defer(() => {
let path: HTMLAnchorElement[] = []
return [...table].reduce((index, [anchor, target]) => {
return of([...table].reduce((index, [anchor, target]) => {
while (path.length) {
const last = table.get(path[path.length - 1])!
if (last.tagName >= target.tagName) {
@ -158,21 +165,23 @@ export function watchTableOfContents(
[...path = [...path, anchor]].reverse(),
offset
)
}, new Map<HTMLAnchorElement[], number>())
}),
}, 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))),
/* Re-compute partition when viewport offset changes */
switchMap(index => combineLatest([adjust$, viewport$])
switchMap(index => combineLatest([viewport$, adjust$])
.pipe(
scan(([prev, next], [adjust, { offset: { y } }]) => {
scan(([prev, next], [{ offset: { y }, size }, adjust]) => {
const last = y + size.height >= Math.floor(body.height)
/* Look forward */
while (next.length) {
const [, offset] = next[0]
if (offset - adjust < y) {
if (offset - adjust < y || last) {
prev = [...prev, next.shift()!]
} else {
break
@ -182,7 +191,7 @@ export function watchTableOfContents(
/* Look backward */
while (prev.length) {
const [, offset] = prev[prev.length - 1]
if (offset - adjust >= y) {
if (offset - adjust >= y && !last) {
next = [prev.pop()!, ...next]
} else {
break
@ -199,6 +208,8 @@ export function watchTableOfContents(
)
)
)
)
)
/* Compute and return anchor list migrations */
return partition$
@ -236,7 +247,7 @@ export function watchTableOfContents(
/**
* Mount table of contents
*
* @param el - Anchor list element
* @param el - Table of contents element
* @param options - Options
*
* @returns Table of contents component observable
@ -262,11 +273,31 @@ export function mountTableOfContents(
setAnchorActive(anchor, index === prev.length - 1)
setAnchorState(anchor, "blur")
}
/* Set up anchor tracking, if enabled */
if (feature("navigation.tracking")) {
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 */
const anchors = getElements<HTMLAnchorElement>("[href^=\\#]", el)
return watchTableOfContents(anchors, options)
return watchTableOfContents(el, options)
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),

View File

@ -24,6 +24,10 @@ import ClipboardJS from "clipboard"
import { Observable, Subject } from "rxjs"
import { translation } from "~/_"
import {
getElementOrThrow,
getElements
} from "~/browser"
/* ----------------------------------------------------------------------------
* Helper types
@ -36,6 +40,34 @@ interface SetupOptions {
alert$: Subject<string> /* Alert subject */
}
/* ----------------------------------------------------------------------------
* Helper functions
* ------------------------------------------------------------------------- */
/**
* Extract text to copy
*
* This function hides annotations prior to extracting the text from the given
* code block, so they're not included in the text that is copied to clipboard.
*
* @param el - HTML element
*
* @returns Extracted text
*/
function extract(el: HTMLElement): string {
const annotations = getElements(".md-annotation", el)
for (const annotation of annotations)
annotation.hidden = true
/* Extract text and show annotations */
const text = el.innerText
for (const annotation of annotations)
annotation.hidden = false
/* Return extracted text */
return text
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
@ -50,7 +82,14 @@ export function setupClipboardJS(
): void {
if (ClipboardJS.isSupported()) {
new Observable<ClipboardJS.Event>(subscriber => {
new ClipboardJS("[data-clipboard-target], [data-clipboard-text]")
new ClipboardJS("[data-clipboard-target], [data-clipboard-text]", {
text: el => (
el.getAttribute("data-clipboard-text")! ||
extract(getElementOrThrow(
el.getAttribute("data-clipboard-target")!
))
)
})
.on("success", ev => subscriber.next(ev))
})
.subscribe(() => alert$.next(translation("clipboard.copied")))

View File

@ -0,0 +1,5 @@
{
"rules": {
"no-null/no-null": "off"
}
}

View File

@ -20,9 +20,19 @@
* IN THE SOFTWARE.
*/
import { combineLatest } from "rxjs"
import { map } from "rxjs/operators"
import { configuration } from "~/_"
import { getElementOrThrow, requestJSON } from "~/browser"
import { Version, renderVersionSelector } from "~/templates"
import {
getElementOrThrow,
requestJSON,
} from "~/browser"
import { getComponentElements } from "~/components"
import {
Version,
renderVersionSelector
} from "~/templates"
/* ----------------------------------------------------------------------------
* Functions
@ -33,9 +43,37 @@ import { Version, renderVersionSelector } from "~/templates"
*/
export function setupVersionSelector(): void {
const config = configuration()
requestJSON<Version[]>(new URL("../versions.json", config.base))
.subscribe(versions => {
const versions$ = requestJSON<Version[]>(
new URL("../versions.json", config.base)
)
/* 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]
})
)
/* Render version selector and warning */
combineLatest([versions$, current$])
.subscribe(([versions, current]) => {
const topic = getElementOrThrow(".md-header__topic")
topic.appendChild(renderVersionSelector(versions))
topic.appendChild(renderVersionSelector(versions, current))
/* Check if version state was already determined */
if (__md_get("__outdated", sessionStorage) === null) {
const latest = config.version?.default || "latest"
const outdated = !current.aliases.includes(latest)
/* Persist version state in session storage */
__md_set("__outdated", outdated, sessionStorage)
if (outdated)
for (const warning of getComponentElements("outdated"))
warning.hidden = false
}
})
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (c) 2016-2021 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION 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 'copy-to-clipboard' button
*
* @param id - Unique identifier
* @param content - Annotation content
*
* @returns Element
*/
export function renderAnnotation(
id: number, content: NodeListOf<ChildNode>
): HTMLElement {
return (
<aside class="md-annotation" data-index={id} tabIndex={0}>
<div class="md-tooltip">
<div class="md-tooltip__inner md-typeset">
{...Array.from(content)}
</div>
</div>
<span class="md-annotation__index">{id}</span>
</aside>
)
}

View File

@ -21,6 +21,7 @@
*/
export * from "./clipboard"
export * from "./code"
export * from "./search"
export * from "./source"
export * from "./table"

View File

@ -69,20 +69,13 @@ function renderVersion(version: Version): HTMLElement {
* Render a version selector
*
* @param versions - Versions
* @param active - Active version
*
* @returns Element
*/
export function renderVersionSelector(versions: Version[]): HTMLElement {
const config = configuration()
/* Determine active version */
const [, current] = config.base.match(/([^/]+)\/?$/)!
const active =
versions.find(({ version, aliases }) => (
version === current || aliases.includes(current)
)) || versions[0]
/* Render version selector */
export function renderVersionSelector(
versions: Version[], active: Version
): HTMLElement {
return (
<div class="md-version">
<button

View File

@ -42,7 +42,7 @@
@import "main/typeset";
@import "main/layout/base";
@import "main/layout/announce";
@import "main/layout/banner";
@import "main/layout/clipboard";
@import "main/layout/content";
@import "main/layout/dialog";
@ -55,6 +55,7 @@
@import "main/layout/sidebar";
@import "main/layout/source";
@import "main/layout/tabs";
@import "main/layout/tooltip";
@import "main/layout/top";
@import "main/layout/version";

View File

@ -62,9 +62,9 @@ $admonitions: (
// Admonition
.admonition {
display: flow-root;
margin: px2em(20px, 12.8px) 0;
padding: 0 px2rem(12px);
overflow: hidden;
color: var(--md-admonition-fg-color);
font-size: px2rem(12.8px);
page-break-inside: avoid;
@ -121,6 +121,7 @@ $admonitions: (
font-weight: 700;
background-color: color.adjust($clr-blue-a200, $alpha: -0.9);
border-left: px2rem(4px) solid $clr-blue-a200;
border-top-left-radius: px2rem(2px);
// Adjust for right-to-left languages
[dir="rtl"] & {

View File

@ -60,12 +60,6 @@
border-radius: px2rem(2px);
}
}
// Hack: omit margin collapse
&::after {
display: table;
content: "";
}
}
// Details title

View File

@ -173,6 +173,9 @@
[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;
margin-right: px2em(16px, 13.6px);
margin-left: px2em(-16px, 13.6px);
@ -192,7 +195,6 @@
// Code block with line numbers
.highlighttable {
display: flow-root;
overflow: hidden;
// Set table elements to block layout, because otherwise the whole flexbox
// hacking won't work correctly
@ -246,7 +248,7 @@
// Code block container - stretch to remaining space
.code {
flex: 1;
overflow: hidden;
min-width: 0;
}
}

View File

@ -40,11 +40,11 @@
order: initial;
}
// Code block is the only child of a tab - remove margin and mirror
// Code block is the first child of a tab - remove margin and mirror
// previous (now deprecated) SuperFences code block grouping behavior
> pre:only-child,
> .highlight:only-child pre,
> .highlighttable:only-child {
> pre:first-child,
> .highlight:first-child pre,
> .highlighttable:first-child {
margin: 0;
// Omit rounded borders
@ -114,6 +114,12 @@
cursor: pointer;
transition: color 250ms;
// Hack: omit flickering of content tabs label on initial page load when
// using linked content tabs.
.no-js & {
transition: none;
}
// Tab label on hover
&:hover {
color: var(--md-accent-fg-color);
@ -276,11 +282,11 @@
}
}
// Code block is the only child of a tab - remove margin and mirror
// Code block is the first child of a tab - remove margin and mirror
// previous (now deprecated) SuperFences code block grouping behavior
> pre:only-child,
> .highlight:only-child pre,
> .highlighttable:only-child {
> pre:first-child,
> .highlight:first-child pre,
> .highlighttable:first-child {
margin: 0;
// Omit rounded borders

View File

@ -24,21 +24,27 @@
// Rules
// ----------------------------------------------------------------------------
// Announcement bar
.md-announce {
// Banner for announcements and warnings
.md-banner {
overflow: auto;
color: var(--md-footer-fg-color);
background-color: var(--md-footer-bg-color);
// [print]: Hide announcement bar
// [print]: Hide banner
@media print {
display: none;
}
// Announcement wrapper
// Banner with warning
&--warning {
color: var(--md-default-fg-color);
background: var(--md-typeset-mark-color);
}
// Banner wrapper
&__inner {
margin: px2rem(12px) auto;
padding: 0 px2rem(16px);
color: var(--md-footer-fg-color);
font-size: px2rem(14px);
}
}

View File

@ -27,13 +27,10 @@
// Content area
.md-content {
flex-grow: 1;
// Hack: we must use `overflow: hidden`, 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 break everything. This, however, induces
// margin collapse, which will break scroll margins. Adding a large enough
// scroll padding seems to do the trick, at least in Chrome and Firefox.
overflow: hidden;
scroll-padding-top: px2rem(1024px);
// 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 {

View File

@ -32,7 +32,7 @@
right: px2rem(16px);
bottom: px2rem(16px);
left: initial;
z-index: 3;
z-index: 4;
min-width: px2rem(222px);
padding: px2rem(8px) px2rem(12px);
background-color: var(--md-default-fg-color);

View File

@ -31,7 +31,7 @@
top: 0;
right: 0;
left: 0;
z-index: 3;
z-index: 4;
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

View File

@ -46,7 +46,7 @@
position: fixed;
top: 0;
left: px2rem(-242px);
z-index: 4;
z-index: 5;
display: block;
width: px2rem(242px);
height: 100%;
@ -163,11 +163,11 @@
// [tablet -]: Show overlay on active drawer
@include break-to-device(tablet) {
// Sidebar overlay
// Drawer overlay
.md-overlay {
position: fixed;
top: 0;
z-index: 4;
z-index: 5;
width: 0;
height: 0;
background-color: hsla(0, 0%, 0%, 0.54);

View File

@ -0,0 +1,208 @@
////
/// Copyright (c) 2016-2021 Martin Donath <martin.donath@squidfunk.com>
///
/// Permission is hereby granted, free of charge, to any person obtaining a
/// copy of this software and associated documentation files (the "Software"),
/// to deal in the Software without restriction, including without limitation
/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
/// and/or sell copies of the Software, and to permit persons to whom the
/// Software is furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER 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% {
box-shadow: 0 0 0 0 var(--md-default-fg-color--lightest);
}
75% {
box-shadow: 0 0 0 px2em(10px) transparent;
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
// ----------------------------------------------------------------------------
// Rules
// ----------------------------------------------------------------------------
// Tooltip
.md-tooltip {
position: absolute;
// 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;
max-height: 0;
overflow: auto;
color: var(--md-default-fg-color);
background-color: var(--md-default-bg-color);
border-radius: px2rem(2px);
box-shadow:
0 px2rem(4px) px2rem(10px) hsla(0, 0%, 0%, 0.1),
0 0 px2rem(1px) hsla(0, 0%, 0%, 0.25);
transform: translateY(px2rem(8px));
// Hack: promote to own layer to reduce jitter
backface-visibility: hidden;
opacity: 0;
transition:
transform 250ms 375ms,
opacity 250ms,
max-height 0ms 250ms,
z-index 250ms;
// Disable animation for motion reduction preference
@media (prefers-reduced-motion) {
transition: none;
}
// Tooltip wrapper
&__inner {
padding: px2rem(16px);
font-size: px2rem(12.8px);
// Adjust spacing on first child
> :first-child {
margin-top: 0;
}
// Adjust spacing on last child
> :last-child {
margin-bottom: 0;
}
}
// Tooltip on parent focus
:focus > &,
:focus-within > & {
max-height: 1000%;
transform: translateY(0);
opacity: 1;
transition:
transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1),
opacity 250ms,
max-height 250ms 0ms,
z-index 0ms;
// Disable animation for motion reduction preference
@media (prefers-reduced-motion) {
transition: none;
}
// Modifier for end alignment
&--end {
transform: translate(-100%, 0);
}
// Modifier for center alignment
&--center {
transform: translate(-50%, 0);
}
}
// Show outline for keyboard devices
.focus-visible > & {
outline: var(--md-accent-fg-color) auto;
}
// Modifier for end alignment
&--end {
transform: translate(-100%, px2rem(8px));
}
// Modifier for center alignment
&--center {
transform: translate(-50%, px2rem(8px));
}
}
// ----------------------------------------------------------------------------
// Annotation
.md-annotation {
white-space: initial;
outline: none;
// Promote children to top on focus
&:focus-within > * {
z-index: 2;
}
// Annotation is visible
&:not([hidden]) {
display: inline-block;
}
// Annotation index
&__index {
position: relative;
z-index: 0;
display: inline-block;
min-width: 1.4em;
padding: 0 px2em(6px);
color: var(--md-accent-bg-color);
text-align: center;
background-color: var(--md-default-fg-color--lighter);
border-radius: px2em(20px);
cursor: pointer;
transition:
background-color 250ms,
z-index 250ms;
animation: pulse 2000ms infinite;
user-select: none;
// Disable animation for motion reduction preference
@media (prefers-reduced-motion) {
transition: none;
animation: none;
}
// Annotation index on focus
:focus-within > & {
transition:
background-color 250ms,
z-index 0ms;
animation: none;
// Disable animation for motion reduction preference
@media (prefers-reduced-motion) {
transition: none;
}
}
// Annotation index on focus/hover
:focus-within > &,
:hover > & {
background-color: var(--md-accent-fg-color);
}
}
// Annotation tooltip
.md-tooltip {
min-width: px2rem(320px);
max-width: 60%;
margin: px2em(-16px, 13.6px) px2em(10px, 13.6px) 0;
font-family: var(--md-text-font-family);
// Modifier for center alignment
&--center {
margin-top: px2em(10px, 13.6px);
}
}
}

View File

@ -125,6 +125,9 @@
<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" %}
@ -156,7 +159,6 @@
<!-- Retrieve features from configuration -->
{% set features = config.theme.features or [] %}
{% include "partials/javascripts/base.html" %}
<!-- User preference: color palette -->
{% if not config.theme.palette is mapping %}
@ -206,6 +208,20 @@
{% endif %}
</div>
<!-- Version warning -->
{% if config.extra.version %}
<div data-md-component="outdated" hidden>
<aside class="md-banner md-banner--warning">
{% if self.outdated() %}
<div class="md-banner__inner md-grid md-typeset">
{% block outdated %}{% endblock %}
</div>
{% include "partials/javascripts/outdated.html" %}
{% endif %}
</aside>
</div>
{% endif %}
<!-- Header -->
{% block header %}
{% include "partials/header.html" %}
@ -303,13 +319,12 @@
{{ page.content }}
<!-- Last update of source file -->
{% if page and page.meta %}
{% if page.meta.git_revision_date_localized or
{% if page and page.meta and (
page.meta.git_revision_date_localized or
page.meta.revision_date
%}
) %}
{% include "partials/source-file.html" %}
{% endif %}
{% endif %}
{% endblock %}
<!-- Disqus integration -->

View File

@ -38,7 +38,7 @@
@import "main/typeset";
@import "main/layout/announce";
@import "main/layout/banner";
@import "main/layout/hero";
@import "main/layout/iconsearch";
@import "main/layout/sponsorship";

View File

@ -24,8 +24,8 @@
// Rules
// ----------------------------------------------------------------------------
// Announcement bar
.md-announce {
// Banner for announcements and warnings
.md-banner {
// Text link, also on focus/hover
a,

View File

@ -27,13 +27,26 @@
<script>
/* Prepend the base path to the given key to ensure uniqueness */
function __prefix(key) {
var prefix = new URL("{{ base_url }}", location)
function __md_scope(key, storage, base) {
var prefix = new URL(base || (
storage === localStorage
? "{{ config.extra.scope | d(base_url) }}"
: "{{ base_url }}"
), location)
return prefix.pathname + "." + key
}
/* Fetch the given key from the given storage */
function __get(key, storage = localStorage) {
return JSON.parse(storage.getItem(__prefix(key)))
/* Fetch the value for a key from the given storage */
function __md_get(key, storage = localStorage, base) {
return JSON.parse(storage.getItem(__md_scope(key, storage, base)))
}
/* Persist a key-value pair in the given storage */
function __md_set(key, value, storage = localStorage, base) {
try {
storage.setItem(__md_scope(key, storage, base), JSON.stringify(value))
} catch (err) {
/* Uncritical, just swallow */
}
}
</script>

View File

@ -22,8 +22,8 @@
<!-- User preference: color palette -->
<script>
var palette = __get("__palette")
if (palette !== null && typeof palette.color === "object")
for (var key in palette.color)
document.body.setAttribute("data-md-color-" + key, palette.color[key])
var palette = __md_get("__palette")
if (palette && typeof palette.color === "object")
for (var [key, value] of Object.entries(palette.color))
document.body.setAttribute("data-md-color-" + key, value)
</script>

View File

@ -0,0 +1,29 @@
<!--
Copyright (c) 2016-2021 Martin Donath <martin.donath@squidfunk.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION 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>

View File

@ -23,22 +23,24 @@
{% import "partials/language.html" as lang with context %}
<!-- Last updated date -->
{% set label = lang.t("source.file.date.updated") %}
<hr />
<div class="md-source-date">
<small>
<!-- mkdocs-git-revision-date-localized-plugin -->
{% if page.meta.git_revision_date_localized %}
{{ label }}: {{ 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 }}
<br />
{{ lang.t("source.file.date.created") }}:
{{ page.meta.git_creation_date_localized }}
{% endif %}
<!-- mkdocs-git-revision-date-plugin -->
{% elif page.meta.revision_date %}
{{ label }}: {{ page.meta.revision_date }}
{{ lang.t("source.file.date.updated") }}:
{{ page.meta.revision_date }}
{% endif %}
</small>
</div>

40
typings/_/index.d.ts vendored
View File

@ -52,13 +52,47 @@ declare global {
const __search: GlobalSearchConfig | undefined
/**
* Global function to prefix storage items
* Fetch the value for a key from the given storage
*
* This function is defined in `partials/javascripts/base.html`, so it can be
* used from the templates, as well as from the application bundle.
*
* @template T - Data type
*
* @param key - Key
* @param storage - Storage (default: local storage)
* @param base - Base URL (default: current base)
*
* @return Value or nothing
*/
function __prefix(key: string): string
function __md_get<T>(
key: string, storage?: Storage, base?: string
): T | null
/**
* Persist a key-value pair in the given storage
*
* This function is defined in `partials/javascripts/base.html`, so it can be
* used from the templates, as well as from the application bundle.
*
* @template T - Data type
*
* @param key - Key
* @param value - Value
* @param storage - Storage (default: local storage)
* @param base - Base URL (default: current base)
*/
function __md_set<T>(
key: string, value: T, storage?: Storage, base?: string
): void
}
/* ------------------------------------------------------------------------- */
/**
* Google Analytics
*/
declare global {
function ga(...args: string[]): void
}
@ -74,7 +108,7 @@ declare global {
var viewport$: Observable<Viewport> /* Viewport obsevable */
var tablet$: Observable<boolean> /* Tablet breakpoint observable */
var screen$: Observable<boolean> /* Screen breakpoint observable */
var print$: Observable<void> /* Print mode observable */
var print$: Observable<boolean> /* Print observable */
var alert$: Subject<string> /* Alert subject */
var component$: Observable<Component>/* Component observable */
}