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] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false
# Makefiles # Python
[*.py] [*.py]
indent_style = space
indent_size = 4 indent_size = 4
# Makefiles
[Makefile]
indent_style = tab
indent_size = 8

1
.gitignore vendored
View File

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

View File

@ -80,6 +80,7 @@
"selector-class-pattern": null, "selector-class-pattern": null,
"selector-combinator-space-before": null, "selector-combinator-space-before": null,
"selector-descendant-combinator-no-non-space": null, "selector-descendant-combinator-no-non-space": null,
"selector-id-pattern": null,
"selector-max-empty-lines": 0, "selector-max-empty-lines": 0,
"selector-max-id": 0, "selector-max-id": 0,
"selector-no-qualifying-type": null, "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 %} {% endif %}
{% endblock %} {% endblock %}
{% block styles %} {% 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 %} {% if config.theme.palette %}
{% set palette = config.theme.palette %} {% set palette = config.theme.palette %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.9204c3b2.min.css' | url }}"> <link rel="stylesheet" href="{{ 'assets/stylesheets/palette.9204c3b2.min.css' | url }}">
@ -62,6 +62,7 @@
{% for path in config["extra_css"] %} {% for path in config["extra_css"] %}
<link rel="stylesheet" href="{{ path | url }}"> <link rel="stylesheet" href="{{ path | url }}">
{% endfor %} {% endfor %}
{% include "partials/javascripts/base.html" %}
{% block analytics %} {% block analytics %}
{% include "partials/integrations/analytics.html" %} {% include "partials/integrations/analytics.html" %}
{% endblock %} {% endblock %}
@ -81,7 +82,6 @@
<body dir="{{ direction }}"> <body dir="{{ direction }}">
{% endif %} {% endif %}
{% set features = config.theme.features or [] %} {% set features = config.theme.features or [] %}
{% include "partials/javascripts/base.html" %}
{% if not config.theme.palette is mapping %} {% if not config.theme.palette is mapping %}
{% include "partials/javascripts/palette.html" %} {% include "partials/javascripts/palette.html" %}
{% endif %} {% endif %}
@ -105,6 +105,18 @@
</aside> </aside>
{% endif %} {% endif %}
</div> </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 %} {% block header %}
{% include "partials/header.html" %} {% include "partials/header.html" %}
{% endblock %} {% endblock %}
@ -157,13 +169,12 @@
<h1>{{ page.title | d(config.site_name, true)}}</h1> <h1>{{ page.title | d(config.site_name, true)}}</h1>
{% endif %} {% endif %}
{{ page.content }} {{ page.content }}
{% if page and page.meta %} {% if page and page.meta and (
{% if page.meta.git_revision_date_localized or page.meta.git_revision_date_localized or
page.meta.revision_date page.meta.revision_date
%} ) %}
{% include "partials/source-file.html" %} {% include "partials/source-file.html" %}
{% endif %} {% endif %}
{% endif %}
{% endblock %} {% endblock %}
{% block disqus %} {% block disqus %}
{% include "partials/integrations/disqus.html" %} {% include "partials/integrations/disqus.html" %}
@ -217,7 +228,7 @@
</script> </script>
{% endblock %} {% endblock %}
{% block scripts %} {% 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"] %} {% for path in config["extra_javascript"] %}
<script src="{{ path | url }}"></script> <script src="{{ path | url }}"></script>
{% endfor %} {% 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" %} {% extends "base.html" %}
{% block extrahead %} {% 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 %} {% endblock %}
{% block announce %} {% block announce %}
<a href="https://twitter.com/squidfunk"> <a href="https://twitter.com/squidfunk">
@ -16,5 +16,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script src="{{ 'overrides/assets/javascripts/bundle.525231ca.min.js' | url }}"></script> <script src="{{ 'overrides/assets/javascripts/bundle.35fbbc46.min.js' | url }}"></script>
{% endblock %} {% endblock %}

View File

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

View File

@ -30,6 +30,7 @@ import { getElementOrThrow, getLocation } from "~/browser"
* Feature flag * Feature flag
*/ */
export type Flag = export type Flag =
| "content.code.annotate" /* Code annotations */
| "header.autohide" /* Hide header */ | "header.autohide" /* Hide header */
| "navigation.expand" /* Automatic expansion */ | "navigation.expand" /* Automatic expansion */
| "navigation.instant" /* Instant loading */ | "navigation.instant" /* Instant loading */
@ -38,6 +39,7 @@ export type Flag =
| "navigation.tabs" /* Tabs navigation */ | "navigation.tabs" /* Tabs navigation */
| "navigation.tabs.sticky" /* Tabs navigation (sticky) */ | "navigation.tabs.sticky" /* Tabs navigation (sticky) */
| "navigation.top" /* Back-to-top button */ | "navigation.top" /* Back-to-top button */
| "navigation.tracking" /* Anchor tracking */
| "search.highlight" /* Search highlighting */ | "search.highlight" /* Search highlighting */
| "search.share" /* Search sharing */ | "search.share" /* Search sharing */
| "search.suggest" /* Search suggestions */ | "search.suggest" /* Search suggestions */
@ -76,6 +78,7 @@ export type Translations = Record<Translation, string>
*/ */
export interface Versioning { export interface Versioning {
provider: "mike" /* Version provider */ provider: "mike" /* Version provider */
default?: string /* Default version */
} }
/** /**

View File

@ -24,7 +24,8 @@ import {
NEVER, NEVER,
Observable, Observable,
fromEvent, fromEvent,
fromEventPattern fromEventPattern,
merge
} from "rxjs" } from "rxjs"
import { import {
mapTo, 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> { export function watchPrint(): Observable<boolean> {
return fromEvent(window, "beforeprint") return merge(
.pipe( fromEvent(window, "beforeprint").pipe(mapTo(true)),
mapTo(undefined) fromEvent(window, "afterprint").pipe(mapTo(false))
) )
} }

View File

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

View File

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

View File

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

View File

@ -30,23 +30,33 @@ import {
of of
} from "rxjs" } from "rxjs"
import { import {
combineLatestWith,
distinctUntilKeyChanged, distinctUntilKeyChanged,
finalize, finalize,
map, map,
mergeWith,
switchMap, switchMap,
take,
takeWhile,
tap, tap,
withLatestFrom withLatestFrom
} from "rxjs/operators" } from "rxjs/operators"
import { feature } from "~/_"
import { resetFocusable, setFocusable } from "~/actions" import { resetFocusable, setFocusable } from "~/actions"
import { import {
Viewport, Viewport,
getElement,
getElementContentSize, getElementContentSize,
getElementOrThrow,
getElementSize, getElementSize,
getElements, getElements,
watchMedia watchMedia
} from "~/browser" } from "~/browser"
import { renderClipboardButton } from "~/templates" import {
renderAnnotation,
renderClipboardButton
} from "~/templates"
import { Component } from "../../_" import { Component } from "../../_"
@ -59,6 +69,7 @@ import { Component } from "../../_"
*/ */
export interface CodeBlock { export interface CodeBlock {
scroll: boolean /* Code block overflows */ scroll: boolean /* Code block overflows */
annotations?: HTMLElement[] /* Code block annotations */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -70,6 +81,7 @@ export interface CodeBlock {
*/ */
interface WatchOptions { interface WatchOptions {
viewport$: Observable<Viewport> /* Viewport observable */ viewport$: Observable<Viewport> /* Viewport observable */
print$: Observable<boolean> /* Print observable */
} }
/** /**
@ -77,6 +89,7 @@ interface WatchOptions {
*/ */
interface MountOptions { interface MountOptions {
viewport$: Observable<Viewport> /* Viewport observable */ viewport$: Observable<Viewport> /* Viewport observable */
print$: Observable<boolean> /* Print observable */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -104,7 +117,7 @@ let index = 0
* @returns Code block observable * @returns Code block observable
*/ */
export function watchCodeBlock( export function watchCodeBlock(
el: HTMLElement, { viewport$ }: WatchOptions el: HTMLElement, { viewport$, print$ }: WatchOptions
): Observable<CodeBlock> { ): Observable<CodeBlock> {
const container$ = of(el) const container$ = of(el)
.pipe( .pipe(
@ -112,7 +125,7 @@ export function watchCodeBlock(
const container = child.closest("[data-tabs]") const container = child.closest("[data-tabs]")
if (container instanceof HTMLElement) { if (container instanceof HTMLElement) {
return merge( return merge(
...getElements("input", container) ...getElements(":scope > input", container)
.map(input => fromEvent(input, "change")) .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 */ /* Check overflow on resize and tab change */
return merge( return viewport$
viewport$.pipe(distinctUntilKeyChanged("size")),
container$
)
.pipe( .pipe(
distinctUntilKeyChanged("size"),
mergeWith(container$),
map(() => { map(() => {
const visible = getElementSize(el) const visible = getElementSize(el)
const content = getElementContentSize(el) const content = getElementContentSize(el)
return { return {
scroll: content.width > visible.width scroll: content.width > visible.width,
...annotations.length && { annotations }
} }
}), }),
distinctUntilKeyChanged("scroll") distinctUntilKeyChanged("scroll")
@ -163,10 +235,34 @@ export function mountCodeBlock(
resetFocusable(el) 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 */ /* Render button for Clipboard.js integration */
if (ClipboardJS.isSupported()) { if (ClipboardJS.isSupported()) {
const parent = el.closest("pre")! const parent = el.closest("pre")!
parent.id = `__code_${index++}` parent.id = `__code_${++index}`
parent.insertBefore( parent.insertBefore(
renderClipboardButton(parent.id), renderClipboardButton(parent.id),
el el

View File

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

View File

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

View File

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

View File

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

View File

@ -24,6 +24,10 @@ import ClipboardJS from "clipboard"
import { Observable, Subject } from "rxjs" import { Observable, Subject } from "rxjs"
import { translation } from "~/_" import { translation } from "~/_"
import {
getElementOrThrow,
getElements
} from "~/browser"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Helper types * Helper types
@ -36,6 +40,34 @@ interface SetupOptions {
alert$: Subject<string> /* Alert subject */ 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 * Functions
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
@ -50,7 +82,14 @@ export function setupClipboardJS(
): void { ): void {
if (ClipboardJS.isSupported()) { if (ClipboardJS.isSupported()) {
new Observable<ClipboardJS.Event>(subscriber => { 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)) .on("success", ev => subscriber.next(ev))
}) })
.subscribe(() => alert$.next(translation("clipboard.copied"))) .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. * IN THE SOFTWARE.
*/ */
import { combineLatest } from "rxjs"
import { map } from "rxjs/operators"
import { configuration } from "~/_" import { configuration } from "~/_"
import { getElementOrThrow, requestJSON } from "~/browser" import {
import { Version, renderVersionSelector } from "~/templates" getElementOrThrow,
requestJSON,
} from "~/browser"
import { getComponentElements } from "~/components"
import {
Version,
renderVersionSelector
} from "~/templates"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * Functions
@ -33,9 +43,37 @@ import { Version, renderVersionSelector } from "~/templates"
*/ */
export function setupVersionSelector(): void { export function setupVersionSelector(): void {
const config = configuration() const config = configuration()
requestJSON<Version[]>(new URL("../versions.json", config.base)) const versions$ = requestJSON<Version[]>(
.subscribe(versions => { 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") 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 "./clipboard"
export * from "./code"
export * from "./search" export * from "./search"
export * from "./source" export * from "./source"
export * from "./table" export * from "./table"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -40,11 +40,11 @@
order: initial; 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 // previous (now deprecated) SuperFences code block grouping behavior
> pre:only-child, > pre:first-child,
> .highlight:only-child pre, > .highlight:first-child pre,
> .highlighttable:only-child { > .highlighttable:first-child {
margin: 0; margin: 0;
// Omit rounded borders // Omit rounded borders
@ -114,6 +114,12 @@
cursor: pointer; cursor: pointer;
transition: color 250ms; 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 // Tab label on hover
&:hover { &:hover {
color: var(--md-accent-fg-color); 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 // previous (now deprecated) SuperFences code block grouping behavior
> pre:only-child, > pre:first-child,
> .highlight:only-child pre, > .highlight:first-child pre,
> .highlighttable:only-child { > .highlighttable:first-child {
margin: 0; margin: 0;
// Omit rounded borders // Omit rounded borders

View File

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

View File

@ -27,13 +27,10 @@
// Content area // Content area
.md-content { .md-content {
flex-grow: 1; flex-grow: 1;
// Hack: we must use `overflow: hidden`, so the content area is capped by // Hack: we must use `min-width: 0`, so the content area is capped by the
// the dimensions of its parent. Otherwise, long code blocks might lead to // dimensions of its parent. Otherwise, long code blocks might lead to a
// a wider content area which will break everything. This, however, induces // wider content area which will overflow. See https://bit.ly/3bP3f8k
// margin collapse, which will break scroll margins. Adding a large enough min-width: 0;
// scroll padding seems to do the trick, at least in Chrome and Firefox.
overflow: hidden;
scroll-padding-top: px2rem(1024px);
// Content wrapper // Content wrapper
&__inner { &__inner {

View File

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

View File

@ -31,7 +31,7 @@
top: 0; top: 0;
right: 0; right: 0;
left: 0; left: 0;
z-index: 3; z-index: 4;
color: var(--md-primary-bg-color); color: var(--md-primary-bg-color);
background-color: var(--md-primary-fg-color); background-color: var(--md-primary-fg-color);
// Hack: reduce jitter by adding a transparent box shadow of the same size // Hack: reduce jitter by adding a transparent box shadow of the same size

View File

@ -46,7 +46,7 @@
position: fixed; position: fixed;
top: 0; top: 0;
left: px2rem(-242px); left: px2rem(-242px);
z-index: 4; z-index: 5;
display: block; display: block;
width: px2rem(242px); width: px2rem(242px);
height: 100%; height: 100%;
@ -163,11 +163,11 @@
// [tablet -]: Show overlay on active drawer // [tablet -]: Show overlay on active drawer
@include break-to-device(tablet) { @include break-to-device(tablet) {
// Sidebar overlay // Drawer overlay
.md-overlay { .md-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
z-index: 4; z-index: 5;
width: 0; width: 0;
height: 0; height: 0;
background-color: hsla(0, 0%, 0%, 0.54); 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 }}" /> <link rel="stylesheet" href="{{ path | url }}" />
{% endfor %} {% endfor %}
<!-- Helper functions for inline scripts -->
{% include "partials/javascripts/base.html" %}
<!-- Analytics --> <!-- Analytics -->
{% block analytics %} {% block analytics %}
{% include "partials/integrations/analytics.html" %} {% include "partials/integrations/analytics.html" %}
@ -156,7 +159,6 @@
<!-- Retrieve features from configuration --> <!-- Retrieve features from configuration -->
{% set features = config.theme.features or [] %} {% set features = config.theme.features or [] %}
{% include "partials/javascripts/base.html" %}
<!-- User preference: color palette --> <!-- User preference: color palette -->
{% if not config.theme.palette is mapping %} {% if not config.theme.palette is mapping %}
@ -206,6 +208,20 @@
{% endif %} {% endif %}
</div> </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 --> <!-- Header -->
{% block header %} {% block header %}
{% include "partials/header.html" %} {% include "partials/header.html" %}
@ -303,13 +319,12 @@
{{ page.content }} {{ page.content }}
<!-- Last update of source file --> <!-- Last update of source file -->
{% if page and page.meta %} {% if page and page.meta and (
{% if page.meta.git_revision_date_localized or page.meta.git_revision_date_localized or
page.meta.revision_date page.meta.revision_date
%} ) %}
{% include "partials/source-file.html" %} {% include "partials/source-file.html" %}
{% endif %} {% endif %}
{% endif %}
{% endblock %} {% endblock %}
<!-- Disqus integration --> <!-- Disqus integration -->

View File

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

View File

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

View File

@ -27,13 +27,26 @@
<script> <script>
/* Prepend the base path to the given key to ensure uniqueness */ /* Prepend the base path to the given key to ensure uniqueness */
function __prefix(key) { function __md_scope(key, storage, base) {
var prefix = new URL("{{ base_url }}", location) var prefix = new URL(base || (
storage === localStorage
? "{{ config.extra.scope | d(base_url) }}"
: "{{ base_url }}"
), location)
return prefix.pathname + "." + key return prefix.pathname + "." + key
} }
/* Fetch the given key from the given storage */ /* Fetch the value for a key from the given storage */
function __get(key, storage = localStorage) { function __md_get(key, storage = localStorage, base) {
return JSON.parse(storage.getItem(__prefix(key))) 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> </script>

View File

@ -22,8 +22,8 @@
<!-- User preference: color palette --> <!-- User preference: color palette -->
<script> <script>
var palette = __get("__palette") var palette = __md_get("__palette")
if (palette !== null && typeof palette.color === "object") if (palette && typeof palette.color === "object")
for (var key in palette.color) for (var [key, value] of Object.entries(palette.color))
document.body.setAttribute("data-md-color-" + key, palette.color[key]) document.body.setAttribute("data-md-color-" + key, value)
</script> </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 %} {% import "partials/language.html" as lang with context %}
<!-- Last updated date --> <!-- Last updated date -->
{% set label = lang.t("source.file.date.updated") %}
<hr /> <hr />
<div class="md-source-date"> <div class="md-source-date">
<small> <small>
<!-- mkdocs-git-revision-date-localized-plugin --> <!-- mkdocs-git-revision-date-localized-plugin -->
{% if page.meta.git_revision_date_localized %} {% 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 %} {% 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 %} {% endif %}
<!-- mkdocs-git-revision-date-plugin --> <!-- mkdocs-git-revision-date-plugin -->
{% elif page.meta.revision_date %} {% elif page.meta.revision_date %}
{{ label }}: {{ page.meta.revision_date }} {{ lang.t("source.file.date.updated") }}:
{{ page.meta.revision_date }}
{% endif %} {% endif %}
</small> </small>
</div> </div>

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

@ -52,13 +52,47 @@ declare global {
const __search: GlobalSearchConfig | undefined 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 * Google Analytics
*/ */
declare global {
function ga(...args: string[]): void function ga(...args: string[]): void
} }
@ -74,7 +108,7 @@ declare global {
var viewport$: Observable<Viewport> /* Viewport obsevable */ var viewport$: Observable<Viewport> /* Viewport obsevable */
var tablet$: Observable<boolean> /* Tablet breakpoint observable */ var tablet$: Observable<boolean> /* Tablet breakpoint observable */
var screen$: Observable<boolean> /* Screen 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 alert$: Subject<string> /* Alert subject */
var component$: Observable<Component>/* Component observable */ var component$: Observable<Component>/* Component observable */
} }