mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-06-14 11:52:32 +03:00
Merge of Insiders features tied to 'Aji Panca' funding goal
This commit is contained in:
parent
90137011e9
commit
b5e71dfcb1
@ -24,4 +24,5 @@ material
|
||||
src/**/*.html
|
||||
|
||||
# Prevent stylelint from constantly complaining
|
||||
*.css
|
||||
*.ts
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
29
material/assets/javascripts/bundle.8aa65030.min.js
vendored
Normal file
29
material/assets/javascripts/bundle.8aa65030.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
material/assets/javascripts/bundle.8aa65030.min.js.map
Normal file
8
material/assets/javascripts/bundle.8aa65030.min.js.map
Normal file
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
1
material/assets/stylesheets/main.e8d9bf0c.min.css
vendored
Normal file
1
material/assets/stylesheets/main.e8d9bf0c.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
material/assets/stylesheets/main.e8d9bf0c.min.css.map
Normal file
1
material/assets/stylesheets/main.e8d9bf0c.min.css.map
Normal file
File diff suppressed because one or more lines are too long
@ -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
18
material/overrides/assets/javascripts/bundle.90528257.min.js
vendored
Normal file
18
material/overrides/assets/javascripts/bundle.90528257.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
19
material/partials/tags.html
Normal file
19
material/partials/tags.html
Normal 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 %}
|
0
material/plugins/search/__init__.py
Normal file
0
material/plugins/search/__init__.py
Normal file
49
material/plugins/search/plugin.py
Normal file
49
material/plugins/search/plugin.py
Normal 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"]
|
0
material/plugins/tags/__init__.py
Normal file
0
material/plugins/tags/__init__.py
Normal file
117
material/plugins/tags/plugin.py
Normal file
117
material/plugins/tags/plugin.py
Normal 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
13167
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
8
setup.py
8
setup.py
@ -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
|
||||
|
@ -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"
|
||||
|
70
src/assets/javascripts/browser/script/index.ts
Normal file
70
src/assets/javascripts/browser/script/index.ts
Normal 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)
|
||||
)
|
||||
})
|
||||
}
|
@ -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)),
|
||||
|
216
src/assets/javascripts/components/content/code/_/index.ts
Normal file
216
src/assets/javascripts/components/content/code/_/index.ts
Normal 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 }))
|
||||
)
|
||||
})
|
||||
}
|
@ -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"
|
||||
|
350
src/assets/javascripts/components/content/code/mermaid/index.css
Normal file
350
src/assets/javascripts/components/content/code/mermaid/index.css
Normal 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);
|
||||
}
|
122
src/assets/javascripts/components/content/code/mermaid/index.ts
Normal file
122
src/assets/javascripts/components/content/code/mermaid/index.ts
Normal 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 })
|
||||
)
|
||||
}
|
@ -23,4 +23,5 @@
|
||||
export * from "./clipboard"
|
||||
export * from "./instant"
|
||||
export * from "./search"
|
||||
export * from "./sitemap"
|
||||
export * from "./version"
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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 }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
104
src/assets/javascripts/integrations/sitemap/index.ts
Normal file
104
src/assets/javascripts/integrations/sitemap/index.ts
Normal 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))
|
||||
)
|
||||
}
|
||||
}
|
@ -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]) => {
|
||||
|
@ -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}
|
||||
|
@ -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";
|
||||
|
@ -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}) {
|
||||
|
43
src/assets/stylesheets/main/integrations/_mermaid.scss
Normal file
43
src/assets/stylesheets/main/integrations/_mermaid.scss
Normal 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;
|
||||
}
|
65
src/assets/stylesheets/main/layout/_tag.scss
Normal file
65
src/assets/stylesheets/main/layout/_tag.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -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
41
src/partials/tags.html
Normal 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 %}
|
0
src/plugins/search/__init__.py
Normal file
0
src/plugins/search/__init__.py
Normal file
49
src/plugins/search/plugin.py
Normal file
49
src/plugins/search/plugin.py
Normal 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"]
|
0
src/plugins/tags/__init__.py
Normal file
0
src/plugins/tags/__init__.py
Normal file
117
src/plugins/tags/plugin.py
Normal file
117
src/plugins/tags/plugin.py
Normal 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)
|
@ -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()
|
||||
|
@ -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
28
typings/mermaid/index.d.ts
vendored
Normal 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
|
||||
|
Loading…
Reference in New Issue
Block a user