Merge of Insiders features tied to 'Aji Panca' funding goal

This commit is contained in:
squidfunk 2022-02-17 17:20:36 +01:00
parent 90137011e9
commit b5e71dfcb1
57 changed files with 1670 additions and 13500 deletions

View File

@ -24,4 +24,5 @@ material
src/**/*.html
# Prevent stylelint from constantly complaining
*.css
*.ts

View File

@ -150,7 +150,7 @@ are still exclusively available to sponsors as part of [Insiders]:
- [Navigation icons]
- [Remove generator notice]
- [Rich search previews]
- [Stay on page when switching versions]
- Stay on page when switching versions
- [Search highlighting]
- [Search sharing]
- [Search suggestions]
@ -202,7 +202,6 @@ __55__ times, `mkdocs-material-insiders` was shipped __72__ times.
[Section index pages]: ../../setup/setting-up-navigation.md#section-index-pages
[Site language selection]: ../../setup/changing-the-language.md#site-language-selector
[Social cards]: ../../setup/setting-up-social-cards.md
[Stay on page when switching versions]: ../../setup/setting-up-versioning.md#stay-on-page
[Sticky navigation tabs]: ../../setup/setting-up-navigation.md#sticky-navigation-tabs
[Tags with search integration]: ../../setup/setting-up-tags.md
[Tokenizer with lookahead]: search-better-faster-smaller.md#tokenizer-lookahead

View File

@ -184,7 +184,6 @@ The following features are solely available via Material for MkDocs Insiders:
- [x] [Linking content tabs]
- [x] [Boosting pages in search]
- [x] [Tags with search integration]
- [x] [Stay on page when switching versions]
- [x] [Custom admonition icons]
- [x] [Mermaid.js integration]
@ -200,16 +199,6 @@ features prefixed with a checkmark symbol, denoting whether a feature is
:octicons-check-circle-fill-24:{ style="color: var(--md-default-fg-color--lightest)" } planned, but not yet implemented. When the funding goal is hit, the features
are released for general availability.
#### $ 5,000 Aji Panca
- [x] [Mermaid.js integration]
- [x] [Stay on page when switching versions]
- [x] [Tags with search integration]
[Mermaid.js integration]: ../reference/diagrams.md
[Stay on page when switching versions]: ../setup/setting-up-versioning.md#stay-on-page
[Tags with search integration]: ../setup/setting-up-tags.md
#### $ 6,000 Trinidad Scorpion
- [x] [Boosting pages in search]
@ -277,6 +266,15 @@ This section lists all funding goals that were previously completed, which means
that those features were part of Insiders, but are now generally available and
can be used by all users.
#### $ 5,000 Aji Panca
- [x] [Mermaid.js integration]
- [x] Stay on page when switching versions
- [x] [Tags with search integration]
[Mermaid.js integration]: ../reference/diagrams.md
[Tags with search integration]: ../setup/setting-up-tags.md
#### $ 4,000 Ghost Pepper
- [x] [Anchor tracking]

View File

@ -14,8 +14,7 @@ popular and flexible solution for drawing diagrams.
## Configuration
[:octicons-heart-fill-24:{ .mdx-heart } Insiders][Insiders]{ .mdx-insiders } ·
[:octicons-tag-24: insiders-1.15.0][Insiders] ·
[:octicons-tag-24: 8.2.0][Diagrams support] ·
:octicons-beaker-24: Experimental
This configuration enables native support for [Mermaid.js] diagrams. Material
@ -44,7 +43,7 @@ No further configuration is necessary. Advantages over a custom integration:
sequence diagrams, class diagams, state diagrams and entity relationship
diagrams.
[Insiders]: ../insiders/index.md
[Diagrams support]: https://github.com/squidfunk/mkdocs-material/releases/tag/8.2.0
[instant loading]: ../setup/setting-up-navigation.md#instant-loading
[additional style sheets]: ../customization.md#additional-css

View File

@ -13,8 +13,7 @@ can help to discover relevant information faster.
### Built-in tags
[:octicons-heart-fill-24:{ .mdx-heart } Insiders][Insiders]{ .mdx-insiders } ·
[:octicons-tag-24: insiders-2.7.0][Insiders] ·
[:octicons-tag-24: 8.2.0][tags support] ·
:octicons-cpu-24: Plugin ·
:octicons-beaker-24: Experimental
@ -47,7 +46,7 @@ The following configuration options are available:
option is not specified, tags are still rendered and searchable,
but without a tags index.
[Insiders]: ../insiders/index.md
[tags support]: https://github.com/squidfunk/mkdocs-material/releases/tag/8.2.0
[adding a tags index]: #adding-a-tags-index
## Usage

View File

@ -108,33 +108,6 @@ Make sure that this matches the [default version].
[Version warning preview]: ../assets/screenshots/version-warning.png
[default version]: #setting-a-default-version
### Stay on page
[:octicons-heart-fill-24:{ .mdx-heart } Insiders][Insiders]{ .mdx-insiders } ·
[:octicons-tag-24: insiders-2.6.0][Insiders]
Insiders improves the user experience when switching between versions: if
version A and B contain a page with the same path name, the user will stay on
the current page:
=== "New behavior"
```
docs.example.com/0.1/ -> docs.example.com/0.2/
docs.example.com/0.1/foo/ -> docs.example.com/0.2/foo/
docs.example.com/0.1/bar/ -> docs.example.com/0.2/bar/
```
=== "Old behavior"
```
docs.example.com/0.1/ -> docs.example.com/0.2/
docs.example.com/0.1/foo/ -> docs.example.com/0.2/
docs.example.com/0.1/bar/ -> docs.example.com/0.2/
```
[Insiders]: ../insiders/index.md
## Usage
While this section outlines the basic workflow for publishing new versions,

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.50e68009.min.css' | url }}">
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.e8d9bf0c.min.css' | url }}">
{% if config.theme.palette %}
{% set palette = config.theme.palette %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.e6a45f82.min.css' | url }}">
@ -184,7 +184,7 @@
"base": base_url,
"features": features,
"translations": {},
"search": "assets/javascripts/workers/search.092fa1f6.min.js" | url
"search": "assets/javascripts/workers/search.bd0b6b67.min.js" | url
} -%}
{%- if config.extra.version -%}
{%- set _ = app.update({ "version": config.extra.version }) -%}
@ -213,7 +213,7 @@
</script>
{% endblock %}
{% block scripts %}
<script src="{{ 'assets/javascripts/bundle.5a9542cf.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.8aa65030.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

View File

@ -16,5 +16,5 @@
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{ 'overrides/assets/javascripts/bundle.316827ea.min.js' | url }}"></script>
<script src="{{ 'overrides/assets/javascripts/bundle.90528257.min.js' | url }}"></script>
{% endblock %}

View File

@ -6,6 +6,9 @@
{% include ".icons/material/pencil.svg" %}
</a>
{% endif %}
{% if "tags" in config.plugins %}
{% include "partials/tags.html" %}
{% endif %}
{% if not "\x3ch1" in page.content %}
<h1>{{ page.title | d(config.site_name, true)}}</h1>
{% endif %}

View File

@ -0,0 +1,19 @@
{#-
This file was automatically generated - do not edit
-#}
{% if page and page.meta and page.meta.hide %}
{% set hidden = "hidden" if "tags" in page.meta.hide %}
{% endif %}
{% if tags %}
<nav class="md-tags" {{ hidden }}>
{% for tag in tags %}
{% if tag.url %}
<a href="{{ tag.url | url }}" class="md-tag">
{{ tag.name }}
</a>
{% else %}
<span class="md-tag">{{ tag.name }}</span>
{% endif %}
{% endfor %}
</nav>
{% endif %}

View File

View File

@ -0,0 +1,49 @@
# 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.
from mkdocs.contrib.search import SearchPlugin as BasePlugin
from mkdocs.contrib.search.search_index import SearchIndex as BaseIndex
# -----------------------------------------------------------------------------
# Class
# -----------------------------------------------------------------------------
# Search plugin with custom search index
class SearchPlugin(BasePlugin):
# Override to use a custom search index
def on_pre_build(self, config):
super().on_pre_build(config)
self.search_index = SearchIndex(**self.config)
# -----------------------------------------------------------------------------
# Search index with support for additional fields
class SearchIndex(BaseIndex):
# Override to add additional fields for each page
def add_entry_from_context(self, page):
index = len(self._entries)
super().add_entry_from_context(page)
entry = self._entries[index]
# Add document tags
if "tags" in page.meta:
entry["tags"] = page.meta["tags"]

View File

View File

@ -0,0 +1,117 @@
# Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com>
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from collections import defaultdict
from markdown.extensions.toc import slugify
from mkdocs import utils
from mkdocs.config.config_options import Type
from mkdocs.plugins import BasePlugin
# -----------------------------------------------------------------------------
# Class
# -----------------------------------------------------------------------------
# Tags plugin
class TagsPlugin(BasePlugin):
# Configuration scheme
config_scheme = (
("tags_file", Type(str, required = False)),
)
# Initialize plugin
def __init__(self):
self.tags = defaultdict(list)
self.tags_file = None
self.slugify = None
# Retrieve configuration for anchor generation
def on_config(self, config):
if "toc" in config["markdown_extensions"]:
toc = { "slugify": slugify, "separator": "-" }
if "toc" in config["mdx_configs"]:
toc = { **toc, **config["mdx_configs"]["toc"] }
# Partially apply slugify function
self.slugify = lambda value: (
toc["slugify"](value, toc["separator"])
)
# Hack: 2nd pass for tags index page
def on_nav(self, nav, files, **kwargs):
file = self.config.get("tags_file")
if file:
self.tags_file = files.get_file_from_path(file)
files.append(self.tags_file)
# Build and render tags index page
def on_page_markdown(self, markdown, page, **kwargs):
if page.file == self.tags_file:
return self.__render_tag_index(markdown)
# Add page to tags index
for tag in page.meta.get("tags", []):
self.tags[tag].append(page)
# Inject tags into page (after search and before minification)
def on_page_context(self, context, page, **kwargs):
if "tags" in page.meta:
context["tags"] = [
self.__render_tag(tag)
for tag in page.meta["tags"]
]
# -------------------------------------------------------------------------
# Render tags index
def __render_tag_index(self, markdown):
if not "[TAGS]" in markdown:
markdown += "\n[TAGS]"
# Replace placeholder in Markdown with rendered tags index
return markdown.replace("[TAGS]", "\n".join([
self.__render_tag_links(*args)
for args in sorted(self.tags.items())
]))
# Render the given tag and links to all pages with occurrences
def __render_tag_links(self, tag, pages):
content = ["## <span class=\"md-tag\">{}</span>".format(tag), ""]
for page in pages:
url = utils.get_relative_url(
page.file.src_path,
self.tags_file.src_path
)
content.append("- [{}]({})".format(
page.meta.get("title", page.title),
url
))
# Return rendered tag links
return "\n".join(content)
# Render the given tag, linking to the tags index (if enabled)
def __render_tag(self, tag):
if not self.tags_file or not self.slugify:
return dict(name = tag)
else:
url = self.tags_file.url
url += "#{}".format(self.slugify(tag))
return dict(name = tag, url = url)

13167
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -55,6 +55,7 @@
"@mdi/svg": "^6.5.95",
"@primer/octicons": "^16.3.1",
"@types/clipboard": "^2.0.7",
"@types/css-modules": "^1.0.2",
"@types/escape-html": "1.0.1",
"@types/fuzzaldrin-plus": "^0.6.2",
"@types/html-minifier": "^4.0.2",

View File

@ -58,13 +58,17 @@ setup(
"Topic :: Software Development :: Documentation",
"Topic :: Text Processing :: Markup :: HTML"
],
packages = find_packages(exclude = ["src"]),
packages = find_packages(exclude = ["src", "src.*"]),
include_package_data = True,
install_requires = install_requires,
python_requires='>=3.6',
entry_points = {
"mkdocs.themes": [
"material = material",
"material = material"
],
"mkdocs.plugins": [
"search = material.plugins.search.plugin:SearchPlugin",
"tags = material.plugins.tags.plugin:TagsPlugin"
]
},
zip_safe = False

View File

@ -26,6 +26,7 @@ export * from "./keyboard"
export * from "./location"
export * from "./media"
export * from "./request"
export * from "./script"
export * from "./toggle"
export * from "./viewport"
export * from "./worker"

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import {
Observable,
defer,
finalize,
fromEvent,
mapTo,
merge,
switchMap,
take,
throwError
} from "rxjs"
import { h } from "~/utilities"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Create and load a `script` element
*
* This function returns an observable that will emit when the script was
* successfully loaded, or throw an error if it didn't.
*
* @param src - Script URL
*
* @returns Script observable
*/
export function watchScript(src: string): Observable<void> {
const script = h("script", { src })
return defer(() => {
document.head.appendChild(script)
return merge(
fromEvent(script, "load"),
fromEvent(script, "error")
.pipe(
switchMap(() => (
throwError(() => new ReferenceError(`Invalid script: ${src}`))
))
)
)
.pipe(
mapTo(undefined),
finalize(() => document.head.removeChild(script)),
take(1)
)
})
}

View File

@ -26,10 +26,24 @@ import { getElements } from "~/browser"
import { Component } from "../../_"
import { Annotation } from "../annotation"
import { CodeBlock, mountCodeBlock } from "../code"
import { Details, mountDetails } from "../details"
import { DataTable, mountDataTable } from "../table"
import { ContentTabs, mountContentTabs } from "../tabs"
import {
CodeBlock,
Mermaid,
mountCodeBlock,
mountMermaid
} from "../code"
import {
Details,
mountDetails
} from "../details"
import {
DataTable,
mountDataTable
} from "../table"
import {
ContentTabs,
mountContentTabs
} from "../tabs"
/* ----------------------------------------------------------------------------
* Types
@ -42,6 +56,7 @@ export type Content =
| Annotation
| ContentTabs
| CodeBlock
| Mermaid
| DataTable
| Details
@ -78,9 +93,13 @@ export function mountContent(
return merge(
/* Code blocks */
...getElements("pre > code", el)
...getElements("pre:not(.mermaid) > code", el)
.map(child => mountCodeBlock(child, { print$ })),
/* Mermaid diagrams */
...getElements("pre.mermaid", el)
.map(child => mountMermaid(child)),
/* Data tables */
...getElements("table:not([class])", el)
.map(child => mountDataTable(child)),

View File

@ -0,0 +1,216 @@
/*
* Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import ClipboardJS from "clipboard"
import {
EMPTY,
Observable,
Subject,
defer,
distinctUntilChanged,
distinctUntilKeyChanged,
finalize,
map,
mergeWith,
switchMap,
takeLast,
takeUntil,
tap
} from "rxjs"
import { feature } from "~/_"
import {
getElementContentSize,
watchElementSize
} from "~/browser"
import { renderClipboardButton } from "~/templates"
import { Component } from "../../../_"
import {
Annotation,
mountAnnotationList
} from "../../annotation"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Code block
*/
export interface CodeBlock {
scrollable: boolean /* Code block overflows */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Mount options
*/
interface MountOptions {
print$: Observable<boolean> /* Media print observable */
}
/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */
/**
* Global sequence number for Clipboard.js integration
*/
let sequence = 0
/* ----------------------------------------------------------------------------
* Helper functions
* ------------------------------------------------------------------------- */
/**
* Find candidate list element directly following a code block
*
* @param el - Code block element
*
* @returns List element or nothing
*/
function findCandidateList(el: HTMLElement): HTMLElement | undefined {
if (el.nextElementSibling) {
const sibling = el.nextElementSibling as HTMLElement
if (sibling.tagName === "OL")
return sibling
/* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */
else if (sibling.tagName === "P" && !sibling.children.length)
return findCandidateList(sibling)
}
/* Everything else */
return undefined
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch code block
*
* This function monitors size changes of the viewport, as well as switches of
* content tabs with embedded code blocks, as both may trigger overflow.
*
* @param el - Code block element
*
* @returns Code block observable
*/
export function watchCodeBlock(
el: HTMLElement
): Observable<CodeBlock> {
return watchElementSize(el)
.pipe(
map(({ width }) => {
const content = getElementContentSize(el)
return {
scrollable: content.width > width
}
}),
distinctUntilKeyChanged("scrollable")
)
}
/**
* Mount code block
*
* This function ensures that an overflowing code block is focusable through
* keyboard, so it can be scrolled without a mouse to improve on accessibility.
* Furthermore, if code annotations are enabled, they are mounted if and only
* if the code block is currently visible, e.g., not in a hidden content tab.
*
* @param el - Code block element
* @param options - Options
*
* @returns Code block and annotation component observable
*/
export function mountCodeBlock(
el: HTMLElement, options: MountOptions
): Observable<Component<CodeBlock | Annotation>> {
const { matches: hover } = matchMedia("(hover)")
return defer(() => {
const push$ = new Subject<CodeBlock>()
push$.subscribe(({ scrollable }) => {
if (scrollable && hover)
el.setAttribute("tabindex", "0")
else
el.removeAttribute("tabindex")
})
/* Render button for Clipboard.js integration */
if (ClipboardJS.isSupported()) {
const parent = el.closest("pre")!
parent.id = `__code_${++sequence}`
parent.insertBefore(
renderClipboardButton(parent.id),
el
)
}
/* Handle code annotations */
const container = el.closest([
":not(td):not(.code) > .highlight",
".highlighttable"
].join(", "))
if (container instanceof HTMLElement) {
const list = findCandidateList(container)
/* Mount code annotations, if enabled */
if (typeof list !== "undefined" && (
container.classList.contains("annotate") ||
feature("content.code.annotate")
)) {
const annotations$ = mountAnnotationList(list, el, options)
/* Create and return component */
return watchCodeBlock(el)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state })),
mergeWith(watchElementSize(container)
.pipe(
takeUntil(push$.pipe(takeLast(1))),
map(({ width, height }) => width && height),
distinctUntilChanged(),
switchMap(active => active ? annotations$ : EMPTY)
)
)
)
}
}
/* Create and return component */
return watchCodeBlock(el)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
})
}

View File

@ -20,197 +20,5 @@
* IN THE SOFTWARE.
*/
import ClipboardJS from "clipboard"
import {
EMPTY,
Observable,
Subject,
defer,
distinctUntilChanged,
distinctUntilKeyChanged,
finalize,
map,
mergeWith,
switchMap,
takeLast,
takeUntil,
tap
} from "rxjs"
import { feature } from "~/_"
import {
getElementContentSize,
watchElementSize
} from "~/browser"
import { renderClipboardButton } from "~/templates"
import { Component } from "../../_"
import {
Annotation,
mountAnnotationList
} from "../annotation"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Code block
*/
export interface CodeBlock {
scrollable: boolean /* Code block overflows */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Mount options
*/
interface MountOptions {
print$: Observable<boolean> /* Media print observable */
}
/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */
/**
* Global sequence number for Clipboard.js integration
*/
let sequence = 0
/* ----------------------------------------------------------------------------
* Helper functions
* ------------------------------------------------------------------------- */
/**
* Find candidate list element directly following a code block
*
* @param el - Code block element
*
* @returns List element or nothing
*/
function findCandidateList(el: HTMLElement): HTMLElement | undefined {
if (el.nextElementSibling) {
const sibling = el.nextElementSibling as HTMLElement
if (sibling.tagName === "OL")
return sibling
/* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */
else if (sibling.tagName === "P" && !sibling.children.length)
return findCandidateList(sibling)
}
/* Everything else */
return undefined
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch code block
*
* This function monitors size changes of the viewport, as well as switches of
* content tabs with embedded code blocks, as both may trigger overflow.
*
* @param el - Code block element
*
* @returns Code block observable
*/
export function watchCodeBlock(
el: HTMLElement
): Observable<CodeBlock> {
return watchElementSize(el)
.pipe(
map(({ width }) => {
const content = getElementContentSize(el)
return {
scrollable: content.width > width
}
}),
distinctUntilKeyChanged("scrollable")
)
}
/**
* Mount code block
*
* This function ensures that an overflowing code block is focusable through
* keyboard, so it can be scrolled without a mouse to improve on accessibility.
* Furthermore, if code annotations are enabled, they are mounted if and only
* if the code block is currently visible, e.g., not in a hidden content tab.
*
* @param el - Code block element
* @param options - Options
*
* @returns Code block and annotation component observable
*/
export function mountCodeBlock(
el: HTMLElement, options: MountOptions
): Observable<Component<CodeBlock | Annotation>> {
const { matches: hover } = matchMedia("(hover)")
return defer(() => {
const push$ = new Subject<CodeBlock>()
push$.subscribe(({ scrollable }) => {
if (scrollable && hover)
el.setAttribute("tabindex", "0")
else
el.removeAttribute("tabindex")
})
/* Render button for Clipboard.js integration */
if (ClipboardJS.isSupported()) {
const parent = el.closest("pre")!
parent.id = `__code_${++sequence}`
parent.insertBefore(
renderClipboardButton(parent.id),
el
)
}
/* Handle code annotations */
const container = el.closest([
":not(td):not(.code) > .highlight",
".highlighttable"
].join(", "))
if (container instanceof HTMLElement) {
const list = findCandidateList(container)
/* Mount code annotations, if enabled */
if (typeof list !== "undefined" && (
container.classList.contains("annotate") ||
feature("content.code.annotate")
)) {
const annotations$ = mountAnnotationList(list, el, options)
/* Create and return component */
return watchCodeBlock(el)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state })),
mergeWith(watchElementSize(container)
.pipe(
takeUntil(push$.pipe(takeLast(1))),
map(({ width, height }) => width && height),
distinctUntilChanged(),
switchMap(active => active ? annotations$ : EMPTY)
)
)
)
}
}
/* Create and return component */
return watchCodeBlock(el)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
})
}
export * from "./_"
export * from "./mermaid"

View File

@ -0,0 +1,350 @@
/*
* Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Rules: general
* ------------------------------------------------------------------------- */
/* General node */
.node circle,
.node ellipse,
.node path,
.node polygon,
.node rect {
fill: var(--md-mermaid-node-bg-color);
stroke: var(--md-mermaid-node-fg-color);
}
/* General marker */
marker {
fill: var(--md-mermaid-edge-color) !important;
}
/* General edge label */
.edgeLabel .label rect {
fill: transparent;
}
/* ----------------------------------------------------------------------------
* Rules: flowcharts
* ------------------------------------------------------------------------- */
/* Flowchart node label */
.label {
color: var(--md-mermaid-label-fg-color);
font-family: var(--md-mermaid-font-family);
}
/* Flowchart node label container */
.label foreignObject {
overflow: visible;
line-height: initial;
}
/* Flowchart edge label in node label */
.label div .edgeLabel {
color: var(--md-mermaid-label-fg-color);
background-color: var(--md-mermaid-label-bg-color);
}
/* Flowchart edge label */
.edgeLabel,
.edgeLabel rect {
color: var(--md-mermaid-edge-color);
background-color: var(--md-mermaid-label-bg-color);
fill: var(--md-mermaid-label-bg-color);
}
/* Flowchart edge path */
.edgePath .path,
.flowchart-link {
stroke: var(--md-mermaid-edge-color);
}
/* Flowchart arrow head */
.edgePath .arrowheadPath {
fill: var(--md-mermaid-edge-color);
stroke: none;
}
/* Flowchart subgraph */
.cluster rect {
fill: var(--md-default-fg-color--lightest);
stroke: var(--md-default-fg-color--lighter);
}
/* ----------------------------------------------------------------------------
* Rules: class diagrams
* ------------------------------------------------------------------------- */
/* Class group node */
g.classGroup line,
g.classGroup rect {
fill: var(--md-mermaid-node-bg-color);
stroke: var(--md-mermaid-node-fg-color);
}
/* Class group node text */
g.classGroup text {
font-family: var(--md-mermaid-font-family);
fill: var(--md-mermaid-label-fg-color);
}
/* Class label box */
.classLabel .box {
background-color: var(--md-mermaid-label-bg-color);
opacity: 1;
fill: var(--md-mermaid-label-bg-color);
}
/* Class label text */
.classLabel .label {
font-family: var(--md-mermaid-font-family);
fill: var(--md-mermaid-label-fg-color);
}
/* Class group divider */
.node .divider {
stroke: var(--md-mermaid-node-fg-color);
}
/* Class relation */
.relation {
stroke: var(--md-mermaid-edge-color);
}
/* Class relation cardinality */
.cardinality {
font-family: var(--md-mermaid-font-family);
fill: var(--md-mermaid-label-fg-color);
}
/* Class relation cardinality text */
.cardinality text {
fill: inherit !important;
}
/* Class extension, composition and dependency marker */
#extensionStart,
#extensionEnd,
#compositionStart,
#compositionEnd,
#dependencyStart,
#dependencyEnd {
fill: var(--md-mermaid-edge-color) !important;
stroke: var(--md-mermaid-edge-color) !important;
}
/* Class aggregation marker */
#aggregationStart,
#aggregationEnd {
fill: var(--md-mermaid-label-bg-color) !important;
stroke: var(--md-mermaid-edge-color) !important;
}
/* ----------------------------------------------------------------------------
* Rules: state diagrams
* ------------------------------------------------------------------------- */
/* State group node */
g.stateGroup rect {
fill: var(--md-mermaid-node-bg-color);
stroke: var(--md-mermaid-node-fg-color);
}
/* State group title */
g.stateGroup .state-title {
font-family: var(--md-mermaid-font-family);
fill: var(--md-mermaid-label-fg-color) !important;
}
/* State group background */
g.stateGroup .composit {
fill: var(--md-mermaid-label-bg-color);
}
/* State node label */
.nodeLabel {
color: var(--md-mermaid-label-fg-color);
font-family: var(--md-mermaid-font-family);
}
/* State start and end marker */
.start-state,
.node circle.state-start,
.node circle.state-end {
fill: var(--md-mermaid-edge-color);
stroke: none;
}
/* State end marker */
.end-state-outer,
.end-state-inner {
fill: var(--md-mermaid-edge-color);
}
/* State end marker */
.end-state-inner,
.node circle.state-end {
stroke: var(--md-mermaid-label-bg-color);
}
/* State transition */
.transition {
stroke: var(--md-mermaid-edge-color);
}
/* State fork and join */
[id^=state-fork] rect,
[id^=state-join] rect {
fill: var(--md-mermaid-edge-color) !important;
stroke: none !important;
}
/* State cluster (yes, 2x... Mermaid WTF) */
.statediagram-cluster.statediagram-cluster .inner {
fill: var(--md-default-bg-color);
}
/* State cluster node */
.statediagram-cluster rect {
fill: var(--md-mermaid-node-bg-color);
stroke: var(--md-mermaid-node-fg-color);
}
/* State cluster divider */
.statediagram-state rect.divider {
fill: var(--md-default-fg-color--lightest);
stroke: var(--md-default-fg-color--lighter);
}
/* ----------------------------------------------------------------------------
* Rules: entity-relationship diagrams
* ------------------------------------------------------------------------- */
/* Entity node */
.entityBox {
fill: var(--md-mermaid-label-bg-color);
stroke: var(--md-mermaid-node-fg-color);
}
/* Entity node label */
.entityLabel {
font-family: var(--md-mermaid-font-family);
fill: var(--md-mermaid-label-fg-color);
}
/* Entity relationship label container */
.relationshipLabelBox {
background-color: var(--md-mermaid-label-bg-color);
opacity: 1;
fill: var(--md-mermaid-label-bg-color);
fill-opacity: 1;
}
/* Entity relationship label */
.relationshipLabel {
fill: var(--md-mermaid-label-fg-color);
}
/* Entity relationship line { */
.relationshipLine {
stroke: var(--md-mermaid-edge-color);
}
/* Entity relationship line markers */
#ZERO_OR_ONE_START *,
#ZERO_OR_ONE_END *,
#ZERO_OR_MORE_START *,
#ZERO_OR_MORE_END *,
#ONLY_ONE_START *,
#ONLY_ONE_END *,
#ONE_OR_MORE_START *,
#ONE_OR_MORE_END * {
stroke: var(--md-mermaid-edge-color) !important;
}
/* Entity relationship line markers */
#ZERO_OR_MORE_START circle,
#ZERO_OR_MORE_END circle {
fill: var(--md-mermaid-label-bg-color);
}
/* ----------------------------------------------------------------------------
* Rules: sequence diagrams
* ------------------------------------------------------------------------- */
/* Sequence actor */
.actor {
fill: var(--md-mermaid-label-bg-color);
stroke: var(--md-mermaid-node-fg-color);
}
/* Sequence actor text */
text.actor > tspan {
font-family: var(--md-mermaid-font-family);
fill: var(--md-mermaid-label-fg-color);
}
/* Sequence actor line */
line {
stroke: var(--md-default-fg-color--lighter);
}
/* Sequence message line */
.messageLine0,
.messageLine1 {
stroke: var(--md-mermaid-edge-color);
}
/* Sequence message and loop text */
.messageText,
.loopText > tspan {
font-family: var(--md-mermaid-font-family) !important;
fill: var(--md-mermaid-edge-color);
stroke: none;
}
/* Sequence arrow head */
#arrowhead path {
fill: var(--md-mermaid-edge-color);
stroke: none;
}
/* Sequence loop line */
.loopLine {
fill: var(--md-mermaid-node-bg-color);
stroke: var(--md-mermaid-node-fg-color);
}
/* Sequence label box */
.labelBox {
fill: var(--md-mermaid-node-bg-color);
stroke: none;
}
/* Sequence label text */
.labelText,
.labelText > span {
font-family: var(--md-mermaid-font-family);
fill: var(--md-mermaid-node-fg-color);
}

View File

@ -0,0 +1,122 @@
/*
* Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import {
Observable,
mapTo,
of,
shareReplay,
tap
} from "rxjs"
import { watchScript } from "~/browser"
import { h } from "~/utilities"
import { Component } from "../../../_"
import themeCSS from "./index.css"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Mermaid diagram
*/
export interface Mermaid {}
/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */
/**
* Mermaid instance observable
*/
let mermaid$: Observable<void>
/**
* Global index for Mermaid integration
*/
let index = 0
/* ----------------------------------------------------------------------------
* Helper functions
* ------------------------------------------------------------------------- */
/**
* Fetch Mermaid script
*
* @returns Mermaid scripts observable
*/
function fetchScripts(): Observable<void> {
return typeof mermaid === "undefined"
? watchScript("https://unpkg.com/mermaid@8.13.3/dist/mermaid.min.js")
: of(undefined)
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Mount Mermaid diagram
*
* @param el - Code block element
*
* @returns Mermaid diagram component observable
*/
export function mountMermaid(
el: HTMLElement
): Observable<Component<Mermaid>> {
el.classList.remove("mermaid") // Hack: mitigate https://bit.ly/3CiN6Du
mermaid$ ||= fetchScripts()
.pipe(
tap(() => mermaid.initialize({
startOnLoad: false,
themeCSS
})),
mapTo(undefined),
shareReplay(1)
)
/* Render diagram */
mermaid$.subscribe(() => {
el.classList.add("mermaid") // Hack: mitigate https://bit.ly/3CiN6Du
const id = `__mermaid_${index++}`
const host = h("div", { class: "mermaid" })
mermaid.mermaidAPI.render(id, el.textContent, (svg: string) => {
/* Create a shadow root and inject diagram */
const shadow = host.attachShadow({ mode: "closed" })
shadow.innerHTML = svg
/* Replace code block with diagram */
el.replaceWith(host)
})
})
/* Create and return component */
return mermaid$
.pipe(
mapTo({ ref: el })
)
}

View File

@ -23,4 +23,5 @@
export * from "./clipboard"
export * from "./instant"
export * from "./search"
export * from "./sitemap"
export * from "./version"

View File

@ -50,13 +50,14 @@ import {
getElements,
getOptionalElement,
request,
requestXML,
setLocation,
setLocationHash
} from "~/browser"
import { getComponentElement } from "~/components"
import { h } from "~/utilities"
import { fetchSitemap } from "../sitemap"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
@ -82,44 +83,6 @@ interface SetupOptions {
viewport$: Observable<Viewport> /* Viewport observable */
}
/* ----------------------------------------------------------------------------
* Helper functions
* ------------------------------------------------------------------------- */
/**
* Preprocess a list of URLs
*
* This function replaces the `site_url` in the sitemap with the actual base
* URL, to allow instant loading to work in occasions like Netlify previews.
*
* @param urls - URLs
*
* @returns Processed URLs
*/
function preprocess(urls: string[]): string[] {
if (urls.length < 2)
return urls
/* Take the first two URLs and remove everything after the last slash */
const [root, next] = urls
.sort((a, b) => a.length - b.length)
.map(url => url.replace(/[^/]+$/, ""))
/* Compute common prefix */
let index = 0
if (root === next)
index = root.length
else
while (root.charCodeAt(index) === next.charCodeAt(index))
index++
/* Replace common prefix (i.e. base) with effective base */
const config = configuration()
return urls.map(url => (
url.replace(root.slice(0, index), config.base)
))
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
@ -169,17 +132,13 @@ export function setupInstantLoading(
favicon.href = favicon.href
/* Intercept internal navigation */
const push$ = requestXML(new URL("sitemap.xml", config.base))
const push$ = fetchSitemap()
.pipe(
map(sitemap => preprocess(getElements("loc", sitemap)
.map(node => node.textContent!)
)),
map(paths => paths.map(path => `${new URL(path, config.base)}`)),
switchMap(urls => fromEvent<MouseEvent>(document.body, "click")
.pipe(
filter(ev => !ev.metaKey && !ev.ctrlKey),
switchMap(ev => {
/* Handle HTML and SVG elements */
if (ev.target instanceof Element) {
const el = ev.target.closest("a")
if (el && !el.target) {

View File

@ -55,6 +55,7 @@ export interface SearchIndexDocument {
location: string /* Document location */
title: string /* Document title */
text: string /* Document text */
tags?: string[] /* Document tags */
}
/* ------------------------------------------------------------------------- */
@ -202,6 +203,7 @@ export class Search {
/* Set up fields */
this.field("title", { boost: 1e3 })
this.field("text")
this.field("tags", { boost: 1e6 })
/* Index documents */
for (const doc of docs)
@ -243,7 +245,7 @@ export class Search {
.reduce<SearchResultItem>((item, { ref, score, matchData }) => {
const document = this.documents.get(ref)
if (typeof document !== "undefined") {
const { location, title, text, parent } = document
const { location, title, text, tags, parent } = document
/* Compute and analyze search query terms */
const terms = getSearchQueryTerms(
@ -257,6 +259,7 @@ export class Search {
location,
title: highlight(title),
text: highlight(text),
...tags && { tags: tags.map(highlight) },
score: score * (1 + boost),
terms
})

View File

@ -61,9 +61,10 @@ export function setupSearchDocumentMap(
for (const doc of docs) {
const [path, hash] = doc.location.split("#")
/* Extract location and title */
/* Extract location, title and tags */
const location = doc.location
const title = doc.title
const tags = doc.tags
/* Escape and cleanup text */
const text = escapeHTML(doc.text)
@ -97,7 +98,8 @@ export function setupSearchDocumentMap(
documents.set(location, {
location,
title,
text
text,
...tags && { tags }
})
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import {
Observable,
defaultIfEmpty,
map,
of,
tap
} from "rxjs"
import { configuration } from "~/_"
import { getElements, requestXML } from "~/browser"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Sitemap, i.e. a list of URLs
*/
export type Sitemap = string[]
/* ----------------------------------------------------------------------------
* Helper functions
* ------------------------------------------------------------------------- */
/**
* Preprocess a list of URLs
*
* This function replaces the `site_url` in the sitemap with the actual base
* URL, to allow instant loading to work in occasions like Netlify previews.
*
* @param urls - URLs
*
* @returns URL path parts
*/
function preprocess(urls: Sitemap): Sitemap {
if (urls.length < 2)
return [""]
/* Take the first two URLs and remove everything after the last slash */
const [root, next] = [...urls]
.sort((a, b) => a.length - b.length)
.map(url => url.replace(/[^/]+$/, ""))
/* Compute common prefix */
let index = 0
if (root === next)
index = root.length
else
while (root.charCodeAt(index) === next.charCodeAt(index))
index++
/* Remove common prefix and return in original order */
return urls.map(url => url.replace(root.slice(0, index), ""))
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Fetch the sitemap for the given base URL
*
* @param base - Base URL
*
* @returns Sitemap observable
*/
export function fetchSitemap(base?: URL): Observable<Sitemap> {
const cached = __md_get<Sitemap>("__sitemap", sessionStorage, base)
if (cached) {
return of(cached)
} else {
const config = configuration()
return requestXML(new URL("sitemap.xml", base || config.base))
.pipe(
map(sitemap => preprocess(getElements("loc", sitemap)
.map(node => node.textContent!)
)),
defaultIfEmpty([]),
tap(sitemap => __md_set("__sitemap", sitemap, sessionStorage, base))
)
}
}

View File

@ -20,12 +20,22 @@
* IN THE SOFTWARE.
*/
import { combineLatest, map } from "rxjs"
import {
EMPTY,
combineLatest,
filter,
fromEvent,
map,
of,
switchMap
} from "rxjs"
import { configuration } from "~/_"
import {
getElement,
requestJSON
getLocation,
requestJSON,
setLocation
} from "~/browser"
import { getComponentElements } from "~/components"
import {
@ -33,6 +43,8 @@ import {
renderVersionSelector
} from "~/templates"
import { fetchSitemap } from "../sitemap"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
@ -57,6 +69,47 @@ export function setupVersionSelector(): void {
})
)
/* Intercept inter-version navigation */
combineLatest([versions$, current$])
.pipe(
map(([versions, current]) => new Map(versions
.filter(version => version !== current)
.map(version => [
`${new URL(`../${version.version}/`, config.base)}`,
version
])
)),
switchMap(urls => fromEvent<MouseEvent>(document.body, "click")
.pipe(
filter(ev => !ev.metaKey && !ev.ctrlKey),
switchMap(ev => {
if (ev.target instanceof Element) {
const el = ev.target.closest("a")
if (el && !el.target && urls.has(el.href)) {
ev.preventDefault()
return of(el.href)
}
}
return EMPTY
}),
switchMap(url => {
const { version } = urls.get(url)!
return fetchSitemap(new URL(url))
.pipe(
map(sitemap => {
const location = getLocation()
const path = location.href.replace(config.base, "")
return sitemap.includes(path)
? new URL(`../${version}/${path}`, config.base)
: new URL(url)
})
)
})
)
)
)
.subscribe(url => setLocation(url))
/* Render version selector and warning */
combineLatest([versions$, current$])
.subscribe(([versions, current]) => {

View File

@ -93,6 +93,9 @@ function renderSearchDocument(
{truncate(document.text, 320)}
</p>
}
{document.tags && document.tags.map(tag => (
<span class="md-tag">{tag}</span>
))}
{teaser > 0 && missing.length > 0 &&
<p class="md-search-result__terms">
{translation("search.result.term.missing")}: {...missing}

View File

@ -55,6 +55,7 @@
@import "main/layout/sidebar";
@import "main/layout/source";
@import "main/layout/tabs";
@import "main/layout/tag";
@import "main/layout/tooltip";
@import "main/layout/top";
@import "main/layout/version";
@ -72,4 +73,6 @@
@import "main/extensions/pymdownx/tabbed";
@import "main/extensions/pymdownx/tasklist";
@import "main/integrations/mermaid";
@import "main/modifiers";

View File

@ -45,6 +45,11 @@
height: 0;
opacity: 0;
// Adjust scroll margin
&:target {
--md-scroll-offset: #{px2em(10px, 16px)};
}
// Tab label states
@for $i from 20 through 1 {
&:nth-child(#{$i}) {

View File

@ -0,0 +1,43 @@
////
/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com>
///
/// Permission is hereby granted, free of charge, to any person obtaining a
/// copy of this software and associated documentation files (the "Software"),
/// to deal in the Software without restriction, including without limitation
/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
/// and/or sell copies of the Software, and to permit persons to whom the
/// Software is furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
/// DEALINGS
////
// ----------------------------------------------------------------------------
// Rules
// ----------------------------------------------------------------------------
// All definitions
:root > * {
--md-mermaid-font-family: var(--md-text-font-family), sans-serif;
// Colors
--md-mermaid-edge-color: var(--md-code-fg-color);
--md-mermaid-node-bg-color: var(--md-accent-fg-color--transparent);
--md-mermaid-node-fg-color: var(--md-accent-fg-color);
--md-mermaid-label-bg-color: var(--md-default-bg-color);
--md-mermaid-label-fg-color: var(--md-code-fg-color);
}
// Mermaid container
.mermaid {
margin: 1em 0;
line-height: normal;
}

View File

@ -0,0 +1,65 @@
////
/// Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com>
///
/// Permission is hereby granted, free of charge, to any person obtaining a
/// copy of this software and associated documentation files (the "Software"),
/// to deal in the Software without restriction, including without limitation
/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
/// and/or sell copies of the Software, and to permit persons to whom the
/// Software is furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
/// DEALINGS
////
// ----------------------------------------------------------------------------
// Rules
// ----------------------------------------------------------------------------
// Tag list
.md-tags {
margin-bottom: px2em(12px);
}
// Tag
.md-tag {
display: inline-block;
margin-inline-end: 0.5em;
margin-bottom: 0.5em;
padding: px2em(4px, 12.8px) px2em(12px, 12.8px);
font-weight: 700;
font-size: px2rem(12.8px);
line-height: 1.6;
background: var(--md-default-fg-color--lightest);
border-radius: px2rem(8px);
// Linked tag
&[href] {
color: inherit;
outline: none;
-webkit-tap-highlight-color: transparent;
transition:
color 125ms,
background-color 125ms;
// Linked tag on focus/hover
&:focus,
&:hover {
color: var(--md-accent-bg-color);
background-color: var(--md-accent-fg-color);
}
}
// Tag inside headline
[id] > & {
vertical-align: text-top;
}
}

View File

@ -31,6 +31,11 @@
</a>
{% endif %}
<!-- Tags -->
{% if "tags" in config.plugins %}
{% include "partials/tags.html" %}
{% endif %}
<!--
Hack: check whether the content contains a h1 headline. If it
doesn't, the page title (or respectively site name) is used

41
src/partials/tags.html Normal file
View File

@ -0,0 +1,41 @@
<!--
Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
-->
<!-- Determine whether to show tags -->
{% if page and page.meta and page.meta.hide %}
{% set hidden = "hidden" if "tags" in page.meta.hide %}
{% endif %}
<!-- Tags -->
{% if tags %}
<nav class="md-tags" {{ hidden }}>
{% for tag in tags %}
{% if tag.url %}
<a href="{{ tag.url | url }}" class="md-tag">
{{ tag.name }}
</a>
{% else %}
<span class="md-tag">{{ tag.name }}</span>
{% endif %}
{% endfor %}
</nav>
{% endif %}

View File

View File

@ -0,0 +1,49 @@
# 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.
from mkdocs.contrib.search import SearchPlugin as BasePlugin
from mkdocs.contrib.search.search_index import SearchIndex as BaseIndex
# -----------------------------------------------------------------------------
# Class
# -----------------------------------------------------------------------------
# Search plugin with custom search index
class SearchPlugin(BasePlugin):
# Override to use a custom search index
def on_pre_build(self, config):
super().on_pre_build(config)
self.search_index = SearchIndex(**self.config)
# -----------------------------------------------------------------------------
# Search index with support for additional fields
class SearchIndex(BaseIndex):
# Override to add additional fields for each page
def add_entry_from_context(self, page):
index = len(self._entries)
super().add_entry_from_context(page)
entry = self._entries[index]
# Add document tags
if "tags" in page.meta:
entry["tags"] = page.meta["tags"]

View File

117
src/plugins/tags/plugin.py Normal file
View File

@ -0,0 +1,117 @@
# Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com>
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from collections import defaultdict
from markdown.extensions.toc import slugify
from mkdocs import utils
from mkdocs.config.config_options import Type
from mkdocs.plugins import BasePlugin
# -----------------------------------------------------------------------------
# Class
# -----------------------------------------------------------------------------
# Tags plugin
class TagsPlugin(BasePlugin):
# Configuration scheme
config_scheme = (
("tags_file", Type(str, required = False)),
)
# Initialize plugin
def __init__(self):
self.tags = defaultdict(list)
self.tags_file = None
self.slugify = None
# Retrieve configuration for anchor generation
def on_config(self, config):
if "toc" in config["markdown_extensions"]:
toc = { "slugify": slugify, "separator": "-" }
if "toc" in config["mdx_configs"]:
toc = { **toc, **config["mdx_configs"]["toc"] }
# Partially apply slugify function
self.slugify = lambda value: (
toc["slugify"](value, toc["separator"])
)
# Hack: 2nd pass for tags index page
def on_nav(self, nav, files, **kwargs):
file = self.config.get("tags_file")
if file:
self.tags_file = files.get_file_from_path(file)
files.append(self.tags_file)
# Build and render tags index page
def on_page_markdown(self, markdown, page, **kwargs):
if page.file == self.tags_file:
return self.__render_tag_index(markdown)
# Add page to tags index
for tag in page.meta.get("tags", []):
self.tags[tag].append(page)
# Inject tags into page (after search and before minification)
def on_page_context(self, context, page, **kwargs):
if "tags" in page.meta:
context["tags"] = [
self.__render_tag(tag)
for tag in page.meta["tags"]
]
# -------------------------------------------------------------------------
# Render tags index
def __render_tag_index(self, markdown):
if not "[TAGS]" in markdown:
markdown += "\n[TAGS]"
# Replace placeholder in Markdown with rendered tags index
return markdown.replace("[TAGS]", "\n".join([
self.__render_tag_links(*args)
for args in sorted(self.tags.items())
]))
# Render the given tag and links to all pages with occurrences
def __render_tag_links(self, tag, pages):
content = ["## <span class=\"md-tag\">{}</span>".format(tag), ""]
for page in pages:
url = utils.get_relative_url(
page.file.src_path,
self.tags_file.src_path
)
content.append("- [{}]({})".format(
page.meta.get("title", page.title),
url
))
# Return rendered tag links
return "\n".join(content)
# Render the given tag, linking to the tags index (if enabled)
def __render_tag(self, tag):
if not self.tags_file or not self.slugify:
return dict(name = tag)
else:
url = self.tags_file.url
url += "#{}".format(self.slugify(tag))
return dict(name = tag, url = url)

View File

@ -138,13 +138,20 @@ const assets$ = concat(
})),
/* Copy images and configurations */
...[".icons/*.svg", "assets/images/*", "**/*.{py,yml}"]
...[".icons/*.svg", "assets/images/*", "**/*.yml"]
.map(pattern => copyAll(pattern, {
from: "src",
to: base
}))
)
/* Copy plugins and extensions */
const sources$ = copyAll("**/*.py", {
from: "src",
to: base,
watch: process.argv.includes("--watch")
})
/* ------------------------------------------------------------------------- */
/* Transform styles */
@ -345,8 +352,8 @@ const schema$ = merge(
/* Assemble pipeline */
const build$ =
process.argv.includes("--dirty")
? templates$
: concat(assets$, merge(templates$, index$, schema$))
? merge(templates$, sources$)
: concat(assets$, merge(templates$, sources$, index$, schema$))
/* Let's get rolling */
build$.subscribe()

View File

@ -22,6 +22,7 @@
import { createHash } from "crypto"
import { build as esbuild } from "esbuild"
import * as fs from "fs/promises"
import * as path from "path"
import postcss, { Plugin, Rule } from "postcss"
import {
@ -202,7 +203,29 @@ export function transformScript(
sourcemap: true,
sourceRoot: "../../../..",
legalComments: "inline",
minify: process.argv.includes("--optimize")
minify: process.argv.includes("--optimize"),
plugins: [
/* Plugin to minify inlined CSS (e.g. for Mermaid.js) */
{
name: "mkdocs-material/inline",
setup(build) {
build.onLoad({ filter: /\.css/ }, async args => {
const content = await fs.readFile(args.path, "utf8")
const { css } = await postcss([require("cssnano")])
.process(content, {
from: undefined
})
/* Return minified CSS */
return {
contents: css,
loader: "text"
}
});
}
}
]
}))
.pipe(
switchMap(({ outputFiles: [file] }) => {

28
typings/mermaid/index.d.ts vendored Normal file
View File

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