mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-11-27 17:00:54 +01:00
Merge pull request #5683 from squidfunk/merge/piri-piri
Features tied to 'Piri Piri' funding goal
This commit is contained in:
commit
46ca6c5418
67
CHANGELOG
67
CHANGELOG
@ -1,3 +1,52 @@
|
||||
mkdocs-material-9.2.0 (2023-08-21)
|
||||
|
||||
Additions and improvements
|
||||
|
||||
* Added blogging support via built-in blog plugin
|
||||
* Added support for Chinese language segmentaiton in search plugin
|
||||
* Added support for adding custom dates to blog posts
|
||||
* Added support for paginating archive and category pages
|
||||
* Added support for annotations (outside of code blocks)
|
||||
* Added support for navigation icons
|
||||
* Added support for navigation pruning
|
||||
* Added support for navigation status
|
||||
* Added support for customizing site icons
|
||||
* Added support for customizing (code) annotation icons
|
||||
* Added focus outline to admonitions and details
|
||||
* Added prompt for bug report name to info plugin
|
||||
* Added Luxembourgish translations
|
||||
* Improved rendering of (code) annotation markers
|
||||
* Improved print styles for (code) annotations
|
||||
* Improved customizability of navigation tabs
|
||||
* Improved interop of plugins with external tools like mike
|
||||
* Improved interop of blog plugin with awesome pages plugin
|
||||
* Improved header partial by moving buttons into separate partials
|
||||
* Improved clarity of site_url warning in social plugin
|
||||
* Improved blog plugin to automatically setup directory structure
|
||||
* Switched info plugin to importlib to mitigate deprecations
|
||||
* Automatically download ResizeObserver polyfill when necessary
|
||||
* Automatically add iframe-worker polyfill when necessary in offline plugin
|
||||
* Automatically focus and bring up keyboard on touch devices
|
||||
* Updated Serbo-Croatian translations
|
||||
* Updated MkDocs to 1.5.2
|
||||
|
||||
Removals
|
||||
|
||||
* Removed Universal Analytics integration
|
||||
* Removed ancient polyfills to reduce size of bundled JavaScript by 20%
|
||||
* Removed necessity for Array.flat and Array.flatMap polyfill
|
||||
* Removed announcement bar button when JavaScript is not available
|
||||
|
||||
Fixes
|
||||
|
||||
* Fixed rendering of tags when announcement bar is present
|
||||
* Fixed tags plugin rendering pages excluded by other plugins
|
||||
* Fixed #5132: Blog plugin requires nav entry in mkdocs.yml
|
||||
* Fixed #5599: Insufficient contrast for default link color
|
||||
* Fixed #5715: Blog plugin missing integrated table of contents in pagination
|
||||
* Fixed #5806: Version selector not hoverable on some Android devices
|
||||
* Fixed #5826: Blog post drafts with tags show up in tags index
|
||||
|
||||
mkdocs-material-9.1.21+insiders-4.39.0 (2023-08-01)
|
||||
|
||||
* Added support for hoisting theme media files when building projects
|
||||
@ -46,6 +95,24 @@ mkdocs-material-9.1.18+insiders-4.37.0 (2023-07-07)
|
||||
* Fixed jumping of content tabs anchor links when instant loading is enabled
|
||||
* Fixed #5676: Optimize plugin doesn't check for pngquant
|
||||
|
||||
mkdocs-material-9.2.0 (2023-07-06)
|
||||
|
||||
* Added blogging support via built-in blog plugin
|
||||
* Added Chinese language support to built-in search plugin
|
||||
* Added support for annotations (outside of code blocks)
|
||||
* Added support for navigation icons
|
||||
* Added support for navigation pruning
|
||||
* Added support for navigation status
|
||||
* Added support for customizing site icons
|
||||
* Improved rendering of (code) annotation markers
|
||||
* Improved print styles for (code) annotations
|
||||
* Improved customizability of navigation tabs
|
||||
* Removed Universal Analytics integration
|
||||
* Removed ancient polyfills to reduce size of bundled JavaScript by 20%
|
||||
* Removed necessity for Array.flat and Array.flatMap polyfill
|
||||
* Removed announcement bar button when JavaScript is not available
|
||||
* Automatically download ResizeObserver polyfill when necessary
|
||||
|
||||
mkdocs-material-9.1.18 (2023-07-03)
|
||||
|
||||
* Updated Danish translations
|
||||
|
@ -47,6 +47,8 @@ check the distribution of browser types and versions among your users.
|
||||
[open an issue]: https://github.com/squidfunk/mkdocs-material/issues/new/choose
|
||||
[caniuse.com]: https://caniuse.com/
|
||||
[:is pseudo selector]: https://caniuse.com/css-matches-pseudo
|
||||
[browser support]: #supported-browsers
|
||||
[built-in privacy plugin]: setup/ensuring-data-privacy.md#built-in-privacy-plugin
|
||||
|
||||
## Other browsers
|
||||
|
||||
|
@ -2,6 +2,55 @@
|
||||
|
||||
## Material for MkDocs
|
||||
|
||||
### 9.2.0 <small>July 6, 2023</small> { id="9.2.0" }
|
||||
|
||||
__Additions and improvements__
|
||||
|
||||
- Added blogging support via built-in blog plugin
|
||||
- Added support for Chinese language segmentaiton in search plugin
|
||||
- Added support for adding custom dates to blog posts
|
||||
- Added support for paginating archive and category pages
|
||||
- Added support for annotations (outside of code blocks)
|
||||
- Added support for navigation icons
|
||||
- Added support for navigation pruning
|
||||
- Added support for navigation status
|
||||
- Added support for customizing site icons
|
||||
- Added support for customizing (code) annotation icons
|
||||
- Added focus outline to admonitions and details
|
||||
- Added prompt for bug report name to info plugin
|
||||
- Added Luxembourgish translations
|
||||
- Improved rendering of (code) annotation markers
|
||||
- Improved print styles for (code) annotations
|
||||
- Improved customizability of navigation tabs
|
||||
- Improved interop of plugins with external tools like mike
|
||||
- Improved interop of blog plugin with awesome pages plugin
|
||||
- Improved header partial by moving buttons into separate partials
|
||||
- Improved clarity of `site_url` warning in social plugin
|
||||
- Improved blog plugin to automatically setup directory structure
|
||||
- Switched info plugin to `importlib` to mitigate deprecations
|
||||
- Automatically download ResizeObserver polyfill when necessary
|
||||
- Automatically add iframe-worker polyfill when necessary in offline plugin
|
||||
- Automatically focus and bring up keyboard on touch devices
|
||||
- Updated Serbo-Croatian translations
|
||||
- Updated MkDocs to 1.5.2
|
||||
|
||||
__Removals__
|
||||
|
||||
- Removed Universal Analytics integration
|
||||
- Removed ancient polyfills to reduce size of bundled JavaScript by 20%
|
||||
- Removed necessity for `Array.flat` and `Array.flatMap` polyfill
|
||||
- Removed announcement bar button when JavaScript is not available
|
||||
|
||||
__Fixes__
|
||||
|
||||
- Fixed rendering of tags when announcement bar is present
|
||||
- Fixed tags plugin rendering pages excluded by other plugins
|
||||
- Fixed #5132: Blog plugin requires `nav` entry in `mkdocs.yml`
|
||||
- Fixed #5599: Insufficient contrast for default link color
|
||||
- Fixed #5715: Blog plugin missing integrated table of contents in pagination
|
||||
- Fixed #5806: Version selector not hoverable on some Android devices
|
||||
- Fixed #5826: Blog post drafts with tags show up in tags index
|
||||
|
||||
### 9.1.21 <small>July 27, 2023</small> { id="9.1.20" }
|
||||
|
||||
- Fixed MkDocs 1.4 compat issue in social plugin (9.1.20 regression)
|
||||
|
@ -100,6 +100,7 @@ assets may also be put in the `overrides` directory:
|
||||
│ │ └─ analytics.html # Analytics setup
|
||||
│ ├─ languages/ # Translation languages
|
||||
│ ├─ actions.html # Actions
|
||||
│ ├─ alternate.html # Site language selector
|
||||
│ ├─ comments.html # Comment system (empty by default)
|
||||
│ ├─ consent.html # Consent
|
||||
│ ├─ content.html # Page content
|
||||
@ -113,6 +114,7 @@ assets may also be put in the `overrides` directory:
|
||||
│ ├─ nav.html # Main navigation
|
||||
│ ├─ nav-item.html # Main navigation item
|
||||
│ ├─ pagination.html # Pagination (used for blog)
|
||||
│ ├─ palette.html # Color palette toggle
|
||||
│ ├─ post.html # Blog post excerpt
|
||||
│ ├─ search.html # Search interface
|
||||
│ ├─ social.html # Social links
|
||||
|
@ -31,11 +31,51 @@ See additional configuration options:
|
||||
[Markdown in HTML]: ../setup/extensions/python-markdown.md#markdown-in-html
|
||||
[SuperFences]: ../setup/extensions/python-markdown-extensions.md#superfences
|
||||
|
||||
### Annotation icons
|
||||
|
||||
[:octicons-tag-24: 9.2.0][Annotation icons support]
|
||||
|
||||
The annotation icon can be changed to any icon bundled with the theme, or even
|
||||
a [custom icon], e.g. to material/arrow-right-circle:. Simply add the following
|
||||
lines to `mkdocs.yml`:
|
||||
|
||||
``` yaml
|
||||
theme:
|
||||
icon:
|
||||
annotation: material/arrow-right-circle # (1)!
|
||||
```
|
||||
|
||||
1. Enter a few keywords to find the perfect icon using our [icon search] and
|
||||
click on the shortcode to copy it to your clipboard:
|
||||
|
||||
<div class="mdx-iconsearch" data-mdx-component="iconsearch">
|
||||
<input class="md-input md-input--stretch mdx-iconsearch__input" placeholder="Search icon" data-mdx-component="iconsearch-query" value="material circle" />
|
||||
<div class="mdx-iconsearch-result" data-mdx-component="iconsearch-result" data-mdx-mode="file">
|
||||
<div class="mdx-iconsearch-result__meta"></div>
|
||||
<ol class="mdx-iconsearch-result__list"></ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Some popular choices:
|
||||
|
||||
- :material-plus-circle: - `material/plus-circle`
|
||||
- :material-circle-medium: - `material/circle-medium`
|
||||
- :material-record-circle: - `material/record-circle`
|
||||
- :material-arrow-right-circle: - `material/arrow-right-circle`
|
||||
- :material-arrow-right-circle-outline: - `material/arrow-right-circle-outline`
|
||||
- :material-chevron-right-circle: - `material/chevron-right-circle`
|
||||
- :material-star-four-points-circle: - `material/star-four-points-circle`
|
||||
- :material-plus-circle-outline: - `material/plus-circle-outline`
|
||||
|
||||
[Annotation icons support]: https://github.com/squidfunk/mkdocs-material/releases/tag/9.2.0
|
||||
[custom icon]: ../setup/changing-the-logo-and-icons.md#additional-icons
|
||||
[icon search]: icons-emojis.md#search
|
||||
|
||||
## Usage
|
||||
|
||||
### Using annotations
|
||||
|
||||
[:octicons-tag-24: 9.2.0b0][Annotation support] ·
|
||||
[:octicons-tag-24: 9.2.0][Annotation support] ·
|
||||
:octicons-beaker-24: Experimental
|
||||
|
||||
Annotations consist of two parts: a marker, which can be placed anywhere in
|
||||
@ -64,7 +104,7 @@ Note that the `annotate` class must only be added to the outermost block. All
|
||||
nested elements can use the same list to define annotations, except when
|
||||
annotations are nested themselves.
|
||||
|
||||
[Annotation support]: https://github.com/squidfunk/mkdocs-material/releases/tag/9.2.0b0
|
||||
[Annotation support]: https://github.com/squidfunk/mkdocs-material/releases/tag/9.2.0
|
||||
|
||||
#### in annotations
|
||||
|
||||
|
@ -136,6 +136,21 @@ but it's always possible to use the [Markdown in HTML] extension with literal
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
|
||||
Another option is to use [Markdown captions](https://github.com/Evidlo/markdown_captions) extension
|
||||
which converts images with alt text to \<figure\> with \<figcaption\>.
|
||||
|
||||
``` html title="Image with caption"
|
||||
![Image caption](https://dummyimage.com/600x400/){ width="300" }
|
||||
```
|
||||
|
||||
<div class="result">
|
||||
<figure>
|
||||
<img src="https://dummyimage.com/600x400/f5f5f5/aaaaaa?text=–%20Image%20–" width="300" />
|
||||
<figcaption>Image caption</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
### Image lazy-loading
|
||||
|
||||
Modern browsers provide [native support for lazy-loading images][lazy-loading]
|
||||
|
@ -121,7 +121,7 @@ description: Nullam urna elit, malesuada eget finibus ut, ac tortor. # (1)!
|
||||
|
||||
### Setting the page `icon`
|
||||
|
||||
[:octicons-tag-24: 9.2.0b0][Page icon support] ·
|
||||
[:octicons-tag-24: 9.2.0][Page icon support] ·
|
||||
:octicons-beaker-24: Experimental
|
||||
|
||||
An icon can be assigned to each page, which is then rendered as part of the
|
||||
@ -149,14 +149,14 @@ icon: material/emoticon-happy # (1)!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
[Page icon support]: https://github.com/squidfunk/mkdocs-material/releases/tag/9.2.0b0
|
||||
[Page icon support]: https://github.com/squidfunk/mkdocs-material/releases/tag/9.2.0
|
||||
[Insiders]: ../insiders/index.md
|
||||
[icon search]: icons-emojis.md#search
|
||||
[navigation tabs]: ../setup/setting-up-navigation.md#navigation-tabs
|
||||
|
||||
### Setting the page `status`
|
||||
|
||||
[:octicons-tag-24: 9.2.0b0][Page status support] ·
|
||||
[:octicons-tag-24: 9.2.0][Page status support] ·
|
||||
:octicons-beaker-24: Experimental
|
||||
|
||||
A status can be assigned to each page, which is then displayed as part of the
|
||||
@ -197,7 +197,7 @@ The following status identifiers are currently supported:
|
||||
- :material-alert-decagram: – `new`
|
||||
- :material-trash-can: – `deprecated`
|
||||
|
||||
[Page status support]: https://github.com/squidfunk/mkdocs-material/releases/tag/9.2.0b0
|
||||
[Page status support]: https://github.com/squidfunk/mkdocs-material/releases/tag/9.2.0
|
||||
|
||||
### Setting the page `subtitle`
|
||||
|
||||
|
@ -544,6 +544,21 @@
|
||||
"markdownDescription": "https://facelessuser.github.io/pymdown-extensions/extensions/snippets/#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"url_max_size": {
|
||||
"markdownDescription": "https://facelessuser.github.io/pymdown-extensions/extensions/snippets/#options",
|
||||
"type": "integer",
|
||||
"default": 33554432
|
||||
},
|
||||
"url_timeout": {
|
||||
"markdownDescription": "https://facelessuser.github.io/pymdown-extensions/extensions/snippets/#options",
|
||||
"type": "number",
|
||||
"default": 10.0
|
||||
},
|
||||
"url_request_headers": {
|
||||
"markdownDescription": "https://facelessuser.github.io/pymdown-extensions/extensions/snippets/#options",
|
||||
"type": "object",
|
||||
"default": {}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
@ -132,6 +132,22 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"annotate": {
|
||||
"title": "Custom selectors for annotations",
|
||||
"markdownDescription": "https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#custom-selectors",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"title": "Custom selector",
|
||||
"markdownDescription": "https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#custom-selectors",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^\\.\\w+"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"consent": {
|
||||
"title": "Cookie consent",
|
||||
"markdownDescription": "https://squidfunk.github.io/mkdocs-material/setup/ensuring-data-privacy/#cookie-consent",
|
||||
|
@ -33,6 +33,12 @@
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"post_dir": {
|
||||
"title": "Blog posts directory",
|
||||
"markdownDescription": "https://squidfunk.github.io/mkdocs-material/setup/setting-up-a-blog/#+blog.post_dir",
|
||||
"type": "string",
|
||||
"default": "\"{blog}/posts\""
|
||||
},
|
||||
"post_date_format": {
|
||||
"title": "Format string for post dates",
|
||||
"markdownDescription": "https://squidfunk.github.io/mkdocs-material/setup/setting-up-a-blog/#+blog.post_date_format",
|
||||
@ -329,7 +335,7 @@
|
||||
"title": "Authors file",
|
||||
"markdownDescription": "https://squidfunk.github.io/mkdocs-material/setup/setting-up-a-blog/#+blog.authors_file",
|
||||
"type": "string",
|
||||
"default": ".authors.yml"
|
||||
"default": "\"{blog}/.authors.yml\""
|
||||
},
|
||||
"draft": {
|
||||
"title": "Render posts marked as drafts",
|
||||
|
@ -629,6 +629,13 @@
|
||||
"content.code.copy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Code selection button",
|
||||
"markdownDescription": "https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#code-selection-button",
|
||||
"enum": [
|
||||
"content.code.select"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Linked content tabs",
|
||||
"markdownDescription": "https://squidfunk.github.io/mkdocs-material/reference/content-tabs/#linked-content-tabs",
|
||||
|
@ -72,6 +72,41 @@ theme:
|
||||
[Favicon support]: https://github.com/squidfunk/mkdocs-material/releases/tag/0.1.0
|
||||
[Favicon default]: https://github.com/squidfunk/mkdocs-material/blob/master/material/assets/images/favicon.png
|
||||
|
||||
### Site icons
|
||||
|
||||
[:octicons-tag-24: 9.2.0][Site icon support]
|
||||
|
||||
Most icons you see on your site, such as navigation icons, can also be changed. For example,
|
||||
to change the navigation arrows in the footer, add the following lines to `mkdocs.yml`:
|
||||
|
||||
```yaml
|
||||
theme:
|
||||
icon:
|
||||
previous: fontawesome/solid/angle-left
|
||||
next: fontawesome/solid/angle-right
|
||||
```
|
||||
|
||||
The following is a complete list of customizable icons used by the theme:
|
||||
|
||||
| Icon name | Purpose |
|
||||
|:-------------|:------------------------------------------------------------------------------|
|
||||
| `logo` | See [Logo](#logo) |
|
||||
| `menu` | Open drawer |
|
||||
| `alternate` | Change language |
|
||||
| `search` | Search icon |
|
||||
| `share` | Share search |
|
||||
| `close` | Reset search, dismiss announcements |
|
||||
| `top` | Back-to-top button |
|
||||
| `edit` | Edit current page |
|
||||
| `view` | View page source |
|
||||
| `repo` | Repository icon |
|
||||
| `admonition` | See [Admonition icons](../reference/admonitions.md#admonition-icons) |
|
||||
| `tag` | See [Tag icons and identifiers](setting-up-tags.md#tag-icons-and-identifiers) |
|
||||
| `previous` | Previous page in footer, hide search on mobile |
|
||||
| `next` | Next page in footer |
|
||||
|
||||
[Site icon support]: https://github.com/squidfunk/mkdocs-material/releases/tag/9.2.0
|
||||
|
||||
## Customization
|
||||
|
||||
### Additional icons
|
||||
|
@ -19,7 +19,7 @@ __Check out our [blog], which is created with the new [built-in blog plugin]!__
|
||||
|
||||
### Built-in blog plugin
|
||||
|
||||
[:octicons-tag-24: 9.2.0b0][Blog plugin support] ·
|
||||
[:octicons-tag-24: 9.2.0][Blog plugin support] ·
|
||||
:octicons-cpu-24: Plugin ·
|
||||
:octicons-beaker-24: Experimental
|
||||
|
||||
@ -126,7 +126,7 @@ back here later for fine-tuning the output.__
|
||||
|
||||
---
|
||||
|
||||
[Blog plugin support]: https://github.com/squidfunk/mkdocs-material/releases/tag/9.2.0b0
|
||||
[Blog plugin support]: https://github.com/squidfunk/mkdocs-material/releases/tag/9.2.0
|
||||
[Insiders]: ../insiders/index.md
|
||||
[built-in plugins]: ../insiders/getting-started.md#built-in-plugins
|
||||
[this is configurable]: #+blog.blog_dir
|
||||
@ -138,6 +138,22 @@ back here later for fine-tuning the output.__
|
||||
|
||||
The following configuration options are available for posts:
|
||||
|
||||
[`post_dir`](#+blog.post_dir){ #+blog.post_dir }
|
||||
|
||||
: :octicons-milestone-24: Default: `{blog}/posts` – This option specifies
|
||||
the name of the folder in which the blog plugin should look for posts.
|
||||
The default settings assumes that the folder is called `posts`:
|
||||
|
||||
``` yaml
|
||||
plugins:
|
||||
- blog:
|
||||
post_dir: blog/posts
|
||||
```
|
||||
|
||||
The path must be defined relative to [`docs_dir`][docs_dir]. Note that the
|
||||
`{blog}` placeholder is replaced with the value specified in
|
||||
[`blog_dir`][this is configurable].
|
||||
|
||||
[`post_date_format`](#+blog.post_date_format){ #+blog.post_date_format }
|
||||
|
||||
: :octicons-milestone-24: Default: `long` – This option specifies the date
|
||||
@ -689,7 +705,7 @@ The following configuration options are available for index pagination:
|
||||
pagination_url_format: "{page}"
|
||||
```
|
||||
|
||||
[`pagination_template`](#+blog.pagination_template){ #+blog.pagination_template }
|
||||
[`pagination_format`](#+blog.pagination_format){ #+blog.pagination_format }
|
||||
|
||||
: :octicons-milestone-24: Default: `~2~` – This option specifies the format
|
||||
string that is provided to the [paginate] module, which allows to customize
|
||||
@ -700,7 +716,7 @@ The following configuration options are available for index pagination:
|
||||
``` yaml
|
||||
plugins:
|
||||
- blog:
|
||||
pagination_template: "~2~"
|
||||
pagination_format: "~2~"
|
||||
```
|
||||
|
||||
=== "1 2 3 .. n :material-chevron-right: :material-chevron-double-right:"
|
||||
@ -708,7 +724,7 @@ The following configuration options are available for index pagination:
|
||||
``` yaml
|
||||
plugins:
|
||||
- blog:
|
||||
pagination_template: "$link_first $link_previous ~2~ $link_next $link_last"
|
||||
pagination_format: "$link_first $link_previous ~2~ $link_next $link_last"
|
||||
```
|
||||
|
||||
=== "1 :material-chevron-right:"
|
||||
@ -716,7 +732,7 @@ The following configuration options are available for index pagination:
|
||||
``` yaml
|
||||
plugins:
|
||||
- blog:
|
||||
pagination_template: "$link_previous $page $link_next"
|
||||
pagination_format: "$link_previous $page $link_next"
|
||||
```
|
||||
|
||||
The [paginate] module exposes the following placeholders:
|
||||
@ -736,6 +752,18 @@ The following configuration options are available for index pagination:
|
||||
|
||||
[paginate]: https://pypi.org/project/paginate/
|
||||
|
||||
[`pagination_if_single_page`](#+blog.pagination_if_single_page){ #+blog.pagination_if_single_page }
|
||||
|
||||
: :octicons-milestone-24: Default: `false` – This option specifies whether
|
||||
the pagination should also be rendered if all posts fit into a single page.
|
||||
If you wish to always render pagination, add:
|
||||
|
||||
``` yaml
|
||||
plugins:
|
||||
- blog:
|
||||
pagination_if_single_page: true
|
||||
```
|
||||
|
||||
[`pagination_keep_content`](#+blog.pagination_keep_content){ #+blog.pagination_keep_content }
|
||||
|
||||
: :octicons-milestone-24: Default: `false` – This option specifies whether
|
||||
@ -767,10 +795,10 @@ The following configuration options are available for author info:
|
||||
|
||||
[`authors_file`](#+blog.authors_file){ #+blog.authors_file }
|
||||
|
||||
: :octicons-milestone-24: Default: `.authors.yml` – This option specifies the
|
||||
name of the file where the authors for your posts resides. The default
|
||||
settings assumes that the file is called `.authors.yml` (mind the `.` at
|
||||
the beginning):
|
||||
: :octicons-milestone-24: Default: `{blog}/.authors.yml` – This option
|
||||
specifies the name of the file where the authors for your posts resides.
|
||||
The default settings assumes that the file is called `.authors.yml`
|
||||
(mind the `.` at the beginning):
|
||||
|
||||
``` yaml
|
||||
plugins:
|
||||
@ -778,8 +806,9 @@ The following configuration options are available for author info:
|
||||
authors_file: blog/.authors.yml
|
||||
```
|
||||
|
||||
The path must be defined relative to [`docs_dir`][docs_dir].
|
||||
Also see the section on [adding authors].
|
||||
The path must be defined relative to [`docs_dir`][docs_dir]. Note that the
|
||||
`{blog}` placeholder is replaced with the value specified in
|
||||
[`blog_dir`][this is configurable]. Also see the section on [adding authors].
|
||||
|
||||
[adding authors]: #adding-authors
|
||||
|
||||
@ -840,8 +869,7 @@ The following configuration options are available for drafts:
|
||||
|
||||
### RSS
|
||||
|
||||
[:octicons-heart-fill-24:{ .mdx-heart } Sponsors only][Insiders]{ .mdx-insiders } ·
|
||||
[:octicons-tag-24: insiders-4.23.0][Insiders] ·
|
||||
[:octicons-tag-24: 9.2.0][Blog plugin support] ·
|
||||
[:octicons-cpu-24: Plugin][rss]
|
||||
|
||||
The [built-in blog plugin] integrates seamlessly with the [RSS plugin][rss],
|
||||
@ -993,8 +1021,22 @@ categories:
|
||||
output. [This behavior can be changed], e.g. for rendering drafts when
|
||||
building deploy previews.
|
||||
|
||||
2. You can use `date_updated` to signal when you updated a blog posts. The
|
||||
date will be rendered as part of the sidebar.
|
||||
2. If you wish to provide multiple dates, you can use the following syntax,
|
||||
allowing you to define a date when you last updated the blog post +
|
||||
further custom dates you can add to the template:
|
||||
|
||||
``` yaml
|
||||
---
|
||||
date:
|
||||
created: 2022-01-31
|
||||
updated: 2022-02-02
|
||||
---
|
||||
|
||||
# Hello world!
|
||||
```
|
||||
|
||||
Note that the creation date __must__ be set under `date.created`, as each
|
||||
blog post must have a creation date set.
|
||||
|
||||
When you spin up the [live preview server], you should be greeted by your first
|
||||
post! You'll also realize, that [archive] and [category] indexes have been
|
||||
@ -1359,14 +1401,10 @@ which means you can override all templates used for the blog by using
|
||||
|
||||
The following templates are added by the [built-in blog plugin]:
|
||||
|
||||
- [`blog.html`][blog.html] – Template for blog index
|
||||
- [`blog.html`][blog.html] – Template for blog, archive and category index
|
||||
- [`blog-post.html`][blog-post.html] – Template for blog post
|
||||
- [`blog-archive.html`][blog-archive.html] – Template for blog archive index
|
||||
- [`blog-category.html`][blog-category.html] – Template for blog category index
|
||||
|
||||
[theme extension]: ../customization.md#extending-the-theme
|
||||
|
||||
[blog.html]: https://github.com/squidfunk/mkdocs-material-insiders/blob/master/src/blog.html
|
||||
[blog-post.html]: https://github.com/squidfunk/mkdocs-material-insiders/blob/master/src/blog-post.html
|
||||
[blog-archive.html]: https://github.com/squidfunk/mkdocs-material-insiders/blob/master/src/blog-archive.html
|
||||
[blog-category.html]: https://github.com/squidfunk/mkdocs-material-insiders/blob/master/src/blog-category.html
|
||||
[blog-post.html]: https://github.com/squidfunk/mkdocs-material-insiders/blob/master/src/blog-post.htmlhtml
|
||||
|
@ -223,7 +223,7 @@ theme:
|
||||
|
||||
### Navigation pruning
|
||||
|
||||
[:octicons-tag-24: 9.2.0b0][Navigation pruning support] ·
|
||||
[:octicons-tag-24: 9.2.0][Navigation pruning support] ·
|
||||
:octicons-beaker-24: Experimental
|
||||
|
||||
When pruning is enabled, only the visible navigation items are included in the
|
||||
@ -245,7 +245,7 @@ This feature flag is especially useful for documentation sites with 100+ or even
|
||||
Navigation pruning will replace all expandable sections with links to the first
|
||||
page in that section (or the section index page).
|
||||
|
||||
[Navigation pruning support]: https://github.com/squidfunk/mkdocs-material/releases/tag/9.2.0b0
|
||||
[Navigation pruning support]: https://github.com/squidfunk/mkdocs-material/releases/tag/9.2.0
|
||||
[navigation.expand]: #navigation-expansion
|
||||
|
||||
### Section index pages
|
||||
|
@ -16,27 +16,21 @@ MkDocs natively integrates with [Google Analytics] and offers a customizable
|
||||
[:octicons-tag-24: 7.1.8][Google Analytics support] ·
|
||||
:octicons-milestone-24: Default: _none_
|
||||
|
||||
Material for MkDocs integrates with both, Google Analytics 4 and the now phasing
|
||||
out Universal Analytics. Depending on the given property prefix, add the
|
||||
Material for MkDocs integrates natively with Google Analytics 4[^1]. If you
|
||||
already set up Google Analytics and have a property, enable it by adding the
|
||||
following lines to `mkdocs.yml`:
|
||||
|
||||
=== ":material-google-analytics: Google Analytics 4"
|
||||
[^1]:
|
||||
Prior to Material for MkDocs 9.2.0, Universal Analytics was supported as
|
||||
well. However, since Universal Analytics has been sunset, this integration
|
||||
was removed in 9.2.0.
|
||||
|
||||
``` yaml
|
||||
extra:
|
||||
analytics:
|
||||
provider: google
|
||||
property: G-XXXXXXXXXX
|
||||
```
|
||||
|
||||
=== ":material-google-analytics: Universal Analytics"
|
||||
|
||||
``` yaml
|
||||
extra:
|
||||
analytics:
|
||||
provider: google
|
||||
property: UA-XXXXXXXX-X
|
||||
```
|
||||
``` yaml
|
||||
extra:
|
||||
analytics:
|
||||
provider: google
|
||||
property: G-XXXXXXXXXX
|
||||
```
|
||||
|
||||
[Google Analytics support]: https://github.com/squidfunk/mkdocs-material/releases/tag/7.1.8
|
||||
|
||||
@ -46,21 +40,11 @@ following lines to `mkdocs.yml`:
|
||||
understand how people use your documentation and what they expect to find.
|
||||
In order to enable site search tracking, the following steps are required:
|
||||
|
||||
=== ":material-google-analytics: Google Analytics 4"
|
||||
|
||||
1. Go to your Google Analytics __admin settings__
|
||||
2. Select the property for the respective tracking code
|
||||
3. Select the __data streams__ tab and click the corresponding URL
|
||||
4. Click the gear icon within the __enhanced measurement__ section
|
||||
5. Ensure that __site search__ is enabled
|
||||
|
||||
=== ":material-google-analytics: Universal Analytics"
|
||||
|
||||
1. Go to your Google Analytics __admin settings__
|
||||
2. Select the property for the respective tracking code
|
||||
3. Go to the __view settings__ tab
|
||||
4. Scroll down and enable __site search settings__
|
||||
5. Set the __query parameter__ to `q`
|
||||
1. Go to your Google Analytics __admin settings__
|
||||
2. Select the property for the respective tracking code
|
||||
3. Select the __data streams__ tab and click the corresponding URL
|
||||
4. Click the gear icon within the __enhanced measurement__ section
|
||||
5. Ensure that __site search__ is enabled
|
||||
|
||||
[site search]: setting-up-site-search.md
|
||||
|
||||
@ -102,9 +86,9 @@ extra:
|
||||
Both properties, `title` and `ratings`, are required. Note that it's allowed to
|
||||
define more than two ratings, e.g. to implement a 1-5 star rating. Since the
|
||||
feedback widget sends data to a third-party service, it is, of course, natively
|
||||
integrated with the [cookie consent] feature[^1].
|
||||
integrated with the [cookie consent] feature[^2].
|
||||
|
||||
[^1]:
|
||||
[^2]:
|
||||
If the user doesn't accept the `analytics` cookie, the feedback widget is
|
||||
not shown.
|
||||
|
||||
@ -114,46 +98,35 @@ integrated with the [cookie consent] feature[^1].
|
||||
[Google Analytics] that will quickly show you the worst- and best-rated
|
||||
pages of your project documentation.
|
||||
|
||||
=== ":material-google-analytics: Google Analytics 4"
|
||||
1. Go to your Google Analytics __dashboard__
|
||||
|
||||
1. Go to your Google Analytics __dashboard__
|
||||
2. Go to the __configure__ page on the left hand menu, then select
|
||||
__custom definitions__
|
||||
|
||||
2. Go to the __configure__ page on the left hand menu, then select
|
||||
__custom definitions__
|
||||
3. Click the __custom metrics__ tab and then __create custom metrics__,
|
||||
enter the following values:
|
||||
|
||||
3. Click the __custom metrics__ tab and then __create custom metrics__,
|
||||
enter the following values:
|
||||
* Metric name: Page helpful
|
||||
* Description: Was this page helpful?
|
||||
* Event parameter: `data`
|
||||
* Unit of measurement: Standard
|
||||
|
||||
* Metric name: Page helpful
|
||||
* Description: Was this page helpful?
|
||||
* Event parameter: `data`
|
||||
* Unit of measurement: Standard
|
||||
4. Go to the __explore__ page on the left hand menu, create a new
|
||||
__blank exploration__
|
||||
|
||||
4. Go to the __explore__ page on the left hand menu, create a new
|
||||
__blank exploration__
|
||||
5. Configure the report as follows:
|
||||
|
||||
5. Configure the report as follows:
|
||||
* Dimensions: Add `Event name` and `Page location`
|
||||
* Metrics: Add `Event count` and `Page helpful`
|
||||
(the custom metric created in step 3)
|
||||
* Rows: `Page location`
|
||||
* Values: Drag in both `Event count` and `Page helpful`
|
||||
* Filters: Add a new filter for
|
||||
`Event name / exactly matches / feedback`
|
||||
|
||||
* Dimensions: Add `Event name` and `Page location`
|
||||
* Metrics: Add `Event count` and `Page helpful`
|
||||
(the custom metric created in step 3)
|
||||
* Rows: `Page location`
|
||||
* Values: Drag in both `Event count` and `Page helpful`
|
||||
* Filters: Add a new filter for
|
||||
`Event name / exactly matches / feedback`
|
||||
!!! warning "Delay in data availability"
|
||||
|
||||
!!! warning "Delay in data availability"
|
||||
|
||||
The report may take 24 hours or longer to begin displaying data
|
||||
|
||||
=== ":material-google-analytics: Universal Analytics"
|
||||
|
||||
1. Go to your Google Analytics __dashboard__
|
||||
2. Open the __customization__ panel on the left and go to __custom reports__
|
||||
3. Create a __new custom report__ and set a custom __title__ and __name__
|
||||
4. Add `Avg. Value` and `Total Events` to __metric group__
|
||||
5. Add `Event Label` to __dimension drilldown__
|
||||
6. Add `Event Category` to __filters__ and filter for the value __feedback__
|
||||
The report may take 24 hours or longer to begin displaying data
|
||||
|
||||
Now, after you've saved the report and collected some feedback ratings,
|
||||
you'll have a list of all pages with the total number of ratings, and an
|
||||
@ -185,9 +158,9 @@ The following properties are available for each rating:
|
||||
|
||||
: :octicons-milestone-24: Default: _none_ · :octicons-alert-24: __Required__ –
|
||||
The value of this property is sent as a data value with the custom event
|
||||
that is transmitted to Google Analytics[^2] (or any custom integration).
|
||||
that is transmitted to Google Analytics[^3] (or any custom integration).
|
||||
|
||||
[^2]:
|
||||
[^3]:
|
||||
Note that for Google Analytics, the data value must be an integer.
|
||||
|
||||
[`note`](#+analytics.feedback.ratings.note){ #+analytics.feedback.ratings.note }
|
||||
|
@ -191,7 +191,7 @@ The following configuration options are supported:
|
||||
|
||||
#### Chinese language support
|
||||
|
||||
[:octicons-tag-24: 9.2.0b0][Chinese language support] ·
|
||||
[:octicons-tag-24: 9.2.0][Chinese language support] ·
|
||||
:octicons-beaker-24: Experimental
|
||||
|
||||
In order to add support for Chinese languages to the [built-in search plugin],
|
||||
@ -236,7 +236,7 @@ The following configuration options are available:
|
||||
User dictionaries can be used for tuning the segmenter to preserve
|
||||
technical terms.
|
||||
|
||||
[Chinese language support]: https://github.com/squidfunk/mkdocs-material/releases/tag/9.2.0b0
|
||||
[Chinese language support]: https://github.com/squidfunk/mkdocs-material/releases/tag/9.2.0
|
||||
[chinese search]: ../blog/posts/chinese-search-support.md
|
||||
[jieba]: https://pypi.org/project/jieba/
|
||||
[built-in search plugin]: #built-in-search-plugin
|
||||
|
18
material/.overrides/assets/javascripts/custom.149a65e0.min.js
vendored
Normal file
18
material/.overrides/assets/javascripts/custom.149a65e0.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
File diff suppressed because one or more lines are too long
@ -23,5 +23,5 @@
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{ 'assets/javascripts/custom.a678ee80.min.js' | url }}"></script>
|
||||
<script src="{{ 'assets/javascripts/custom.149a65e0.min.js' | url }}"></script>
|
||||
{% endblock %}
|
||||
|
@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
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.78eede0e.min.js
vendored
Normal file
29
material/assets/javascripts/bundle.78eede0e.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
material/assets/javascripts/bundle.78eede0e.min.js.map
Normal file
8
material/assets/javascripts/bundle.78eede0e.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
1
material/assets/stylesheets/main.0e669242.min.css
vendored
Normal file
1
material/assets/stylesheets/main.0e669242.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
material/assets/stylesheets/main.0e669242.min.css.map
Normal file
1
material/assets/stylesheets/main.0e669242.min.css.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
@ -27,8 +27,12 @@
|
||||
{% if page.next_page %}
|
||||
<link rel="next" href="{{ page.next_page.url | url }}">
|
||||
{% endif %}
|
||||
{% if "rss" in config.plugins %}
|
||||
<link rel="alternate" type="application/rss+xml" title="{{ lang.t('rss.created') }}" href="{{ 'feed_rss_created.xml' | url }}">
|
||||
<link rel="alternate" type="application/rss+xml" title="{{ lang.t('rss.updated') }}" href="{{ 'feed_rss_updated.xml' | url }}">
|
||||
{% endif %}
|
||||
<link rel="icon" href="{{ config.theme.favicon | url }}">
|
||||
<meta name="generator" content="mkdocs-{{ mkdocs_version }}, mkdocs-material-9.1.21">
|
||||
<meta name="generator" content="mkdocs-{{ mkdocs_version }}, mkdocs-material-9.2.0">
|
||||
{% endblock %}
|
||||
{% block htmltitle %}
|
||||
{% if page.meta and page.meta.title %}
|
||||
@ -40,18 +44,22 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.c439c5a1.min.css' | url }}">
|
||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.0e669242.min.css' | url }}">
|
||||
{% if config.theme.palette %}
|
||||
{% set palette = config.theme.palette %}
|
||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.ecc896b0.min.css' | url }}">
|
||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.85d0ee34.min.css' | url }}">
|
||||
{% endif %}
|
||||
{% include "partials/icons.html" %}
|
||||
{% endblock %}
|
||||
{% block libs %}{% endblock %}
|
||||
{% block libs %}
|
||||
{% for script in config.extra.polyfills %}
|
||||
{{ script | script_tag }}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
{% block fonts %}
|
||||
{% if config.theme.font != false %}
|
||||
{% set text = config.theme.font.text | d("Roboto", true) %}
|
||||
{% set code = config.theme.font.code | d("Roboto Mono", true) %}
|
||||
{% set text = config.theme.font.get("text", "Roboto") %}
|
||||
{% set code = config.theme.font.get("code", "Roboto Mono") %}
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family={{
|
||||
text | replace(' ', '+') + ':300,300i,400,400i,700,700i%7C' +
|
||||
@ -108,7 +116,8 @@
|
||||
<div class="md-banner__inner md-grid md-typeset">
|
||||
{% if "announce.dismiss" in features %}
|
||||
<button class="md-banner__button md-icon" aria-label="{{ lang.t('announce.dismiss') }}">
|
||||
{% include ".icons/material/close.svg" %}
|
||||
{% set icon = config.theme.icon.close or "material/close" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% block announce %}{% endblock %}
|
||||
@ -184,7 +193,8 @@
|
||||
</div>
|
||||
{% if "navigation.top" in features %}
|
||||
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
|
||||
{% include ".icons/material/arrow-up.svg" %}
|
||||
{% set icon = config.theme.icon.top or "material/arrow-up" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
{{ lang.t("top") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
@ -212,7 +222,7 @@
|
||||
"base": base_url,
|
||||
"features": features,
|
||||
"translations": {},
|
||||
"search": "assets/javascripts/workers/search.74e28a9f.min.js" | url
|
||||
"search": "assets/javascripts/workers/search.dfff1995.min.js" | url
|
||||
} -%}
|
||||
{%- if config.extra.version -%}
|
||||
{%- set _ = app.update({ "version": config.extra.version }) -%}
|
||||
@ -240,13 +250,9 @@
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="{{ 'assets/javascripts/bundle.220ee61c.min.js' | url }}"></script>
|
||||
<script src="{{ 'assets/javascripts/bundle.78eede0e.min.js' | url }}"></script>
|
||||
{% for script in config.extra_javascript %}
|
||||
{% if script.path %}
|
||||
{{ script | script_tag }}
|
||||
{% else %}
|
||||
<script src="{{ script | url }}"></script>
|
||||
{% endif %}
|
||||
{{ script | script_tag }}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
108
material/blog-post.html
Normal file
108
material/blog-post.html
Normal file
@ -0,0 +1,108 @@
|
||||
{#-
|
||||
This file was automatically generated - do not edit
|
||||
-#}
|
||||
{% extends "main.html" %}
|
||||
{% import "partials/nav-item.html" as item with context %}
|
||||
{% block container %}
|
||||
<div class="md-content md-content--post" data-md-component="content">
|
||||
<div class="md-sidebar md-sidebar--post" data-md-component="sidebar" data-md-type="navigation">
|
||||
<div class="md-sidebar__scrollwrap">
|
||||
<div class="md-sidebar__inner md-post">
|
||||
<nav class="md-nav">
|
||||
<div class="md-post__back">
|
||||
<div class="md-nav__title md-nav__container">
|
||||
<a href="{{ page.parent.url | url }}" class="md-nav__link">
|
||||
{% include ".icons/material/arrow-left.svg" %}
|
||||
<span class="md-ellipsis">
|
||||
{{ lang.t("blog.index") }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if page.authors %}
|
||||
<div class="md-post__authors md-typeset">
|
||||
{% for author in page.authors %}
|
||||
<div class="md-profile md-post__profile">
|
||||
<span class="md-author md-author--long">
|
||||
<img src="{{ author.avatar }}" alt="{{ author.name }}">
|
||||
</span>
|
||||
<span class="md-profile__description">
|
||||
<strong>{{ author.name }}</strong><br>
|
||||
{{ author.description }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<ul class="md-post__meta md-nav__list">
|
||||
<li class="md-nav__item md-nav__title">
|
||||
<div class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
{{ lang.t("blog.meta") }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="md-nav__item">
|
||||
<div class="md-nav__link">
|
||||
{% include ".icons/material/calendar.svg" %}
|
||||
<time datetime="{{ page.config.date.created }}" class="md-ellipsis">
|
||||
{{- page.config.date.created | date -}}
|
||||
</time>
|
||||
</div>
|
||||
</li>
|
||||
{% if page.config.date.updated %}
|
||||
<li class="md-nav__item">
|
||||
<div class="md-nav__link">
|
||||
{% include ".icons/material/calendar-clock.svg" %}
|
||||
<time datetime="{{ page.config.date.updated }}" class="md-ellipsis">
|
||||
{{- page.config.date.updated | date -}}
|
||||
</time>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if page.categories %}
|
||||
<li class="md-nav__item">
|
||||
<div class="md-nav__link">
|
||||
{% include ".icons/material/bookshelf.svg" %}
|
||||
<span class="md-ellipsis">
|
||||
{{ lang.t("blog.categories.in") }}
|
||||
{% for category in page.categories %}
|
||||
<a href="{{ category.url | url }}">
|
||||
{{- category.title -}}
|
||||
</a>
|
||||
{%- if loop.revindex > 1 %}, {% endif -%}
|
||||
{% endfor -%}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if page.config.readtime %}
|
||||
{% set time = page.config.readtime %}
|
||||
<li class="md-nav__item">
|
||||
<div class="md-nav__link">
|
||||
{% include ".icons/material/clock-outline.svg" %}
|
||||
<span class="md-ellipsis">
|
||||
{% if time == 1 %}
|
||||
{{ lang.t("readtime.one") }}
|
||||
{% else %}
|
||||
{{ lang.t("readtime.other") | replace("#", time) }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% if "toc.integrate" in features %}
|
||||
{% include "partials/toc.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<article class="md-content__inner md-typeset">
|
||||
{% block content %}
|
||||
{% include "partials/content.html" %}
|
||||
{% endblock %}
|
||||
</article>
|
||||
</div>
|
||||
{% endblock %}
|
21
material/blog.html
Normal file
21
material/blog.html
Normal file
@ -0,0 +1,21 @@
|
||||
{#-
|
||||
This file was automatically generated - do not edit
|
||||
-#}
|
||||
{% extends "main.html" %}
|
||||
{% block container %}
|
||||
<div class="md-content" data-md-component="content">
|
||||
<div class="md-content__inner">
|
||||
<header class="md-typeset">
|
||||
{{ page.content }}
|
||||
</header>
|
||||
{% for post in posts %}
|
||||
{% include "partials/post.html" %}
|
||||
{% endfor %}
|
||||
{% if pagination %}
|
||||
{% block pagination %}
|
||||
{% include "partials/pagination.html" %}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -38,8 +38,8 @@ font:
|
||||
# Fixed-width font for code listings
|
||||
code: Roboto Mono
|
||||
|
||||
# From Material 5.x on, icons are inlined into the HTML and CSS as SVGs. Some
|
||||
# icons that are part of the HTML can be configured and replaced
|
||||
# From Material 5.x on, icons are inlined into the HTML and CSS as SVGs.
|
||||
# Icons that are part of the HTML can be configured and replaced
|
||||
icon:
|
||||
|
||||
# Favicon to be rendered
|
||||
|
22
material/partials/alternate.html
Normal file
22
material/partials/alternate.html
Normal file
@ -0,0 +1,22 @@
|
||||
{#-
|
||||
This file was automatically generated - do not edit
|
||||
-#}
|
||||
<div class="md-header__option">
|
||||
<div class="md-select">
|
||||
{% set icon = config.theme.icon.alternate or "material/translate" %}
|
||||
<button class="md-header__button md-icon" aria-label="{{ lang.t('select.language') }}">
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</button>
|
||||
<div class="md-select__inner">
|
||||
<ul class="md-select__list">
|
||||
{% for alt in config.extra.alternate %}
|
||||
<li class="md-select__item">
|
||||
<a href="{{ alt.link | url }}" hreflang="{{ alt.lang }}" class="md-select__link">
|
||||
{{ alt.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,7 +1,7 @@
|
||||
{#-
|
||||
This file was automatically generated - do not edit
|
||||
-#}
|
||||
{% if "material/tags" in config.plugins %}
|
||||
{% if "material/tags" in config.plugins and tags %}
|
||||
{% include "partials/tags.html" %}
|
||||
{% endif %}
|
||||
{% include "partials/actions.html" %}
|
||||
|
@ -12,7 +12,8 @@
|
||||
{% set direction = lang.t("footer.previous") %}
|
||||
<a href="{{ page.previous_page.url | url }}" class="md-footer__link md-footer__link--prev" aria-label="{{ direction }}: {{ page.previous_page.title | e }}" rel="prev">
|
||||
<div class="md-footer__button md-icon">
|
||||
{% include ".icons/material/arrow-left.svg" %}
|
||||
{% set icon = config.theme.icon.previous or "material/arrow-left" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</div>
|
||||
<div class="md-footer__title">
|
||||
<span class="md-footer__direction">
|
||||
@ -36,7 +37,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="md-footer__button md-icon">
|
||||
{% include ".icons/material/arrow-right.svg" %}
|
||||
{% set icon = config.theme.icon.next or "material/arrow-right" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -13,7 +13,8 @@
|
||||
{% include "partials/logo.html" %}
|
||||
</a>
|
||||
<label class="md-header__button md-icon" for="__drawer">
|
||||
{% include ".icons/material/menu" ~ ".svg" %}
|
||||
{% set icon = config.theme.icon.menu or "material/menu" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</label>
|
||||
<div class="md-header__title" data-md-component="header-title">
|
||||
<div class="md-header__ellipsis">
|
||||
@ -35,45 +36,16 @@
|
||||
</div>
|
||||
{% if config.theme.palette %}
|
||||
{% if not config.theme.palette is mapping %}
|
||||
<form class="md-header__option" data-md-component="palette">
|
||||
{% for option in config.theme.palette %}
|
||||
{% set scheme = option.scheme | d("default", true) %}
|
||||
{% set primary = option.primary | d("indigo", true) %}
|
||||
{% set accent = option.accent | d("indigo", true) %}
|
||||
<input class="md-option" data-md-color-media="{{ option.media }}" data-md-color-scheme="{{ scheme | replace(' ', '-') }}" data-md-color-primary="{{ primary | replace(' ', '-') }}" data-md-color-accent="{{ accent | replace(' ', '-') }}" {% if option.toggle %} aria-label="{{ option.toggle.name }}" {% else %} aria-hidden="true" {% endif %} type="radio" name="__palette" id="__palette_{{ loop.index }}">
|
||||
{% if option.toggle %}
|
||||
<label class="md-header__button md-icon" title="{{ option.toggle.name }}" for="__palette_{{ loop.index0 or loop.length }}" hidden>
|
||||
{% include ".icons/" ~ option.toggle.icon ~ ".svg" %}
|
||||
</label>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</form>
|
||||
{% include "partials/palette.html" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if config.extra.alternate %}
|
||||
<div class="md-header__option">
|
||||
<div class="md-select">
|
||||
{% set icon = config.theme.icon.alternate or "material/translate" %}
|
||||
<button class="md-header__button md-icon" aria-label="{{ lang.t('select.language') }}">
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</button>
|
||||
<div class="md-select__inner">
|
||||
<ul class="md-select__list">
|
||||
{% for alt in config.extra.alternate %}
|
||||
<li class="md-select__item">
|
||||
<a href="{{ alt.link | url }}" hreflang="{{ alt.lang }}" class="md-select__link">
|
||||
{{ alt.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "partials/alternate.html" %}
|
||||
{% endif %}
|
||||
{% if "material/search" in config.plugins %}
|
||||
<label class="md-header__button md-icon" for="__search">
|
||||
{% include ".icons/material/magnify.svg" %}
|
||||
{% set icon = config.theme.icon.search or "material/magnify" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</label>
|
||||
{% include "partials/search.html" %}
|
||||
{% endif %}
|
||||
|
@ -15,6 +15,18 @@
|
||||
{% set _ = style.append("}\x3c/style\x3e") %}
|
||||
{{ style | join }}
|
||||
{% endif %}
|
||||
{% if config.theme.icon.annotation %}
|
||||
{% set style = ["\x3cstyle\x3e:root{"] %}
|
||||
{% import ".icons/" ~ config.theme.icon.annotation ~ ".svg" as icon %}
|
||||
{% set _ = style.append(
|
||||
"--md-annotation-icon:" ~
|
||||
"url('data:image/svg+xml;charset=utf-8," ~
|
||||
icon | replace("\n", "") ~
|
||||
"');"
|
||||
) %}
|
||||
{% set _ = style.append("}\x3c/style\x3e") %}
|
||||
{{ style | join }}
|
||||
{% endif %}
|
||||
{% if config.theme.icon.tag %}
|
||||
{% set style = ["\x3cstyle\x3e"] %}
|
||||
{% for type, icon in config.theme.icon.tag.items() %}
|
||||
|
@ -4,8 +4,4 @@
|
||||
{% if config.extra.analytics %}
|
||||
{% set property = config.extra.analytics.property | d("", true) %}
|
||||
{% endif %}
|
||||
{% if property.startswith("G-") %}
|
||||
<script id="__analytics">function __md_analytics(){function n(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],n("js",new Date),n("config","{{ property }}"),document.addEventListener("DOMContentLoaded",function(){document.forms.search&&document.forms.search.query.addEventListener("blur",function(){this.value&&n("event","search",{search_term:this.value})}),document$.subscribe(function(){var a=document.forms.feedback;if(void 0!==a)for(var e of a.querySelectorAll("[type=submit]"))e.addEventListener("click",function(e){e.preventDefault();var t=document.location.pathname,e=this.getAttribute("data-md-value");n("event","feedback",{page:t,data:e}),a.firstElementChild.disabled=!0;e=a.querySelector(".md-feedback__note [data-md-value='"+e+"']");e&&(e.hidden=!1)}),a.hidden=!1}),location$.subscribe(function(e){n("config","{{ property }}",{page_path:e.pathname})})});var e=document.createElement("script");e.async=!0,e.src="https://www.googletagmanager.com/gtag/js?id={{ property }}",document.getElementById("__analytics").insertAdjacentElement("afterEnd",e)}</script>
|
||||
{% elif property.startswith("UA-") %}
|
||||
<script id="__analytics">function __md_analytics(){window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)},ga.l=+new Date,ga("create","{{ property }}","auto"),ga("set","anonymizeIp",!0),ga("send","pageview"),document.addEventListener("DOMContentLoaded",function(){document.forms.search&&document.forms.search.query.addEventListener("blur",function(){var e;this.value&&(e=document.location.pathname,ga("send","pageview",e+"?q="+this.value))}),document$.subscribe(function(){var a=document.forms.feedback;if(void 0!==a)for(var e of a.querySelectorAll("[type=submit]"))e.addEventListener("click",function(e){e.preventDefault();var t=document.location.pathname,e=this.getAttribute("data-md-value");ga("send","event","feedback","click",t,e),a.firstElementChild.disabled=!0;e=a.querySelector(".md-feedback__note [data-md-value='"+e+"']");e&&(e.hidden=!1)}),a.hidden=!1}),location$.subscribe(function(e){ga("send","pageview",e.pathname)})});var e=document.createElement("script");e.async=!0,e.src="https://www.google-analytics.com/analytics.js",document.getElementById("__analytics").insertAdjacentElement("afterEnd",e)}</script>
|
||||
{% endif %}
|
||||
<script id="__analytics">function __md_analytics(){function n(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],n("js",new Date),n("config","{{ property }}"),document.addEventListener("DOMContentLoaded",function(){document.forms.search&&document.forms.search.query.addEventListener("blur",function(){this.value&&n("event","search",{search_term:this.value})}),document$.subscribe(function(){var a=document.forms.feedback;if(void 0!==a)for(var e of a.querySelectorAll("[type=submit]"))e.addEventListener("click",function(e){e.preventDefault();var t=document.location.pathname,e=this.getAttribute("data-md-value");n("event","feedback",{page:t,data:e}),a.firstElementChild.disabled=!0;e=a.querySelector(".md-feedback__note [data-md-value='"+e+"']");e&&(e.hidden=!1)}),a.hidden=!1}),location$.subscribe(function(e){n("config","{{ property }}",{page_path:e.pathname})})});var e=document.createElement("script");e.async=!0,e.src="https://www.googletagmanager.com/gtag/js?id={{ property }}",document.getElementById("__analytics").insertAdjacentElement("afterEnd",e)}</script>
|
||||
|
@ -3,4 +3,4 @@
|
||||
-#}
|
||||
{% import "partials/languages/" ~ config.theme.language ~ ".html" as lang %}
|
||||
{% import "partials/languages/en.html" as fallback %}
|
||||
{% macro t(key) %}{{ lang.t(key) or fallback.t(key) }}{% endmacro %}
|
||||
{% macro t(key) %}{{ lang.t(key) or fallback.t(key) or key }}{% endmacro %}
|
||||
|
@ -28,7 +28,7 @@
|
||||
"meta.source": "Source",
|
||||
"nav": "Navigation",
|
||||
"readtime.one": "1 min de lecture",
|
||||
"readtime.other": "# min lues",
|
||||
"readtime.other": "# min de lecture",
|
||||
"rss.created": "Flux RSS",
|
||||
"rss.updated": "Flux RSS du contenu mis à jour",
|
||||
"search": "Recherche",
|
||||
|
@ -26,9 +26,8 @@
|
||||
"header": "頁首",
|
||||
"meta.comments": "評論",
|
||||
"meta.source": "來源",
|
||||
"search.config.lang": "ja",
|
||||
"search.config.pipeline": "stemmer",
|
||||
"search.config.separator": "[\\s\\-,。]+",
|
||||
"search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+",
|
||||
"nav": "導航",
|
||||
"readtime.one": "需要 1 分鐘閲讀",
|
||||
"readtime.other": "需要 # 分鐘閲讀",
|
||||
|
@ -32,9 +32,8 @@
|
||||
"rss.created": "RSS 訂閱",
|
||||
"rss.updated": "RSS 訂閱內容已更新",
|
||||
"search": "搜尋",
|
||||
"search.config.lang": "ja",
|
||||
"search.config.pipeline": "stemmer",
|
||||
"search.config.separator": "[\\s\\- 、。,.?;]+",
|
||||
"search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+",
|
||||
"search.placeholder": "搜尋",
|
||||
"search.share": "分享",
|
||||
"search.reset": "清除",
|
||||
|
@ -32,9 +32,8 @@
|
||||
"rss.created": "RSS 订阅",
|
||||
"rss.updated": "已更新内容的 RSS 订阅",
|
||||
"search": "查找",
|
||||
"search.config.lang": "ja",
|
||||
"search.config.pipeline": "stemmer",
|
||||
"search.config.separator": "[\\s\\-,。]+",
|
||||
"search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+",
|
||||
"search.placeholder": "搜索",
|
||||
"search.share": "分享",
|
||||
"search.reset": "清空当前内容",
|
||||
|
@ -1,63 +1,105 @@
|
||||
{#-
|
||||
This file was automatically generated - do not edit
|
||||
-#}
|
||||
{% macro render_status(nav_item, type) %}
|
||||
{% set class = "md-status md-status--" ~ type %}
|
||||
{% if config.extra.status and config.extra.status[type] %}
|
||||
<span class="{{ class }}" title="{{ config.extra.status[type] }}">
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="{{ class }}"></span>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% macro render_content(nav_item, ref = nav_item) %}
|
||||
{% if nav_item.is_page and nav_item.meta.icon %}
|
||||
{% include ".icons/" ~ nav_item.meta.icon ~ ".svg" %}
|
||||
{% endif %}
|
||||
<span class="md-ellipsis">
|
||||
{{ ref.title }}
|
||||
</span>
|
||||
{% if nav_item.is_page and nav_item.meta.status %}
|
||||
{{ render_status(nav_item, nav_item.meta.status) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% macro render_pruned(nav_item, ref = nav_item) %}
|
||||
{% set first = nav_item.children | first %}
|
||||
{% if first and first.children %}
|
||||
{{ render_pruned(first, ref) }}
|
||||
{% else %}
|
||||
<a href="{{ first.url | url }}" class="md-nav__link">
|
||||
{{ render_content(ref) }}
|
||||
{% if nav_item.children | length > 0 %}
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% macro render(nav_item, path, level) %}
|
||||
{% set class = "md-nav__item" %}
|
||||
{% if nav_item.active %}
|
||||
{% set class = class ~ " md-nav__item--active" %}
|
||||
{% endif %}
|
||||
{% if nav_item.children %}
|
||||
{% set indexes = [] %}
|
||||
{% if "navigation.indexes" in features %}
|
||||
{% for nav_item in nav_item.children %}
|
||||
{% if nav_item.is_index and not index is defined %}
|
||||
{% set _ = indexes.append(nav_item) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if "navigation.sections" in features and level == 1 + (
|
||||
"navigation.tabs" in features
|
||||
) %}
|
||||
{% set class = class ~ " md-nav__item--section" %}
|
||||
{% elif not nav_item.active and "navigation.prune" in features %}
|
||||
{% set class = class ~ " md-nav__item--pruned" %}
|
||||
{% set prune = true %}
|
||||
{% endif %}
|
||||
<li class="{{ class }} md-nav__item--nested">
|
||||
{% set expanded = "navigation.expand" in features %}
|
||||
{% set active = nav_item.active or expanded %}
|
||||
{% set checked = "checked" if nav_item.active %}
|
||||
{% if expanded and not checked %}
|
||||
{% set indeterminate = "md-toggle--indeterminate" %}
|
||||
{% endif %}
|
||||
<input class="md-nav__toggle md-toggle {{ indeterminate }}" type="checkbox" id="{{ path }}" {{ checked }}>
|
||||
{% set indexes = [] %}
|
||||
{% if "navigation.indexes" in features %}
|
||||
{% for nav_item in nav_item.children %}
|
||||
{% if nav_item.is_index and not index is defined %}
|
||||
{% set _ = indexes.append(nav_item) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if not indexes %}
|
||||
<label class="md-nav__link" for="{{ path }}" id="{{ path }}_label" tabindex="0">
|
||||
{{ nav_item.title }}
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
</label>
|
||||
{% else %}
|
||||
{% set index = indexes | first %}
|
||||
{% set class = "md-nav__link--active" if index == page %}
|
||||
<div class="md-nav__link md-nav__link--index {{ class }}">
|
||||
<a href="{{ index.url | url }}">{{ nav_item.title }}</a>
|
||||
{% if nav_item.children | length > 1 %}
|
||||
<label for="{{ path }}">
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<nav class="md-nav" data-md-level="{{ level }}" aria-labelledby="{{ path }}_label" aria-expanded="{{ nav_item.active | tojson }}">
|
||||
<label class="md-nav__title" for="{{ path }}">
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
{{ nav_item.title }}
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
{% for nav_item in nav_item.children %}
|
||||
{% if not indexes or nav_item != indexes | first %}
|
||||
{{ render(nav_item, path ~ "_" ~ loop.index, level + 1) }}
|
||||
{% if not prune %}
|
||||
{% set expanded = "navigation.expand" in features %}
|
||||
{% set active = nav_item.active or expanded %}
|
||||
{% set checked = "checked" if nav_item.active %}
|
||||
{% if expanded and not checked %}
|
||||
{% set indeterminate = "md-toggle--indeterminate" %}
|
||||
{% endif %}
|
||||
<input class="md-nav__toggle md-toggle {{ indeterminate }}" type="checkbox" id="{{ path }}" {{ checked }}>
|
||||
{% if not indexes %}
|
||||
<label class="md-nav__link" for="{{ path }}" id="{{ path }}_label" tabindex="0">
|
||||
{{ render_content(nav_item) }}
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
</label>
|
||||
{% else %}
|
||||
{% set index = indexes | first %}
|
||||
{% set class = "md-nav__link--active" if index == page %}
|
||||
<div class="md-nav__link md-nav__container">
|
||||
<a href="{{ index.url | url }}" class="md-nav__link {{ class }}">
|
||||
{{ render_content(index, nav_item) }}
|
||||
</a>
|
||||
{% if nav_item.children | length > 1 %}
|
||||
<label class="md-nav__link {{ class }}" for="{{ path }}">
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
</label>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
<nav class="md-nav" data-md-level="{{ level }}" aria-labelledby="{{ path }}_label" aria-expanded="{{ nav_item.active | tojson }}">
|
||||
<label class="md-nav__title" for="{{ path }}">
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
{{ nav_item.title }}
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
{% for nav_item in nav_item.children %}
|
||||
{% if not indexes or nav_item != indexes | first %}
|
||||
{{ render(nav_item, path ~ "_" ~ loop.index, level + 1) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% else %}
|
||||
{{ render_pruned(nav_item) }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% elif nav_item == page %}
|
||||
<li class="{{ class }}">
|
||||
@ -69,12 +111,12 @@
|
||||
{% endif %}
|
||||
{% if toc %}
|
||||
<label class="md-nav__link md-nav__link--active" for="__toc">
|
||||
{{ nav_item.title }}
|
||||
{{ render_content(nav_item) }}
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
</label>
|
||||
{% endif %}
|
||||
<a href="{{ nav_item.url | url }}" class="md-nav__link md-nav__link--active">
|
||||
{{ nav_item.title }}
|
||||
{{ render_content(nav_item) }}
|
||||
</a>
|
||||
{% if toc %}
|
||||
{% include "partials/toc.html" %}
|
||||
@ -83,9 +125,8 @@
|
||||
{% else %}
|
||||
<li class="{{ class }}">
|
||||
<a href="{{ nav_item.url | url }}" class="md-nav__link">
|
||||
{{ nav_item.title }}
|
||||
{{ render_content(nav_item) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{{ render(nav_item, path, level) }}
|
||||
|
@ -1,6 +1,7 @@
|
||||
{#-
|
||||
This file was automatically generated - do not edit
|
||||
-#}
|
||||
{% import "partials/nav-item.html" as item with context %}
|
||||
{% set class = "md-nav md-nav--primary" %}
|
||||
{% if "navigation.tabs" in features %}
|
||||
{% set class = class ~ " md-nav--lifted" %}
|
||||
@ -23,8 +24,7 @@
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
{% for nav_item in nav %}
|
||||
{% set path = "__nav_" ~ loop.index %}
|
||||
{% set level = 1 %}
|
||||
{% include "partials/nav-item.html" %}
|
||||
{{ item.render(nav_item, path, 1) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
20
material/partials/pagination.html
Normal file
20
material/partials/pagination.html
Normal file
@ -0,0 +1,20 @@
|
||||
{#-
|
||||
This file was automatically generated - do not edit
|
||||
-#}
|
||||
{% import ".icons/material/chevron-double-left.svg" as icon_first %}
|
||||
{% import ".icons/material/chevron-left.svg" as icon_previous %}
|
||||
{% import ".icons/material/chevron-right.svg" as icon_next %}
|
||||
{% import ".icons/material/chevron-double-right.svg" as icon_last %}
|
||||
<nav class="md-pagination">
|
||||
{{
|
||||
pagination({
|
||||
"link_attr": { "class": "md-pagination__link" },
|
||||
"curpage_attr": { "class": "md-pagination__current" },
|
||||
"dotdot_attr": { "class": "md-pagination__dots" },
|
||||
"symbol_first": icon_first,
|
||||
"symbol_previous": icon_previous,
|
||||
"symbol_next": icon_next,
|
||||
"symbol_last": icon_last
|
||||
})
|
||||
}}
|
||||
</nav>
|
16
material/partials/palette.html
Normal file
16
material/partials/palette.html
Normal file
@ -0,0 +1,16 @@
|
||||
{#-
|
||||
This file was automatically generated - do not edit
|
||||
-#}
|
||||
<form class="md-header__option" data-md-component="palette">
|
||||
{% for option in config.theme.palette %}
|
||||
{% set scheme = option.scheme | d("default", true) %}
|
||||
{% set primary = option.primary | d("indigo", true) %}
|
||||
{% set accent = option.accent | d("indigo", true) %}
|
||||
<input class="md-option" data-md-color-media="{{ option.media }}" data-md-color-scheme="{{ scheme | replace(' ', '-') }}" data-md-color-primary="{{ primary | replace(' ', '-') }}" data-md-color-accent="{{ accent | replace(' ', '-') }}" {% if option.toggle %} aria-label="{{ option.toggle.name }}" {% else %} aria-hidden="true" {% endif %} type="radio" name="__palette" id="__palette_{{ loop.index }}">
|
||||
{% if option.toggle %}
|
||||
<label class="md-header__button md-icon" title="{{ option.toggle.name }}" for="__palette_{{ loop.index0 or loop.length }}" hidden>
|
||||
{% include ".icons/" ~ option.toggle.icon ~ ".svg" %}
|
||||
</label>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</form>
|
60
material/partials/post.html
Normal file
60
material/partials/post.html
Normal file
@ -0,0 +1,60 @@
|
||||
{#-
|
||||
This file was automatically generated - do not edit
|
||||
-#}
|
||||
<article class="md-post md-post--excerpt">
|
||||
<header class="md-post__header">
|
||||
{% if post.authors %}
|
||||
<nav class="md-post__authors md-typeset">
|
||||
{% for author in post.authors %}
|
||||
<span class="md-author">
|
||||
<img src="{{ author.avatar }}" alt="{{ author.name }}">
|
||||
</span>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
<div class="md-post__meta md-meta">
|
||||
<ul class="md-meta__list">
|
||||
<li class="md-meta__item">
|
||||
<time datetime="{{ post.config.date.created }}">
|
||||
{{- post.config.date.created | date -}}
|
||||
</time>
|
||||
{#- Collapse whitespace -#}
|
||||
</li>
|
||||
{% if post.categories %}
|
||||
<li class="md-meta__item">
|
||||
{{ lang.t("blog.categories.in") }}
|
||||
{% for category in post.categories %}
|
||||
<a href="{{ category.url | url }}" class="md-meta__link">
|
||||
{{- category.title -}}
|
||||
</a>
|
||||
{%- if loop.revindex > 1 %}, {% endif -%}
|
||||
{% endfor -%}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if post.config.readtime %}
|
||||
{% set time = post.config.readtime %}
|
||||
<li class="md-meta__item">
|
||||
{% if time == 1 %}
|
||||
{{ lang.t("readtime.one") }}
|
||||
{% else %}
|
||||
{{ lang.t("readtime.other") | replace("#", time) }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% if post.config.draft %}
|
||||
<span class="md-draft">
|
||||
{{ lang.t("blog.draft") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
<div class="md-post__content md-typeset">
|
||||
{{ post.content }}
|
||||
<nav class="md-post__action">
|
||||
<a href="{{ post.url | url }}">
|
||||
{{ lang.t("blog.continue") }}
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</article>
|
@ -7,17 +7,21 @@
|
||||
<form class="md-search__form" name="search">
|
||||
<input type="text" class="md-search__input" name="query" aria-label="{{ lang.t('search.placeholder') }}" placeholder="{{ lang.t('search.placeholder') }}" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
|
||||
<label class="md-search__icon md-icon" for="__search">
|
||||
{% include ".icons/material/magnify.svg" %}
|
||||
{% include ".icons/material/arrow-left.svg" %}
|
||||
{% set icon = config.theme.icon.search or "material/magnify" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
{% set icon = config.theme.icon.previous or "material/arrow-left" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</label>
|
||||
<nav class="md-search__options" aria-label="{{ lang.t('search') }}">
|
||||
{% if "search.share" in features %}
|
||||
<a href="javascript:void(0)" class="md-search__icon md-icon" title="{{ lang.t('search.share') }}" aria-label="{{ lang.t('search.share') }}" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
|
||||
{% include ".icons/material/share-variant.svg" %}
|
||||
{% set icon = config.theme.icon.share or "material/share-variant" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<button type="reset" class="md-search__icon md-icon" title="{{ lang.t('search.reset') }}" aria-label="{{ lang.t('search.reset') }}" tabindex="-1">
|
||||
{% include ".icons/material/close.svg" %}
|
||||
{% set icon = config.theme.icon.close or "material/close" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</button>
|
||||
</nav>
|
||||
{% if "search.suggest" in features %}
|
||||
|
@ -1,28 +1,35 @@
|
||||
{#-
|
||||
This file was automatically generated - do not edit
|
||||
-#}
|
||||
{% if not class %}
|
||||
{% set class = "md-tabs__link" %}
|
||||
{% if nav_item.active %}
|
||||
{% set class = class ~ " md-tabs__link--active" %}
|
||||
{% macro render_content(nav_item, ref = nav_item) %}
|
||||
{% if nav_item == ref or "navigation.indexes" in features %}
|
||||
{% if nav_item.is_index and nav_item.meta.icon %}
|
||||
{% include ".icons/" ~ nav_item.meta.icon ~ ".svg" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ ref.title }}
|
||||
{% endmacro %}
|
||||
{% macro render(nav_item, ref = nav_item) %}
|
||||
{% set class = "md-tabs__item" %}
|
||||
{% if ref.active %}
|
||||
{% set class = class ~ " md-tabs__item--active" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if nav_item.children %}
|
||||
{% set title = title | d(nav_item.title) %}
|
||||
{% set nav_item = nav_item.children | first %}
|
||||
{% if nav_item.children %}
|
||||
{% include "partials/tabs-item.html" %}
|
||||
{% set first = nav_item.children | first %}
|
||||
{% if first.children %}
|
||||
{{ render(first, ref) }}
|
||||
{% else %}
|
||||
<li class="{{ class }}">
|
||||
<a href="{{ first.url | url }}" class="md-tabs__link">
|
||||
{{ render_content(first, ref) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="md-tabs__item">
|
||||
<a href="{{ nav_item.url | url }}" class="{{ class }}">
|
||||
{{ title }}
|
||||
<li class="{{ class }}">
|
||||
<a href="{{ nav_item.url | url }}" class="md-tabs__link">
|
||||
{{ render_content(nav_item) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="md-tabs__item">
|
||||
<a href="{{ nav_item.url | url }}" class="{{ class }}">
|
||||
{{ nav_item.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
@ -1,12 +1,12 @@
|
||||
{#-
|
||||
This file was automatically generated - do not edit
|
||||
-#}
|
||||
{% set class = "" %}
|
||||
{% import "partials/tabs-item.html" as item with context %}
|
||||
<nav class="md-tabs" aria-label="{{ lang.t('tabs') }}" data-md-component="tabs">
|
||||
<div class="md-grid">
|
||||
<ul class="md-tabs__list">
|
||||
{% for nav_item in nav %}
|
||||
{% include "partials/tabs-item.html" %}
|
||||
{{ item.render(nav_item) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -4,24 +4,23 @@
|
||||
{% if 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 config.extra.tags %}
|
||||
{% set icon = " md-tag-icon" %}
|
||||
{% if tag.type %}
|
||||
{% set icon = icon ~ " md-tag--" ~ tag.type %}
|
||||
{% endif %}
|
||||
<nav class="md-tags" {{ hidden }}>
|
||||
{% for tag in tags %}
|
||||
{% set icon = "" %}
|
||||
{% if config.extra.tags %}
|
||||
{% set icon = " md-tag-icon" %}
|
||||
{% if tag.type %}
|
||||
{% set icon = icon ~ " md-tag--" ~ tag.type %}
|
||||
{% endif %}
|
||||
{% if tag.url %}
|
||||
<a href="{{ tag.url | url }}" class="md-tag{{ icon }}">
|
||||
{{- tag.name -}}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="md-tag{{ icon }}">
|
||||
{{- tag.name -}}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if tag.url %}
|
||||
<a href="{{ tag.url | url }}" class="md-tag{{ icon }}">
|
||||
{{- tag.name -}}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="md-tag{{ icon }}">
|
||||
{{- tag.name -}}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
19
material/plugins/blog/__init__.py
Normal file
19
material/plugins/blog/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
38
material/plugins/blog/author.py
Normal file
38
material/plugins/blog/author.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
from mkdocs.config.base import Config
|
||||
from mkdocs.config.config_options import DictOfItems, SubConfig, Type
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Author
|
||||
class Author(Config):
|
||||
name = Type(str)
|
||||
description = Type(str)
|
||||
avatar = Type(str)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Authors
|
||||
class Authors(Config):
|
||||
authors = DictOfItems(SubConfig(Author), default = {})
|
88
material/plugins/blog/config.py
Normal file
88
material/plugins/blog/config.py
Normal file
@ -0,0 +1,88 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
from functools import partial
|
||||
from markdown.extensions.toc import slugify
|
||||
from mkdocs.config.config_options import Choice, Deprecated, Optional, Type
|
||||
from mkdocs.config.base import Config
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Blog plugin configuration
|
||||
class BlogConfig(Config):
|
||||
enabled = Type(bool, default = True)
|
||||
|
||||
# Options for blog
|
||||
blog_dir = Type(str, default = "blog")
|
||||
blog_toc = Type(bool, default = False)
|
||||
|
||||
# Options for posts
|
||||
post_dir = Type(str, default = "{blog}/posts")
|
||||
post_date_format = Type(str, default = "long")
|
||||
post_url_date_format = Type(str, default = "yyyy/MM/dd")
|
||||
post_url_format = Type(str, default = "{date}/{slug}")
|
||||
post_url_max_categories = Type(int, default = 1)
|
||||
post_slugify = Type((type(slugify), partial), default = slugify)
|
||||
post_slugify_separator = Type(str, default = "-")
|
||||
post_excerpt = Choice(["optional", "required"], default = "optional")
|
||||
post_excerpt_max_authors = Type(int, default = 1)
|
||||
post_excerpt_max_categories = Type(int, default = 5)
|
||||
post_excerpt_separator = Type(str, default = "<!-- more -->")
|
||||
post_readtime = Type(bool, default = True)
|
||||
post_readtime_words_per_minute = Type(int, default = 265)
|
||||
|
||||
# Options for archive
|
||||
archive = Type(bool, default = True)
|
||||
archive_name = Type(str, default = "blog.archive")
|
||||
archive_date_format = Type(str, default = "yyyy")
|
||||
archive_url_date_format = Type(str, default = "yyyy")
|
||||
archive_url_format = Type(str, default = "archive/{date}")
|
||||
archive_toc = Optional(Type(bool))
|
||||
|
||||
# Options for categories
|
||||
categories = Type(bool, default = True)
|
||||
categories_name = Type(str, default = "blog.categories")
|
||||
categories_url_format = Type(str, default = "category/{slug}")
|
||||
categories_slugify = Type((type(slugify), partial), default = slugify)
|
||||
categories_slugify_separator = Type(str, default = "-")
|
||||
categories_allowed = Type(list, default = [])
|
||||
categories_toc = Optional(Type(bool))
|
||||
|
||||
# Options for pagination
|
||||
pagination = Type(bool, default = True)
|
||||
pagination_per_page = Type(int, default = 10)
|
||||
pagination_url_format = Type(str, default = "page/{page}")
|
||||
pagination_format = Type(str, default = "~2~")
|
||||
pagination_if_single_page = Type(bool, default = False)
|
||||
pagination_keep_content = Type(bool, default = False)
|
||||
|
||||
# Options for authors
|
||||
authors = Type(bool, default = True)
|
||||
authors_file = Type(str, default = "{blog}/.authors.yml")
|
||||
|
||||
# Options for drafts
|
||||
draft = Type(bool, default = False)
|
||||
draft_on_serve = Type(bool, default = True)
|
||||
draft_if_future_date = Type(bool, default = False)
|
||||
|
||||
# Deprecated options
|
||||
pagination_template = Deprecated(moved_to = "pagination_format")
|
813
material/plugins/blog/plugin.py
Normal file
813
material/plugins/blog/plugin.py
Normal file
@ -0,0 +1,813 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
import readtime
|
||||
import yaml
|
||||
|
||||
from babel.dates import format_date
|
||||
from datetime import datetime
|
||||
from mkdocs.config.defaults import MkDocsConfig
|
||||
from mkdocs.exceptions import PluginError
|
||||
from mkdocs.plugins import BasePlugin, event_priority
|
||||
from mkdocs.structure import StructureItem
|
||||
from mkdocs.structure.files import File, Files, InclusionLevel
|
||||
from mkdocs.structure.nav import Navigation, Section
|
||||
from mkdocs.structure.pages import Page
|
||||
from mkdocs.utils import get_relative_url
|
||||
from paginate import Page as Pagination
|
||||
from shutil import rmtree
|
||||
from tempfile import mkdtemp
|
||||
from yaml import SafeLoader
|
||||
|
||||
from .author import Author, Authors
|
||||
from .config import BlogConfig
|
||||
from .structure import Archive, Category, Excerpt, Post, View
|
||||
from .templates import url_filter
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Blog plugin
|
||||
class BlogPlugin(BasePlugin[BlogConfig]):
|
||||
supports_multiple_instances = True
|
||||
|
||||
# Initialize plugin
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Initialize incremental builds
|
||||
self.is_serve = False
|
||||
self.is_dirty = False
|
||||
|
||||
# Initialize a temporary directory
|
||||
self.temp_dir = mkdtemp()
|
||||
|
||||
# Determine whether we're serving the site
|
||||
def on_startup(self, *, command, dirty):
|
||||
self.is_serve = command == "serve"
|
||||
self.is_dirty = dirty
|
||||
|
||||
# Initialize authors and set defaults
|
||||
def on_config(self, config):
|
||||
if not self.config.enabled:
|
||||
return
|
||||
|
||||
# Initialize entrypoint
|
||||
self.blog: View
|
||||
|
||||
# Initialize and resolve authors, if enabled
|
||||
if self.config.authors:
|
||||
self.authors = self._resolve_authors(config)
|
||||
|
||||
# Initialize table of contents settings
|
||||
if not isinstance(self.config.archive_toc, bool):
|
||||
self.config.archive_toc = self.config.blog_toc
|
||||
if not isinstance(self.config.categories_toc, bool):
|
||||
self.config.categories_toc = self.config.blog_toc
|
||||
|
||||
# By default, drafts are rendered when the documentation is served,
|
||||
# but not when it is built, for a better authoring experience
|
||||
if self.is_serve and self.config.draft_on_serve:
|
||||
self.config.draft = True
|
||||
|
||||
# Remove posts before constructing navigation (run later) - allow other
|
||||
# plugins to alter the list of files and navigation prior to this plugin
|
||||
@event_priority(-50)
|
||||
def on_files(self, files, *, config):
|
||||
if not self.config.enabled:
|
||||
return
|
||||
|
||||
# Resolve path to entrypoint and site directory
|
||||
root = posixpath.normpath(self.config.blog_dir)
|
||||
site = config.site_dir
|
||||
|
||||
# Compute path to posts directory
|
||||
path = self.config.post_dir.format(blog = root)
|
||||
path = posixpath.normpath(path)
|
||||
|
||||
# Temporarily remove posts and adjust destination paths for assets
|
||||
for file in files:
|
||||
if not file.src_uri.startswith(path):
|
||||
continue
|
||||
|
||||
# We must exclude all files related to posts from here on, so MkDocs
|
||||
# will not attach the posts to the navigation when auto-populating.
|
||||
# We add them back in `on_nav`, so MkDocs processes them, unless
|
||||
# excluded by being tagged as a draft or through other means.
|
||||
if file.is_documentation_page():
|
||||
file.inclusion = InclusionLevel.EXCLUDED
|
||||
|
||||
# We also need to adjust destination paths for assets to remove the
|
||||
# purely functional posts directory prefix when building
|
||||
if file.is_media_file():
|
||||
file.dest_uri = file.dest_uri.replace(path, root)
|
||||
file.abs_dest_path = os.path.join(site, file.dest_path)
|
||||
file.url = file.url.replace(path, root)
|
||||
|
||||
# Resolve and load posts and generate indexes (run later) - we resolve all
|
||||
# posts after the navigation is constructed in order to allow other plugins
|
||||
# to alter the navigation (e.g. awesome-pages) before we start to add pages
|
||||
# generated by this plugin. Post URLs must be computed before any Markdown
|
||||
# processing, so that when linking to and from posts, MkDocs behaves exactly
|
||||
# the same as with regular documentation pages. We create all pages related
|
||||
# to posts as part of this plugin, so we control the entire process.
|
||||
@event_priority(-50)
|
||||
def on_nav(self, nav, *, config, files):
|
||||
if not self.config.enabled:
|
||||
return
|
||||
|
||||
# Resolve entrypoint and posts sorted by descending date - if the posts
|
||||
# directory or entrypoint do not exist, they are automatically created
|
||||
self.blog = self._resolve(files, config, nav)
|
||||
self.blog.posts = sorted(
|
||||
self._resolve_posts(files, config),
|
||||
key = lambda post: post.config.date.created,
|
||||
reverse = True
|
||||
)
|
||||
|
||||
# Attach posts to entrypoint without adding them to the navigation, so
|
||||
# that the entrypoint is considered to be the active page for each post.
|
||||
# Hack: MkDocs has a bug where pages that are marked to be not in the
|
||||
# navigation are auto-populated nonetheless - see https://t.ly/7aYnO
|
||||
self._attach(self.blog, [None, *reversed(self.blog.posts), None])
|
||||
for post in self.blog.posts:
|
||||
post.file.inclusion = InclusionLevel.NOT_IN_NAV
|
||||
|
||||
# Generate and attach views for archive
|
||||
if self.config.archive:
|
||||
views = [*self._generate_archive(config, files)]
|
||||
self.blog.views.extend(views)
|
||||
|
||||
# Attach and link views for archive
|
||||
title = self._translate(self.config.archive_name, config)
|
||||
self._attach_to(self.blog.parent, Section(title, views), nav)
|
||||
|
||||
# Generate and attach views for categories
|
||||
if self.config.categories:
|
||||
views = [*self._generate_categories(config, files)]
|
||||
self.blog.views.extend(views)
|
||||
|
||||
# Attach and link views for categories
|
||||
title = self._translate(self.config.categories_name, config)
|
||||
self._attach_to(self.blog.parent, Section(title, views), nav)
|
||||
|
||||
# Paginate generated views, if enabled
|
||||
if self.config.pagination:
|
||||
for view in [*self._resolve_views(self.blog)]:
|
||||
for page in self._generate_pages(view, config, files):
|
||||
view.pages.append(page)
|
||||
|
||||
# Prepare post for rendering (run later) - allow other plugins to alter
|
||||
# the contents or metadata of a post before it is rendered and make sure
|
||||
# that the post includes a separator, which is essential for rendering
|
||||
# excerpts that should be included in views
|
||||
@event_priority(-50)
|
||||
def on_page_markdown(self, markdown, *, page, config, files):
|
||||
if not self.config.enabled:
|
||||
return
|
||||
|
||||
# Skip if page is not a post managed by this instance - this plugin has
|
||||
# support for multiple instances, which is why this check is necessary
|
||||
if page not in self.blog.posts:
|
||||
if not self.config.pagination:
|
||||
return
|
||||
|
||||
# We set the contents of the view to its title if pagination should
|
||||
# not keep the content of the original view on paginaged views
|
||||
if not self.config.pagination_keep_content:
|
||||
if page in self._resolve_views(self.blog):
|
||||
assert isinstance(page, View)
|
||||
if 0 < page.pages.index(page):
|
||||
return f"# {page.title}"
|
||||
|
||||
# Nothing more to be done for views
|
||||
return
|
||||
|
||||
# Extract and assign authors to post, if enabled
|
||||
if self.config.authors:
|
||||
for name in page.config.authors:
|
||||
if name not in self.authors:
|
||||
raise PluginError(f"Couldn't find author '{name}'")
|
||||
|
||||
# Append to list of authors
|
||||
page.authors.append(self.authors[name])
|
||||
|
||||
# Compute readtime of post, if enabled and not explicitly set
|
||||
if self.config.post_readtime:
|
||||
rate = self.config.post_readtime_words_per_minute
|
||||
|
||||
# There's a bug in the readtime library which causes it to fail if
|
||||
# the input string contains emojis - see https://t.ly/qEoHq
|
||||
if not page.config.readtime:
|
||||
data = markdown.encode("unicode_escape")
|
||||
read = readtime.of_markdown(data, rate)
|
||||
page.config.readtime = read.minutes
|
||||
|
||||
# Extract settings for excerpts
|
||||
separator = self.config.post_excerpt_separator
|
||||
max_authors = self.config.post_excerpt_max_authors
|
||||
max_categories = self.config.post_excerpt_max_categories
|
||||
|
||||
# Ensure presence of separator and throw, if its absent and required -
|
||||
# we append the separator to the end of the contents of the post, if it
|
||||
# is not already present, so we can remove footnotes or other content
|
||||
# from the excerpt without affecting the content of the excerpt
|
||||
if separator not in page.markdown:
|
||||
path = page.file.src_uri
|
||||
if self.config.post_excerpt == "required":
|
||||
raise PluginError(
|
||||
f"Couldn't find '{separator}' separator in '{path}'"
|
||||
)
|
||||
else:
|
||||
page.markdown += f"\n\n{separator}"
|
||||
|
||||
# Create excerpt for post and inherit authors and categories - excerpts
|
||||
# can contain a subset of the authors and categories of the post
|
||||
page.excerpt = Excerpt(page, config, files)
|
||||
page.excerpt.authors = page.authors[:max_authors]
|
||||
page.excerpt.categories = page.categories[:max_categories]
|
||||
|
||||
# Register template filters for plugin
|
||||
def on_env(self, env, *, config, files):
|
||||
if not self.config.enabled:
|
||||
return
|
||||
|
||||
# Filter for formatting dates related to posts
|
||||
def date_filter(date: datetime):
|
||||
return self._format_date_for_post(date, config)
|
||||
|
||||
# Register custom template filters
|
||||
env.filters["date"] = date_filter
|
||||
env.filters["url"] = url_filter
|
||||
|
||||
# Prepare view for rendering (run latest) - views are rendered last, as we
|
||||
# need to mutate the navigation to account for pagination. The main problem
|
||||
# is that we need to replace the view in the navigation, because otherwise
|
||||
# the view would not be considered active.
|
||||
@event_priority(-100)
|
||||
def on_page_context(self, context, *, page, config, nav):
|
||||
if not self.config.enabled:
|
||||
return
|
||||
|
||||
# Skip if page is not a view managed by this instance - this plugin has
|
||||
# support for multiple instances, which is why this check is necessary
|
||||
if page not in self._resolve_views(self.blog):
|
||||
return
|
||||
|
||||
# Retrieve parent view or section
|
||||
assert isinstance(page, View)
|
||||
main = page.parent
|
||||
|
||||
# If this page is a view, and the parent page is a view as well, we got
|
||||
# a paginated view and need to update the parent view in the navigation.
|
||||
# Paginated views are always rendered last, which is why we can safely
|
||||
# mutate the navigation at this point
|
||||
if isinstance(main, View):
|
||||
assert isinstance(main.parent, Section)
|
||||
|
||||
# Replace view in navigation and rewire view - the current view in
|
||||
# the navigation becomes the main view, thus the entire chain moves
|
||||
# one level up. It's essential that the rendering order is linear,
|
||||
# or else we might end up with a broken navigation.
|
||||
at = main.parent.children.index(main)
|
||||
main.parent.children[at] = page
|
||||
page.parent = main.parent
|
||||
|
||||
# Render excerpts and perpare pagination
|
||||
posts, pagination = self._render(page)
|
||||
|
||||
# Render pagination links
|
||||
def pager(args: object):
|
||||
return pagination.pager(
|
||||
format = self.config.pagination_format,
|
||||
show_if_single_page = self.config.pagination_if_single_page,
|
||||
**args
|
||||
)
|
||||
|
||||
# Assign posts and pagination to context
|
||||
context["posts"] = posts
|
||||
context["pagination"] = pager if pagination else None
|
||||
|
||||
# Remove temporary directory on shutdown
|
||||
def on_shutdown(self):
|
||||
rmtree(self.temp_dir)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Check if the given post is excluded
|
||||
def _is_excluded(self, post: Post):
|
||||
if self.config.draft:
|
||||
return False
|
||||
|
||||
# If a post was not explicitly marked or unmarked as draft, and the
|
||||
# date should be taken into account, we automatically mark it as draft
|
||||
# if the publishing date is in the future. This, of course, is opt-in
|
||||
# and must be explicitly enabled by the author.
|
||||
if not isinstance(post.config.draft, bool):
|
||||
if self.config.draft_if_future_date:
|
||||
return post.config.date > datetime.now()
|
||||
|
||||
# Post might be a draft
|
||||
return bool(post.config.draft)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Resolve entrypoint - the entrypoint of the blog hosts all posts, sorted
|
||||
# by descending date. The entrypoint must always be present, even if there
|
||||
# are no posts, and is automatically created if it does not exist yet. Note
|
||||
# that posts might be paginated, but this is configurable by the author.
|
||||
def _resolve(self, files: Files, config: MkDocsConfig, nav: Navigation):
|
||||
path = os.path.join(self.config.blog_dir, "index.md")
|
||||
path = os.path.normpath(path)
|
||||
|
||||
# Create entrypoint, if it does not exist
|
||||
docs = os.path.relpath(config.docs_dir)
|
||||
file = os.path.join(docs, path)
|
||||
if not os.path.isfile(file):
|
||||
self._save_to_file(file, "# Blog\n\n")
|
||||
|
||||
# Append entrypoint to files - note that the entrypoint is added to
|
||||
# the docs directory, so we need to set the temporary flag to false
|
||||
files.append(self._path_to_file(path, config, temp = False))
|
||||
|
||||
# Obtain entrypoint page
|
||||
file = files.get_file_from_path(path)
|
||||
page = file.page
|
||||
|
||||
# Create entrypoint view and attach to parent
|
||||
view = View(page.title, file, config)
|
||||
self._attach(page.parent, [
|
||||
page.previous_page,
|
||||
view,
|
||||
page.next_page
|
||||
])
|
||||
|
||||
# Update entrypoint in navigation
|
||||
for items in [view.parent.children, nav.pages]:
|
||||
items[items.index(page)] = view
|
||||
|
||||
# Return view
|
||||
return view
|
||||
|
||||
# Resolve post - the caller must make sure that the given file points to an
|
||||
# actual post (and not a page), or behavior might be unpredictable
|
||||
def _resolve_post(self, file: File, config: MkDocsConfig):
|
||||
post = Post(file, config)
|
||||
|
||||
# Compute path and create a temporary file for path resolution
|
||||
path = self._format_path_for_post(post, config)
|
||||
temp = self._path_to_file(path, config, temp = False)
|
||||
|
||||
# Replace post destination file system path and URL
|
||||
file.dest_uri = temp.dest_uri
|
||||
file.abs_dest_path = temp.abs_dest_path
|
||||
file.url = temp.url
|
||||
|
||||
# Replace canonical URL and return post
|
||||
post._set_canonical_url(config.site_url)
|
||||
return post
|
||||
|
||||
# Resolve posts from directory - traverse all documentation pages and filter
|
||||
# and yield those that are located in the posts directory
|
||||
def _resolve_posts(self, files: Files, config: MkDocsConfig):
|
||||
path = self.config.post_dir.format(blog = self.config.blog_dir)
|
||||
path = os.path.normpath(path)
|
||||
|
||||
# Create posts directory, if it does not exist
|
||||
docs = os.path.relpath(config.docs_dir)
|
||||
name = os.path.join(docs, path)
|
||||
if not os.path.isdir(name):
|
||||
os.makedirs(name, exist_ok = True)
|
||||
|
||||
# Filter posts from pages - prior to calling this function, the caller
|
||||
# should've excluded all posts, so they're not listed in the navigation
|
||||
inclusion = InclusionLevel.is_excluded
|
||||
for file in files.documentation_pages(inclusion = inclusion):
|
||||
if not file.src_path.startswith(path):
|
||||
continue
|
||||
|
||||
# Resolve post - in order to determine whether a post should be
|
||||
# excluded, we must load it and analyze its metadata. All posts
|
||||
# marked as drafts are excluded, except for when the author has
|
||||
# configured drafts to be included in the navigation.
|
||||
post = self._resolve_post(file, config)
|
||||
if not self._is_excluded(post):
|
||||
yield post
|
||||
|
||||
# Resolve authors - check if there's an authors file at the configured
|
||||
# location, and if one was found, load and validate it
|
||||
def _resolve_authors(self, config: MkDocsConfig):
|
||||
path = self.config.authors_file.format(blog = self.config.blog_dir)
|
||||
path = os.path.normpath(path)
|
||||
|
||||
# If the authors file does not exist, return an empty dictionary
|
||||
docs = os.path.relpath(config.docs_dir)
|
||||
file = os.path.join(docs, path)
|
||||
if not os.path.isfile(file):
|
||||
authors: dict[str, Author] = dict()
|
||||
return authors
|
||||
|
||||
# Open file and parse as YAML
|
||||
with open(file, encoding = "utf-8") as f:
|
||||
config: Authors = Authors(os.path.abspath(file))
|
||||
try:
|
||||
config.load_dict(yaml.load(f, SafeLoader) or {})
|
||||
|
||||
# The authors file could not be loaded because of a syntax error,
|
||||
# which we display to the user with a nice error message
|
||||
except Exception as e:
|
||||
raise PluginError(
|
||||
f"Error reading authors file '{path}' in '{docs}':\n"
|
||||
f"{e}"
|
||||
)
|
||||
|
||||
# Validate authors and throw if errors occurred
|
||||
errors, warnings = config.validate()
|
||||
if not config.authors and warnings:
|
||||
log.warning(
|
||||
f"Action required: the format of the authors file changed.\n"
|
||||
f"All authors must now be located under the 'authors' key.\n"
|
||||
f"Please adjust '{file}' to match:\n"
|
||||
f"\n"
|
||||
f"authors:\n"
|
||||
f" squidfunk:\n"
|
||||
f" avatar: https://avatars.githubusercontent.com/u/932156\n"
|
||||
f" description: Creator\n"
|
||||
f" name: Martin Donath\n"
|
||||
f"\n"
|
||||
)
|
||||
for _, w in warnings:
|
||||
log.warning(w)
|
||||
for _, e in errors:
|
||||
raise PluginError(
|
||||
f"Error reading authors file '{path}' in '{docs}':\n"
|
||||
f"{e}"
|
||||
)
|
||||
|
||||
# Return authors
|
||||
return config.authors
|
||||
|
||||
# Resolve views and pages of the given view that were generated by this
|
||||
# plugin when building the site and yield them in pre-order
|
||||
def _resolve_views(self, view: View):
|
||||
yield view
|
||||
|
||||
# Resolve views recursively
|
||||
for page in view.views:
|
||||
for next in self._resolve_views(page):
|
||||
assert isinstance(next, View)
|
||||
yield next
|
||||
|
||||
# Resolve pages
|
||||
for page in view.pages:
|
||||
assert isinstance(page, View)
|
||||
yield page
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Attach a list of pages to each other and to the given parent item without
|
||||
# explicitly adding them to the navigation, which can be done by the caller
|
||||
def _attach(self, parent: StructureItem, pages: list[Page]):
|
||||
for tail, page, head in zip(pages, pages[1:], pages[2:]):
|
||||
|
||||
# Link page to parent and siblings
|
||||
page.parent = parent
|
||||
page.previous_page = tail
|
||||
page.next_page = head
|
||||
|
||||
# Attach a section to the given parent section, make sure it's pages are
|
||||
# part of the navigation, and ensure all pages are linked correctly
|
||||
def _attach_to(self, parent: Section, section: Section, nav: Navigation):
|
||||
section.parent = parent
|
||||
|
||||
# Determine the parent section to attach the section to, which might be
|
||||
# the top-level navigation, if no parent section was given. Note, that
|
||||
# it's currently not possible to chose the position of a section, but
|
||||
# we might add support for this in the future.
|
||||
items = parent.children if parent else nav.items
|
||||
items.append(section)
|
||||
|
||||
# Find last sibling that is a page, skipping sections, as we need to
|
||||
# append the given section after all other pages
|
||||
tail = next(item for item in reversed(items) if isinstance(item, Page))
|
||||
head = tail.next_page
|
||||
|
||||
# Attach section to navigation and pages to each other
|
||||
nav.pages.extend(section.children)
|
||||
self._attach(section, [tail, *section.children, head])
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Generate views for archive - analyze posts and generate the necessary
|
||||
# views, taking the date format provided by the author into account
|
||||
def _generate_archive(self, config: MkDocsConfig, files: Files):
|
||||
for post in self.blog.posts:
|
||||
date = post.config.date.created
|
||||
|
||||
# Compute name and path of archive view
|
||||
name = self._format_date_for_archive(date, config)
|
||||
path = self._format_path_for_archive(post, config)
|
||||
|
||||
# Create view for archive if it doesn't exist
|
||||
file = files.get_file_from_path(path)
|
||||
if not file:
|
||||
file = self._path_to_file(path, config)
|
||||
files.append(file)
|
||||
|
||||
# Create and yield archive view
|
||||
self._save_to_file(file.abs_src_path, f"# {name}")
|
||||
yield Archive(name, file, config)
|
||||
|
||||
# Assign post to archive
|
||||
assert isinstance(file.page, Archive)
|
||||
file.page.posts.append(post)
|
||||
|
||||
# Generate views for categories - analyze posts and generate the necessary
|
||||
# views, taking the allowed categories as set by the author into account
|
||||
def _generate_categories(self, config: MkDocsConfig, files: Files):
|
||||
for post in self.blog.posts:
|
||||
for name in post.config.categories:
|
||||
path = self._format_path_for_category(name)
|
||||
|
||||
# Ensure category is in non-empty allow list
|
||||
categories = self.config.categories_allowed or [name]
|
||||
if name not in categories:
|
||||
docs = os.path.relpath(config.docs_dir)
|
||||
path = os.path.relpath(post.file.abs_src_path, docs)
|
||||
raise PluginError(
|
||||
f"Error reading categories of post '{path}' in "
|
||||
f"'{docs}': category '{name}' not in allow list"
|
||||
)
|
||||
|
||||
# Create view for category if it doesn't exist
|
||||
file = files.get_file_from_path(path)
|
||||
if not file:
|
||||
file = self._path_to_file(path, config)
|
||||
files.append(file)
|
||||
|
||||
# Create and yield archive view
|
||||
self._save_to_file(file.abs_src_path, f"# {name}")
|
||||
yield Category(name, file, config)
|
||||
|
||||
# Assign post to category and vice versa
|
||||
assert isinstance(file.page, Category)
|
||||
file.page.posts.append(post)
|
||||
post.categories.append(file.page)
|
||||
|
||||
# Generate pages for pagination - analyze view and generate the necessary
|
||||
# pages, creating a chain of views for simple rendering and replacement
|
||||
def _generate_pages(self, view: View, config: MkDocsConfig, files: Files):
|
||||
yield view
|
||||
|
||||
# Extract settings for pagination
|
||||
step = self.config.pagination_per_page
|
||||
prev = view
|
||||
|
||||
# Compute pagination boundaries and create pages
|
||||
for at in range(step, len(view.posts), step):
|
||||
path = self._format_path_for_pagination(view.url, 1 + at // step)
|
||||
file = self._path_to_file(path, config)
|
||||
|
||||
# Replace post source file system path and apend to files
|
||||
file.src_uri = view.file.src_uri
|
||||
file.abs_src_path = view.file.abs_src_path
|
||||
files.append(file)
|
||||
|
||||
# Create view and attach to previous page
|
||||
next = View(view.title, file, config)
|
||||
self._attach(prev, [
|
||||
view.previous_page,
|
||||
next,
|
||||
view.next_page
|
||||
])
|
||||
|
||||
# Assign posts and pages to view
|
||||
next.posts = view.posts
|
||||
next.pages = view.pages
|
||||
|
||||
# Continue with next page
|
||||
prev = next
|
||||
yield next
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Render excerpts and pagination for the given view
|
||||
def _render(self, view: View):
|
||||
posts, pagination = view.posts, None
|
||||
|
||||
# Create pagination, if enabled
|
||||
if self.config.pagination:
|
||||
at = view.pages.index(view)
|
||||
|
||||
# Compute pagination boundaries
|
||||
step = self.config.pagination_per_page
|
||||
p, q = at * step, at * step + step
|
||||
|
||||
# Extract posts in pagination boundaries
|
||||
posts = view.posts[p:q]
|
||||
pagination = self._render_pagination(view, (p, q))
|
||||
|
||||
# Render excerpts for selected posts
|
||||
posts = [
|
||||
self._render_post(post.excerpt, view)
|
||||
for post in posts
|
||||
]
|
||||
|
||||
# Return posts and pagination
|
||||
return posts, pagination
|
||||
|
||||
# Render excerpt in the context of the given view
|
||||
def _render_post(self, excerpt: Excerpt, view: View):
|
||||
excerpt.render(view, self.config.post_excerpt_separator)
|
||||
|
||||
# Determine whether to add posts to the table of contents of the view -
|
||||
# note that those settings can be changed individually for each type of
|
||||
# view, which is why we need to check the type of view and the table of
|
||||
# contents setting for that type of view
|
||||
toc = self.config.blog_toc
|
||||
if isinstance(view, Archive):
|
||||
toc = self.config.archive_toc
|
||||
if isinstance(view, Category):
|
||||
toc = self.config.categories_toc
|
||||
|
||||
# Attach top-level table of contents item to view if it should be added
|
||||
# and both, the view and excerpt contain table of contents items
|
||||
if toc and excerpt.toc.items and view.toc.items:
|
||||
view.toc.items[0].children.append(excerpt.toc.items[0])
|
||||
|
||||
# Return excerpt
|
||||
return excerpt
|
||||
|
||||
# Create pagination for the given view and range
|
||||
def _render_pagination(self, view: View, range: tuple[int, int]):
|
||||
p, q = range
|
||||
|
||||
# Create URL from the given page to another page
|
||||
def url_maker(n: int):
|
||||
return get_relative_url(view.pages[n - 1].url, view.url)
|
||||
|
||||
# Return pagination
|
||||
return Pagination(
|
||||
view.posts, page = q // (q - p),
|
||||
items_per_page = q - p,
|
||||
url_maker = url_maker
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Format path for post
|
||||
def _format_path_for_post(self, post: Post, config: MkDocsConfig):
|
||||
categories = post.config.categories[:self.config.post_url_max_categories]
|
||||
categories = [self._slugify_category(name) for name in categories]
|
||||
|
||||
# Replace placeholders in format string
|
||||
date = post.config.date.created
|
||||
path = self.config.post_url_format.format(
|
||||
categories = "/".join(categories),
|
||||
date = self._format_date_for_post_url(date, config),
|
||||
file = post.file.name,
|
||||
slug = post.config.slug or self._slugify_post(post)
|
||||
)
|
||||
|
||||
# Normalize path and strip slashes at the beginning and end
|
||||
path = posixpath.normpath(path.strip("/"))
|
||||
return posixpath.join(self.config.blog_dir, f"{path}.md")
|
||||
|
||||
# Format path for archive
|
||||
def _format_path_for_archive(self, post: Post, config: MkDocsConfig):
|
||||
date = post.config.date.created
|
||||
path = self.config.archive_url_format.format(
|
||||
date = self._format_date_for_archive_url(date, config)
|
||||
)
|
||||
|
||||
# Normalize path and strip slashes at the beginning and end
|
||||
path = posixpath.normpath(path.strip("/"))
|
||||
return posixpath.join(self.config.blog_dir, f"{path}.md")
|
||||
|
||||
# Format path for category
|
||||
def _format_path_for_category(self, name: str):
|
||||
path = self.config.categories_url_format.format(
|
||||
slug = self._slugify_category(name)
|
||||
)
|
||||
|
||||
# Normalize path and strip slashes at the beginning and end
|
||||
path = posixpath.normpath(path.strip("/"))
|
||||
return posixpath.join(self.config.blog_dir, f"{path}.md")
|
||||
|
||||
# Format path for pagination
|
||||
def _format_path_for_pagination(self, base: str, page: int):
|
||||
path = self.config.pagination_url_format.format(
|
||||
page = page
|
||||
)
|
||||
|
||||
# Normalize path and strip slashes at the beginning and end
|
||||
path = posixpath.normpath(path.strip("/"))
|
||||
return posixpath.join(base, f"{path}.md")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Format date
|
||||
def _format_date(self, date: datetime, format: str, config: MkDocsConfig):
|
||||
locale = config.theme["language"]
|
||||
return format_date(date, format = format, locale = locale)
|
||||
|
||||
# Format date for post
|
||||
def _format_date_for_post(self, date: datetime, config: MkDocsConfig):
|
||||
format = self.config.post_date_format
|
||||
return self._format_date(date, format, config)
|
||||
|
||||
# Format date for post URL
|
||||
def _format_date_for_post_url(self, date: datetime, config: MkDocsConfig):
|
||||
format = self.config.post_url_date_format
|
||||
return self._format_date(date, format, config)
|
||||
|
||||
# Format date for archive
|
||||
def _format_date_for_archive(self, date: datetime, config: MkDocsConfig):
|
||||
format = self.config.archive_date_format
|
||||
return self._format_date(date, format, config)
|
||||
|
||||
# Format date for archive URL
|
||||
def _format_date_for_archive_url(self, date: datetime, config: MkDocsConfig):
|
||||
format = self.config.archive_url_date_format
|
||||
return self._format_date(date, format, config)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Slugify post title
|
||||
def _slugify_post(self, post: Post):
|
||||
separator = self.config.post_slugify_separator
|
||||
return self.config.post_slugify(post.title, separator)
|
||||
|
||||
# Slugify category
|
||||
def _slugify_category(self, name: str):
|
||||
separator = self.config.categories_slugify_separator
|
||||
return self.config.categories_slugify(name, separator)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Create a file for the given path, which must point to a valid source file,
|
||||
# either inside the temporary directory or the docs directory
|
||||
def _path_to_file(self, path: str, config: MkDocsConfig, *, temp = True):
|
||||
assert path.endswith(".md")
|
||||
file = File(
|
||||
path,
|
||||
config.docs_dir if not temp else self.temp_dir,
|
||||
config.site_dir,
|
||||
config.use_directory_urls
|
||||
)
|
||||
|
||||
# Hack: mark file as generated, so other plugins don't think it's part
|
||||
# of the file system. This is more or less a new quasi-standard that
|
||||
# still needs to be adopted by MkDocs, and was introduced by the
|
||||
# git-revision-date-localized-plugin - see https://bit.ly/3ZUmdBx
|
||||
if temp:
|
||||
file.generated_by = "material/blog"
|
||||
|
||||
# Return file
|
||||
return file
|
||||
|
||||
# Write the content to the file located at the given path
|
||||
def _save_to_file(self, path: str, content: str):
|
||||
os.makedirs(os.path.dirname(path), exist_ok = True)
|
||||
with open(path, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Translate the placeholder referenced by the given key
|
||||
def _translate(self, key: str, config: MkDocsConfig) -> str:
|
||||
env = config.theme.get_env()
|
||||
template = env.get_template(
|
||||
"partials/language.html", globals = { "config": config }
|
||||
)
|
||||
|
||||
# Translate placeholder
|
||||
return template.module.t(key)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Data
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Set up logging
|
||||
log = logging.getLogger("mkdocs.material.blog")
|
277
material/plugins/blog/structure/__init__.py
Normal file
277
material/plugins/blog/structure/__init__.py
Normal file
@ -0,0 +1,277 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import yaml
|
||||
|
||||
from copy import copy
|
||||
from markdown import Markdown
|
||||
from material.plugins.blog.author import Author
|
||||
from mkdocs.config.defaults import MkDocsConfig
|
||||
from mkdocs.exceptions import PluginError
|
||||
from mkdocs.structure.files import File, Files
|
||||
from mkdocs.structure.nav import Section
|
||||
from mkdocs.structure.pages import Page, _RelativePathTreeprocessor
|
||||
from mkdocs.structure.toc import get_toc
|
||||
from mkdocs.utils.meta import YAML_RE
|
||||
from re import Match
|
||||
from typing import Union
|
||||
from yaml import SafeLoader
|
||||
|
||||
from .config import PostConfig
|
||||
from .markdown import ExcerptTreeprocessor
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Post
|
||||
class Post(Page):
|
||||
|
||||
# Initialize post - posts are never listed in the navigation, which is why
|
||||
# they will never include a title that was manually set, so we can omit it
|
||||
def __init__(self, file: File, config: MkDocsConfig):
|
||||
super().__init__(None, file, config)
|
||||
|
||||
# Resolve path relative to docs directory for error reporting
|
||||
docs = os.path.relpath(config.docs_dir)
|
||||
path = os.path.relpath(file.abs_src_path, docs)
|
||||
|
||||
# Read contents and metadata immediately
|
||||
with open(file.abs_src_path, encoding = "utf-8") as f:
|
||||
self.markdown = f.read()
|
||||
|
||||
# Sadly, MkDocs swallows any exceptions that occur during parsing.
|
||||
# As we want to provide the best possible authoring experience, we
|
||||
# need to catch errors early and display them nicely. We decided to
|
||||
# drop support for MkDocs' MultiMarkdown syntax, because it is not
|
||||
# correctly implemented anyway. When using MultiMarkdown syntax, all
|
||||
# date formats are returned as strings and list are not properly
|
||||
# supported. Thus, we just use the relevants parts of `get_data`.
|
||||
match: Match = YAML_RE.match(self.markdown)
|
||||
if not match:
|
||||
raise PluginError(
|
||||
f"Error reading metadata of post '{path}' in '{docs}':\n"
|
||||
f"Expected metadata to be defined but found nothing"
|
||||
)
|
||||
|
||||
# Extract metadata and parse as YAML
|
||||
try:
|
||||
self.meta = yaml.load(match.group(1), SafeLoader) or {}
|
||||
self.markdown = self.markdown[match.end():].lstrip("\n")
|
||||
|
||||
# The post's metadata could not be parsed because of a syntax error,
|
||||
# which we display to the user with a nice error message
|
||||
except Exception as e:
|
||||
raise PluginError(
|
||||
f"Error reading metadata of post '{path}' in '{docs}':\n"
|
||||
f"{e}"
|
||||
)
|
||||
|
||||
# Initialize post configuration, but remove all keys that this plugin
|
||||
# doesn't care about, or they will be reported as invalid configuration
|
||||
self.config: PostConfig = PostConfig(file.abs_src_path)
|
||||
self.config.load_dict({
|
||||
key: self.meta[key] for key in (
|
||||
set(self.meta.keys()) &
|
||||
set(self.config.keys())
|
||||
)
|
||||
})
|
||||
|
||||
# Validate configuration and throw if errors occurred
|
||||
errors, warnings = self.config.validate()
|
||||
for _, w in warnings:
|
||||
log.warning(w)
|
||||
for k, e in errors:
|
||||
raise PluginError(
|
||||
f"Error reading metadata '{k}' of post '{path}' in '{docs}':\n"
|
||||
f"{e}"
|
||||
)
|
||||
|
||||
# Excerpts are subsets of posts that are used in views like archive and
|
||||
# category views. They are not rendered as standalone pages, but are
|
||||
# included in the context of the parent post. Each post has a dedicated
|
||||
# excerpt instance which is reused when rendering views.
|
||||
self.excerpt: Excerpt = None
|
||||
|
||||
# Initialize authors and actegories
|
||||
self.authors: list[Author] = []
|
||||
self.categories: list[Category] = []
|
||||
|
||||
# Ensure template is set or use default
|
||||
self.meta.setdefault("template", "blog-post.html")
|
||||
|
||||
# Ensure template hides navigation
|
||||
self.meta["hide"] = self.meta.get("hide", [])
|
||||
if "navigation" not in self.meta["hide"]:
|
||||
self.meta["hide"].append("navigation")
|
||||
|
||||
# The contents and metadata were already read in the constructor (and not
|
||||
# in `read_source` as for pages), so this function must be set to a no-op
|
||||
def read_source(self, config: MkDocsConfig):
|
||||
pass
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Excerpt
|
||||
class Excerpt(Page):
|
||||
|
||||
# Initialize an excerpt for the given post - we create the Markdown parser
|
||||
# when intitializing the excerpt in order to improve rendering performance
|
||||
# for excerpts, as they are reused across several different views, because
|
||||
# posts might be referenced from multiple different locations
|
||||
def __init__(self, post: Post, config: MkDocsConfig, files: Files):
|
||||
self.file = copy(post.file)
|
||||
self.post = post
|
||||
|
||||
# Initialize configuration, contents and metadata
|
||||
self.config = post.config
|
||||
self.markdown = post.markdown
|
||||
self.meta = post.meta
|
||||
|
||||
# Initialize authors and categories - note that views usually contain
|
||||
# subsets of those lists, which is why we need to manage them here
|
||||
self.authors: list[Author] = []
|
||||
self.categories: list[Category] = []
|
||||
|
||||
# Initialize parser - note that we need to patch the configuration,
|
||||
# more specifically the table of contents extension
|
||||
config = _patch(config)
|
||||
self.md = Markdown(
|
||||
extensions = config.markdown_extensions,
|
||||
extension_configs = config.mdx_configs,
|
||||
)
|
||||
|
||||
# Register excerpt tree processor - this processor resolves anchors to
|
||||
# posts from within views, so they point to the correct location
|
||||
self.md.treeprocessors.register(
|
||||
ExcerptTreeprocessor(post),
|
||||
"excerpt",
|
||||
0
|
||||
)
|
||||
|
||||
# Register relative path tree processor - this processor resolves links
|
||||
# to other pages and assets, and is used by MkDocs itself
|
||||
self.md.treeprocessors.register(
|
||||
_RelativePathTreeprocessor(self.file, files, config),
|
||||
"relpath",
|
||||
1
|
||||
)
|
||||
|
||||
# Render an excerpt of the post on the given page - note that this is not
|
||||
# thread-safe because excerpts are shared across views, as it cuts down on
|
||||
# the cost of initialization. However, if in the future, we decide to render
|
||||
# posts and views concurrently, we must change this behavior.
|
||||
def render(self, page: Page, separator: str):
|
||||
self.file.url = page.url
|
||||
|
||||
# Retrieve excerpt tree processor and set page as base
|
||||
at = self.md.treeprocessors.get_index_for_name("excerpt")
|
||||
processor: ExcerptTreeprocessor = self.md.treeprocessors[at]
|
||||
processor.base = page
|
||||
|
||||
# Convert Markdown to HTML and extract excerpt
|
||||
self.content = self.md.convert(self.markdown)
|
||||
self.content, *_ = self.content.split(separator, 1)
|
||||
|
||||
# Extract table of contents and reset post URL - if we wouldn't reset
|
||||
# the excerpt URL, linking to the excerpt from the view would not work
|
||||
self.toc = get_toc(getattr(self.md, "toc_tokens", []))
|
||||
self.file.url = self.post.url
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# View
|
||||
class View(Page):
|
||||
|
||||
# Initialize view
|
||||
def __init__(self, title: str | None, file: File, config: MkDocsConfig):
|
||||
super().__init__(title, file, config)
|
||||
self.parent: Union[View, Section]
|
||||
|
||||
# Initialize posts and views
|
||||
self.posts: list[Post] = []
|
||||
self.views: list[View] = []
|
||||
|
||||
# Initialize pages for pagination
|
||||
self.pages: list[View] = []
|
||||
|
||||
# Set necessary metadata
|
||||
def read_source(self, config: MkDocsConfig):
|
||||
super().read_source(config)
|
||||
|
||||
# Ensure template is set or use default
|
||||
self.meta.setdefault("template", "blog.html")
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Archive view
|
||||
class Archive(View):
|
||||
pass
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Category view
|
||||
class Category(View):
|
||||
pass
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helper functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Patch configuration
|
||||
def _patch(config: MkDocsConfig):
|
||||
config = copy(config)
|
||||
|
||||
# Copy configuration that needs to be patched
|
||||
config.validation = copy(config.validation)
|
||||
config.validation.links = copy(config.validation.links)
|
||||
config.mdx_configs = copy(config.mdx_configs)
|
||||
config.mdx_configs["toc"] = copy(config.mdx_configs["toc"])
|
||||
|
||||
# In order to render excerpts for posts, we need to make sure that the
|
||||
# table of contents extension is appropriately configured
|
||||
config.mdx_configs["toc"] = {
|
||||
**config.mdx_configs["toc"],
|
||||
**{
|
||||
"anchorlink": True, # Render headline as clickable
|
||||
"baselevel": 2, # Render h1 as h2 and so forth
|
||||
"permalink": False, # Remove permalinks
|
||||
"toc_depth": 2 # Remove everything below h2
|
||||
}
|
||||
}
|
||||
|
||||
# Additionally, we disable link validation when rendering excerpts, because
|
||||
# invalid links have already been reported when rendering the page
|
||||
links = config.validation.links
|
||||
links.not_found = logging.DEBUG
|
||||
links.absolute_links = logging.DEBUG
|
||||
links.unrecognized_links = logging.DEBUG
|
||||
|
||||
# Return patched configuration
|
||||
return config
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Data
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Set up logging
|
||||
log = logging.getLogger("mkdocs.material.blog")
|
37
material/plugins/blog/structure/config.py
Normal file
37
material/plugins/blog/structure/config.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
from mkdocs.config.base import Config
|
||||
from mkdocs.config.config_options import ListOfItems, Optional, Type
|
||||
|
||||
from .options import PostDate
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Post configuration
|
||||
class PostConfig(Config):
|
||||
authors = ListOfItems(Type(str), default = [])
|
||||
categories = ListOfItems(Type(str), default = [])
|
||||
date = PostDate()
|
||||
draft = Optional(Type(bool))
|
||||
readtime = Optional(Type(int))
|
||||
slug = Optional(Type(str))
|
58
material/plugins/blog/structure/markdown.py
Normal file
58
material/plugins/blog/structure/markdown.py
Normal file
@ -0,0 +1,58 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
from markdown.treeprocessors import Treeprocessor
|
||||
from mkdocs.structure.pages import Page
|
||||
from mkdocs.utils import get_relative_url
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Excerpt tree processor
|
||||
class ExcerptTreeprocessor(Treeprocessor):
|
||||
|
||||
# Initialize excerpt tree processor
|
||||
def __init__(self, page: Page, base: Page = None):
|
||||
self.page = page
|
||||
self.base = base
|
||||
|
||||
# Transform HTML after Markdown processing
|
||||
def run(self, root: Element):
|
||||
main = True
|
||||
|
||||
# We're only interested in anchors, which is why we continue when the
|
||||
# link does not start with an anchor tag
|
||||
for el in root.iter("a"):
|
||||
anchor = el.get("href")
|
||||
if not anchor.startswith("#"):
|
||||
continue
|
||||
|
||||
# The main headline should link to the post page, not to a specific
|
||||
# anchor, which is why we remove the anchor in that case
|
||||
path = get_relative_url(self.page.url, self.base.url)
|
||||
if main:
|
||||
el.set("href", path)
|
||||
else:
|
||||
el.set("href", path + anchor)
|
||||
|
||||
# Main headline has been seen
|
||||
main = False
|
85
material/plugins/blog/structure/options.py
Normal file
85
material/plugins/blog/structure/options.py
Normal file
@ -0,0 +1,85 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
from collections import UserDict
|
||||
from datetime import date, datetime, time
|
||||
from mkdocs.config.base import BaseConfigOption, Config, ValidationError
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Date dictionary
|
||||
class DateDict(UserDict[str, datetime]):
|
||||
|
||||
# Initialize date dictionary
|
||||
def __init__(self, data: dict):
|
||||
super().__init__(data)
|
||||
|
||||
# Initialize date of creation
|
||||
if "created" in data:
|
||||
self.created: datetime = data["created"]
|
||||
|
||||
def __getattr__(self, name: str):
|
||||
if name in self.data:
|
||||
return self.data[name]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Post date option
|
||||
class PostDate(BaseConfigOption[DateDict]):
|
||||
|
||||
# Initialize post dates
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Normalize the supported types for post dates to datetime
|
||||
def pre_validation(self, config: Config, key_name: str):
|
||||
|
||||
# If the date points to a scalar value, convert it to a dictionary,
|
||||
# since we want to allow the user to specify custom and arbitrary date
|
||||
# values for posts. Currently, only the `created` date is mandatory,
|
||||
# because it's needed to sort posts for views.
|
||||
if not isinstance(config[key_name], dict):
|
||||
config[key_name] = { "created": config[key_name] }
|
||||
|
||||
# Initialize date dictionary and convert all date values to datetime
|
||||
config[key_name] = DateDict(config[key_name])
|
||||
for key, value in config[key_name].items():
|
||||
if isinstance(value, date):
|
||||
config[key_name][key] = datetime.combine(value, time())
|
||||
|
||||
# Ensure each date value is of type datetime
|
||||
def run_validation(self, value: DateDict):
|
||||
for key in value:
|
||||
if not isinstance(value[key], datetime):
|
||||
raise ValidationError(
|
||||
f"Expected type: {date} or {datetime} "
|
||||
f"but received: {type(value[key])}"
|
||||
)
|
||||
|
||||
# Ensure presence of `date.created`
|
||||
if not value.created:
|
||||
raise ValidationError(
|
||||
"Expected 'created' date when using dictionary syntax"
|
||||
)
|
||||
|
||||
# Return date dictionary
|
||||
return value
|
42
material/plugins/blog/templates/__init__.py
Normal file
42
material/plugins/blog/templates/__init__.py
Normal file
@ -0,0 +1,42 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
from jinja2 import pass_context
|
||||
from jinja2.runtime import Context
|
||||
from material.plugins.blog.structure import View
|
||||
from mkdocs.utils.templates import url_filter as _url_filter
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Filter for normalizing URLs with support for paginated views
|
||||
@pass_context
|
||||
def url_filter(context: Context, url: str | None):
|
||||
page = context["page"]
|
||||
|
||||
# If the current page is a view, check if the URL links to the page
|
||||
# itself, and replace it with the URL of the main view
|
||||
if isinstance(page, View):
|
||||
if page.url == url:
|
||||
url = page.pages[0].url
|
||||
|
||||
# Forward to original template filter
|
||||
return _url_filter(context, url)
|
@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
35
material/plugins/info/config.py
Normal file
35
material/plugins/info/config.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
from mkdocs.config.config_options import Type
|
||||
from mkdocs.config.base import Config
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Info plugin configuration
|
||||
class InfoConfig(Config):
|
||||
enabled = Type(bool, default = True)
|
||||
enabled_on_serve = Type(bool, default = False)
|
||||
|
||||
# Options for archive
|
||||
archive = Type(bool, default = True)
|
||||
archive_stop_on_violation = Type(bool, default = True)
|
@ -26,40 +26,38 @@ import requests
|
||||
import sys
|
||||
|
||||
from colorama import Fore, Style
|
||||
from importlib.metadata import distributions, version
|
||||
from io import BytesIO
|
||||
from mkdocs import utils
|
||||
from mkdocs.commands.build import DuplicateFilter
|
||||
from mkdocs.config import config_options as opt
|
||||
from mkdocs.config.base import Config
|
||||
from markdown.extensions.toc import slugify
|
||||
from mkdocs.plugins import BasePlugin, event_priority
|
||||
from mkdocs.structure.files import get_files
|
||||
from pkg_resources import get_distribution, working_set
|
||||
from mkdocs.utils import get_theme_dir
|
||||
from zipfile import ZipFile, ZIP_DEFLATED
|
||||
|
||||
from .config import InfoConfig
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Class
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Info plugin configuration scheme
|
||||
class InfoPluginConfig(Config):
|
||||
enabled = opt.Type(bool, default = True)
|
||||
enabled_on_serve = opt.Type(bool, default = False)
|
||||
|
||||
# Options for archive
|
||||
archive = opt.Type(bool, default = True)
|
||||
archive_name = opt.Type(str, default = "example")
|
||||
archive_stop_on_violation = opt.Type(bool, default = True)
|
||||
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Info plugin
|
||||
class InfoPlugin(BasePlugin[InfoPluginConfig]):
|
||||
class InfoPlugin(BasePlugin[InfoConfig]):
|
||||
|
||||
# Determine whether we're serving
|
||||
# Initialize plugin
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Initialize incremental builds
|
||||
self.is_serve = False
|
||||
|
||||
# Determine whether we're serving the site
|
||||
def on_startup(self, *, command, dirty):
|
||||
self.is_serve = (command == "serve")
|
||||
self.is_serve = command == "serve"
|
||||
|
||||
# Initialize plugin (run earliest)
|
||||
# Create a self-contained example (run earliest) - determine all files that
|
||||
# are visible to MkDocs and are used to build the site, create an archive
|
||||
# that contains all of them, and print a summary of the archive contents.
|
||||
# The user must attach this archive to the bug report.
|
||||
@event_priority(100)
|
||||
def on_config(self, config):
|
||||
if not self.config.enabled:
|
||||
@ -76,11 +74,11 @@ class InfoPlugin(BasePlugin[InfoPluginConfig]):
|
||||
res = requests.get(url, allow_redirects = False)
|
||||
|
||||
# Check if we're running the latest version
|
||||
_, version = res.headers.get("location").rsplit("/", 1)
|
||||
package = get_distribution("mkdocs-material")
|
||||
if not package.version.startswith(version):
|
||||
_, current = res.headers.get("location").rsplit("/", 1)
|
||||
present = version("mkdocs-material")
|
||||
if not present.startswith(current):
|
||||
log.error("Please upgrade to the latest version.")
|
||||
self._help_on_versions_and_exit(package.version, version)
|
||||
self._help_on_versions_and_exit(present, current)
|
||||
|
||||
# Print message that we're creating a bug report
|
||||
log.info("Started archive creation for bug report")
|
||||
@ -89,8 +87,8 @@ class InfoPlugin(BasePlugin[InfoPluginConfig]):
|
||||
# hack to detect whether the custom_dir setting was used without parsing
|
||||
# mkdocs.yml again - we check at which position the directory provided
|
||||
# by the theme resides, and if it's not the first one, abort.
|
||||
base = utils.get_theme_dir(config.theme.name)
|
||||
if config.theme.dirs.index(base):
|
||||
path = get_theme_dir(config.theme.name)
|
||||
if config.theme.dirs.index(path):
|
||||
log.error("Please remove 'custom_dir' setting.")
|
||||
self._help_on_customizations_and_exit()
|
||||
|
||||
@ -102,33 +100,39 @@ class InfoPlugin(BasePlugin[InfoPluginConfig]):
|
||||
log.error("Please remove 'hooks' setting.")
|
||||
self._help_on_customizations_and_exit()
|
||||
|
||||
# Create in-memory archive
|
||||
# Create in-memory archive and prompt user to enter a short descriptive
|
||||
# name for the archive, which is also used as the directory name. Note
|
||||
# that the name is slugified for better readability and stripped of any
|
||||
# file extension that the user might have entered.
|
||||
archive = BytesIO()
|
||||
archive_name = self.config.archive_name
|
||||
example = input("\nPlease name your bug report (2-4 words): ")
|
||||
example, _ = os.path.splitext(example)
|
||||
example = slugify(example, "-")
|
||||
|
||||
# Create self-contained example from project
|
||||
files = []
|
||||
files: list[str] = []
|
||||
with ZipFile(archive, "a", ZIP_DEFLATED, False) as f:
|
||||
for path in ["mkdocs.yml", "requirements.txt"]:
|
||||
if os.path.isfile(path):
|
||||
f.write(path, os.path.join(archive_name, path))
|
||||
f.write(path, os.path.join(example, path))
|
||||
|
||||
# Append all files visible to MkDocs
|
||||
for file in get_files(config):
|
||||
path = os.path.relpath(file.abs_src_path, os.path.curdir)
|
||||
f.write(path, os.path.join(archive_name, path))
|
||||
f.write(path, os.path.join(example, path))
|
||||
|
||||
# Add information on installed packages
|
||||
f.writestr(
|
||||
os.path.join(archive_name, "requirements.lock.txt"),
|
||||
os.path.join(example, "requirements.lock.txt"),
|
||||
"\n".join(sorted([
|
||||
f"{dist.as_requirement()}" for dist in working_set
|
||||
"==".join([package.name, package.version])
|
||||
for package in distributions()
|
||||
]))
|
||||
)
|
||||
|
||||
# Add information in platform
|
||||
f.writestr(
|
||||
os.path.join(archive_name, "platform.json"),
|
||||
os.path.join(example, "platform.json"),
|
||||
json.dumps(
|
||||
{
|
||||
"system": platform.platform(),
|
||||
@ -148,7 +152,7 @@ class InfoPlugin(BasePlugin[InfoPluginConfig]):
|
||||
|
||||
# Finally, write archive to disk
|
||||
buffer = archive.getbuffer()
|
||||
with open(f"{archive_name}.zip", "wb") as f:
|
||||
with open(f"{example}.zip", "wb") as f:
|
||||
f.write(archive.getvalue())
|
||||
|
||||
# Print summary
|
||||
@ -172,7 +176,7 @@ class InfoPlugin(BasePlugin[InfoPluginConfig]):
|
||||
if buffer.nbytes > 1000000:
|
||||
log.warning("Archive exceeds recommended maximum size of 1 MB")
|
||||
|
||||
# Aaaaaand done.
|
||||
# Aaaaaand done
|
||||
sys.exit(1)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@ -235,5 +239,4 @@ def _size(value, factor = 1):
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Set up logging
|
||||
log = logging.getLogger("mkdocs")
|
||||
log.addFilter(DuplicateFilter())
|
||||
log = logging.getLogger("mkdocs.material.info")
|
||||
|
@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
30
material/plugins/offline/config.py
Normal file
30
material/plugins/offline/config.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
from mkdocs.config.config_options import Type
|
||||
from mkdocs.config.base import Config
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Offline plugin configuration
|
||||
class OfflineConfig(Config):
|
||||
enabled = Type(bool, default = True)
|
@ -20,50 +20,48 @@
|
||||
|
||||
import os
|
||||
|
||||
from mkdocs import utils
|
||||
from mkdocs.config import config_options as opt
|
||||
from mkdocs.config.base import Config
|
||||
from mkdocs.plugins import BasePlugin, event_priority
|
||||
from mkdocs.utils import write_file
|
||||
|
||||
from .config import OfflineConfig
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Class
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Offline plugin configuration scheme
|
||||
class OfflinePluginConfig(Config):
|
||||
enabled = opt.Type(bool, default = True)
|
||||
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Offline plugin
|
||||
class OfflinePlugin(BasePlugin[OfflinePluginConfig]):
|
||||
class OfflinePlugin(BasePlugin[OfflineConfig]):
|
||||
|
||||
# Initialize plugin
|
||||
# Set configuration for offline build
|
||||
def on_config(self, config):
|
||||
if not self.config.enabled:
|
||||
return
|
||||
|
||||
# Ensure correct resolution of links
|
||||
# Ensure correct resolution of links when viewing the site from the
|
||||
# file system by disabling directory URLs
|
||||
config.use_directory_urls = False
|
||||
|
||||
# Support offline search (run latest)
|
||||
# Append iframe-worker to polyfills/shims
|
||||
config.extra.polyfills = config.extra.get("polyfills", [])
|
||||
if not any("iframe-worker" in url for url in config.extra.polyfills):
|
||||
worker = "https://unpkg.com/iframe-worker/shim"
|
||||
config.extra.polyfills.append(worker)
|
||||
|
||||
# Add support for offline search (run latest) - the search index is copied
|
||||
# and inlined into a script, so that it can be used without a server
|
||||
@event_priority(-100)
|
||||
def on_post_build(self, *, config):
|
||||
if not self.config.enabled:
|
||||
return
|
||||
|
||||
# Check for existence of search index
|
||||
base = os.path.join(config.site_dir, "search")
|
||||
path = os.path.join(base, "search_index.json")
|
||||
if not os.path.exists(path):
|
||||
path = os.path.join(config.site_dir, "search", "search_index.json")
|
||||
if not os.path.isfile(path):
|
||||
return
|
||||
|
||||
# Retrieve search index
|
||||
with open(path, "r") as data:
|
||||
index = data.read()
|
||||
|
||||
# Inline search index into script
|
||||
utils.write_file(
|
||||
f"var __index = {index}".encode("utf-8"),
|
||||
os.path.join(base, "search_index.js")
|
||||
# Create script with inlined search index
|
||||
with open(path, encoding = "utf-8") as f:
|
||||
write_file(
|
||||
f"var __index = {f.read()}".encode("utf-8"),
|
||||
path.replace(".json", ".js"),
|
||||
)
|
||||
|
@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
55
material/plugins/search/config.py
Normal file
55
material/plugins/search/config.py
Normal file
@ -0,0 +1,55 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
from mkdocs.config.config_options import (
|
||||
Choice,
|
||||
Deprecated,
|
||||
Optional,
|
||||
ListOfItems,
|
||||
Type
|
||||
)
|
||||
from mkdocs.config.base import Config
|
||||
from mkdocs.contrib.search import LangOption
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Options
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Options for search pipeline
|
||||
pipeline = ("stemmer", "stopWordFilter", "trimmer")
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Search plugin configuration
|
||||
class SearchConfig(Config):
|
||||
lang = Optional(LangOption())
|
||||
separator = Optional(Type(str))
|
||||
pipeline = ListOfItems(Choice(pipeline), default = [])
|
||||
|
||||
# Options for text segmentation (Chinese)
|
||||
jieba_dict = Optional(Type(str))
|
||||
jieba_dict_user = Optional(Type(str))
|
||||
|
||||
# Unsupported options, originally implemented in MkDocs
|
||||
indexing = Deprecated(message = "Unsupported option")
|
||||
prebuild_index = Deprecated(message = "Unsupported option")
|
||||
min_search_length = Deprecated(message = "Unsupported option")
|
@ -26,43 +26,36 @@ import regex as re
|
||||
from html import escape
|
||||
from html.parser import HTMLParser
|
||||
from mkdocs import utils
|
||||
from mkdocs.commands.build import DuplicateFilter
|
||||
from mkdocs.config import config_options as opt
|
||||
from mkdocs.config.base import Config
|
||||
from mkdocs.contrib.search import LangOption
|
||||
from mkdocs.plugins import BasePlugin
|
||||
|
||||
from .config import SearchConfig
|
||||
|
||||
try:
|
||||
import jieba
|
||||
except ImportError:
|
||||
jieba = None
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Class
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Search plugin configuration scheme
|
||||
class SearchPluginConfig(Config):
|
||||
lang = opt.Optional(LangOption())
|
||||
separator = opt.Optional(opt.Type(str))
|
||||
pipeline = opt.ListOfItems(
|
||||
opt.Choice(("stemmer", "stopWordFilter", "trimmer")),
|
||||
default = []
|
||||
)
|
||||
|
||||
# Deprecated options
|
||||
indexing = opt.Deprecated(message = "Unsupported option")
|
||||
prebuild_index = opt.Deprecated(message = "Unsupported option")
|
||||
min_search_length = opt.Deprecated(message = "Unsupported option")
|
||||
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Search plugin
|
||||
class SearchPlugin(BasePlugin[SearchPluginConfig]):
|
||||
class SearchPlugin(BasePlugin[SearchConfig]):
|
||||
|
||||
# Determine whether we're running under dirty reload
|
||||
def on_startup(self, *, command, dirty):
|
||||
# Initialize plugin
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Initialize incremental builds
|
||||
self.is_dirtyreload = False
|
||||
self.is_dirty = dirty
|
||||
|
||||
# Initialize search index cache
|
||||
self.search_index_prev = None
|
||||
|
||||
# Determine whether we're serving the site
|
||||
def on_startup(self, *, command, dirty):
|
||||
self.is_dirty = dirty
|
||||
|
||||
# Initialize plugin
|
||||
def on_config(self, config):
|
||||
if not self.config.lang:
|
||||
@ -85,6 +78,30 @@ class SearchPlugin(BasePlugin[SearchPluginConfig]):
|
||||
# Initialize search index
|
||||
self.search_index = SearchIndex(**self.config)
|
||||
|
||||
# Set jieba dictionary, if given
|
||||
if self.config.jieba_dict:
|
||||
path = os.path.normpath(self.config.jieba_dict)
|
||||
if os.path.isfile(path):
|
||||
jieba.set_dictionary(path)
|
||||
log.debug(f"Loading jieba dictionary: {path}")
|
||||
else:
|
||||
log.warning(
|
||||
f"Configuration error for 'search.jieba_dict': "
|
||||
f"'{self.config.jieba_dict}' does not exist."
|
||||
)
|
||||
|
||||
# Set jieba user dictionary, if given
|
||||
if self.config.jieba_dict_user:
|
||||
path = os.path.normpath(self.config.jieba_dict_user)
|
||||
if os.path.isfile(path):
|
||||
jieba.load_userdict(path)
|
||||
log.debug(f"Loading jieba user dictionary: {path}")
|
||||
else:
|
||||
log.warning(
|
||||
f"Configuration error for 'search.jieba_dict_user': "
|
||||
f"'{self.config.jieba_dict_user}' does not exist."
|
||||
)
|
||||
|
||||
# Add page to search index
|
||||
def on_page_context(self, context, *, page, config, nav):
|
||||
self.search_index.add_entry_from_context(page)
|
||||
@ -167,9 +184,10 @@ class SearchIndex:
|
||||
title = "".join(section.title).strip()
|
||||
text = "".join(section.text).strip()
|
||||
|
||||
# Reset text, if only titles should be indexed
|
||||
if self.config["indexing"] == "titles":
|
||||
text = ""
|
||||
# Segment Chinese characters if jieba is available
|
||||
if jieba:
|
||||
title = self._segment_chinese(title)
|
||||
text = self._segment_chinese(text)
|
||||
|
||||
# Create entry for section
|
||||
entry = {
|
||||
@ -252,6 +270,25 @@ class SearchIndex:
|
||||
# No item found
|
||||
return None
|
||||
|
||||
# Find and segment Chinese characters in string
|
||||
def _segment_chinese(self, data):
|
||||
expr = re.compile(r"(\p{IsHan}+)", re.UNICODE)
|
||||
|
||||
# Replace callback
|
||||
def replace(match):
|
||||
value = match.group(0)
|
||||
|
||||
# Replace occurrence in original string with segmented version and
|
||||
# surround with zero-width whitespace for efficient indexing
|
||||
return "".join([
|
||||
"\u200b",
|
||||
"\u200b".join(jieba.cut(value.encode("utf-8"))),
|
||||
"\u200b",
|
||||
])
|
||||
|
||||
# Return string with segmented occurrences
|
||||
return expr.sub(replace, data).strip("\u200b")
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# HTML element
|
||||
@ -341,7 +378,8 @@ class Parser(HTMLParser):
|
||||
self.keep = set([
|
||||
"p", # Paragraphs
|
||||
"code", "pre", # Code blocks
|
||||
"li", "ol", "ul" # Lists
|
||||
"li", "ol", "ul", # Lists
|
||||
"sub", "sup" # Sub- and superscripts
|
||||
])
|
||||
|
||||
# Current context and section
|
||||
@ -362,7 +400,7 @@ class Parser(HTMLParser):
|
||||
else:
|
||||
return
|
||||
|
||||
# Handle headings
|
||||
# Handle heading
|
||||
if tag in ([f"h{x}" for x in range(1, 7)]):
|
||||
depth = len(self.context)
|
||||
if "id" in attrs:
|
||||
@ -507,23 +545,22 @@ class Parser(HTMLParser):
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Set up logging
|
||||
log = logging.getLogger("mkdocs")
|
||||
log.addFilter(DuplicateFilter())
|
||||
log = logging.getLogger("mkdocs.material.search")
|
||||
|
||||
# Tags that are self-closing
|
||||
void = set([
|
||||
"area", # Image map areas
|
||||
"base", # Document base
|
||||
"br", # Line breaks
|
||||
"col", # Table columns
|
||||
"embed", # External content
|
||||
"hr", # Horizontal rules
|
||||
"img", # Images
|
||||
"input", # Input fields
|
||||
"link", # Links
|
||||
"meta", # Metadata
|
||||
"param", # External parameters
|
||||
"source", # Image source sets
|
||||
"track", # Text track
|
||||
"wbr" # Line break opportunities
|
||||
"area", # Image map areas
|
||||
"base", # Document base
|
||||
"br", # Line breaks
|
||||
"col", # Table columns
|
||||
"embed", # External content
|
||||
"hr", # Horizontal rules
|
||||
"img", # Images
|
||||
"input", # Input fields
|
||||
"link", # Links
|
||||
"meta", # Metadata
|
||||
"param", # External parameters
|
||||
"source", # Image source sets
|
||||
"track", # Text track
|
||||
"wbr" # Line break opportunities
|
||||
])
|
||||
|
@ -0,0 +1,38 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Checks
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Check for pillow and cairosvg
|
||||
try:
|
||||
import cairosvg as _
|
||||
import PIL as _
|
||||
except ImportError:
|
||||
log = logging.getLogger("mkdocs.material.social")
|
||||
log.error(
|
||||
"Required dependencies of \"social\" plugin not found. "
|
||||
"Install with: pip install pillow cairosvg"
|
||||
)
|
||||
sys.exit(1)
|
48
material/plugins/social/config.py
Normal file
48
material/plugins/social/config.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
from mkdocs.config.base import Config
|
||||
from mkdocs.config.config_options import Deprecated, Type
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Social plugin configuration
|
||||
class SocialConfig(Config):
|
||||
enabled = Type(bool, default = True)
|
||||
cache_dir = Type(str, default = ".cache/plugin/social")
|
||||
|
||||
# Options for social cards
|
||||
cards = Type(bool, default = True)
|
||||
cards_dir = Type(str, default = "assets/images/social")
|
||||
cards_layout_options = Type(dict, default = {})
|
||||
|
||||
# Deprecated options
|
||||
cards_color = Deprecated(
|
||||
option_type = Type(dict, default = {}),
|
||||
message =
|
||||
"Deprecated, use 'cards_layout_options.background_color' "
|
||||
"and 'cards_layout_options.color' with 'default' layout"
|
||||
)
|
||||
cards_font = Deprecated(
|
||||
option_type = Type(str),
|
||||
message = "Deprecated, use 'cards_layout_options.font_family'"
|
||||
)
|
@ -27,54 +27,25 @@ import re
|
||||
import requests
|
||||
import sys
|
||||
|
||||
from cairosvg import svg2png
|
||||
from collections import defaultdict
|
||||
from hashlib import md5
|
||||
from io import BytesIO
|
||||
from mkdocs.commands.build import DuplicateFilter
|
||||
from mkdocs.config import config_options as opt
|
||||
from mkdocs.config.base import Config
|
||||
from mkdocs.plugins import BasePlugin
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from shutil import copyfile
|
||||
from tempfile import TemporaryFile
|
||||
from zipfile import ZipFile
|
||||
|
||||
try:
|
||||
from cairosvg import svg2png
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
dependencies = True
|
||||
except ImportError:
|
||||
dependencies = False
|
||||
from .config import SocialConfig
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Class
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Social plugin configuration scheme
|
||||
class SocialPluginConfig(Config):
|
||||
enabled = opt.Type(bool, default = True)
|
||||
cache_dir = opt.Type(str, default = ".cache/plugin/social")
|
||||
|
||||
# Options for social cards
|
||||
cards = opt.Type(bool, default = True)
|
||||
cards_dir = opt.Type(str, default = "assets/images/social")
|
||||
cards_layout_options = opt.Type(dict, default = {})
|
||||
|
||||
# Deprecated options
|
||||
cards_color = opt.Deprecated(
|
||||
option_type = opt.Type(dict, default = {}),
|
||||
message =
|
||||
"Deprecated, use 'cards_layout_options.background_color' "
|
||||
"and 'cards_layout_options.color' with 'default' layout"
|
||||
)
|
||||
cards_font = opt.Deprecated(
|
||||
option_type = opt.Type(str),
|
||||
message = "Deprecated, use 'cards_layout_options.font_family'"
|
||||
)
|
||||
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Social plugin
|
||||
class SocialPlugin(BasePlugin[SocialPluginConfig]):
|
||||
class SocialPlugin(BasePlugin[SocialConfig]):
|
||||
|
||||
def __init__(self):
|
||||
self._executor = concurrent.futures.ThreadPoolExecutor(4)
|
||||
@ -103,14 +74,6 @@ class SocialPlugin(BasePlugin[SocialPluginConfig]):
|
||||
value = self.config.cards_font
|
||||
self.config.cards_layout_options["font_family"] = value
|
||||
|
||||
# Check if required dependencies are installed
|
||||
if not dependencies:
|
||||
log.error(
|
||||
"Required dependencies of \"social\" plugin not found. "
|
||||
"Install with: pip install pillow cairosvg"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Check if site URL is defined
|
||||
if not config.site_url:
|
||||
log.warning(
|
||||
|
@ -0,0 +1,27 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Casefold a string for comparison when sorting
|
||||
def casefold(tag: str):
|
||||
return tag.casefold()
|
43
material/plugins/tags/config.py
Normal file
43
material/plugins/tags/config.py
Normal file
@ -0,0 +1,43 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
from functools import partial
|
||||
from markdown.extensions.toc import slugify
|
||||
from mkdocs.config.config_options import Optional, Type
|
||||
from mkdocs.config.base import Config
|
||||
|
||||
from . import casefold
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Tags plugin configuration
|
||||
class TagsConfig(Config):
|
||||
enabled = Type(bool, default = True)
|
||||
|
||||
# Options for tags
|
||||
tags_file = Optional(Type(str))
|
||||
tags_extra_files = Type(dict, default = dict())
|
||||
tags_slugify = Type((type(slugify), partial), default = slugify)
|
||||
tags_slugify_separator = Type(str, default = "-")
|
||||
tags_compare = Optional(Type(type(casefold)))
|
||||
tags_compare_reverse = Type(bool, default = False)
|
||||
tags_allowed = Type(list, default = [])
|
@ -24,26 +24,19 @@ import sys
|
||||
from collections import defaultdict
|
||||
from markdown.extensions.toc import slugify
|
||||
from mkdocs import utils
|
||||
from mkdocs.commands.build import DuplicateFilter
|
||||
from mkdocs.config.base import Config
|
||||
from mkdocs.config import config_options as opt
|
||||
from mkdocs.plugins import BasePlugin
|
||||
|
||||
# deprecated, but kept for downward compatibility. Use 'material.plugins.tags'
|
||||
# as an import source instead. This import is removed in the next major version.
|
||||
from . import casefold
|
||||
from .config import TagsConfig
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Class
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Tags plugin configuration scheme
|
||||
class TagsPluginConfig(Config):
|
||||
enabled = opt.Type(bool, default = True)
|
||||
|
||||
# Options for tags
|
||||
tags_file = opt.Optional(opt.Type(str))
|
||||
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Tags plugin
|
||||
class TagsPlugin(BasePlugin[TagsPluginConfig]):
|
||||
class TagsPlugin(BasePlugin[TagsConfig]):
|
||||
supports_multiple_instances = True
|
||||
|
||||
# Initialize plugin
|
||||
@ -83,6 +76,10 @@ class TagsPlugin(BasePlugin[TagsPluginConfig]):
|
||||
if not self.config.enabled:
|
||||
return
|
||||
|
||||
# Skip, if page is excluded
|
||||
if page.file.inclusion.is_excluded():
|
||||
return
|
||||
|
||||
# Render tags index page
|
||||
if page.file == self.tags_file:
|
||||
return self._render_tag_index(markdown)
|
||||
@ -166,5 +163,4 @@ class TagsPlugin(BasePlugin[TagsPluginConfig]):
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Set up logging
|
||||
log = logging.getLogger("mkdocs")
|
||||
log.addFilter(DuplicateFilter())
|
||||
log = logging.getLogger("mkdocs.material.tags")
|
||||
|
@ -84,6 +84,7 @@ theme:
|
||||
|
||||
# Plugins
|
||||
plugins:
|
||||
- blog
|
||||
- search:
|
||||
separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
|
||||
- minify:
|
||||
@ -231,10 +232,3 @@ nav:
|
||||
- Changelog: insiders/changelog.md
|
||||
- Blog:
|
||||
- blog/index.md
|
||||
- 2022:
|
||||
- blog/posts/blog-support-just-landed.md
|
||||
- blog/posts/chinese-search-support.md
|
||||
- 2021:
|
||||
- blog/posts/the-past-present-and-future.md
|
||||
- blog/posts/excluding-content-from-search.md
|
||||
- blog/posts/search-better-faster-smaller.md
|
||||
|
67
package-lock.json
generated
67
package-lock.json
generated
@ -1,26 +1,21 @@
|
||||
{
|
||||
"name": "mkdocs-material",
|
||||
"version": "9.1.21",
|
||||
"version": "9.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mkdocs-material",
|
||||
"version": "9.1.21",
|
||||
"version": "9.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-flat-polyfill": "^1.0.1",
|
||||
"clipboard": "^2.0.11",
|
||||
"escape-html": "^1.0.3",
|
||||
"focus-visible": "^5.2.0",
|
||||
"fuzzaldrin-plus": "^0.6.0",
|
||||
"iframe-worker": "^1.0.3",
|
||||
"lunr": "^2.3.9",
|
||||
"lunr-languages": "^1.13.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"unfetch": "^5.0.0",
|
||||
"url-polyfill": "^1.1.12"
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
@ -1647,14 +1642,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flat-polyfill": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flat-polyfill/-/array-flat-polyfill-1.0.1.tgz",
|
||||
"integrity": "sha512-hfJmKupmQN0lwi0xG6FQ5U8Rd97RnIERplymOv/qpq8AoNKPPAnxJadjFA23FNWm88wykh9HmpLJUUwUtNU/iw==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/array-includes": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
|
||||
@ -4758,14 +4745,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iframe-worker": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/iframe-worker/-/iframe-worker-1.0.3.tgz",
|
||||
"integrity": "sha512-bzOpLcy1R4vBFGsx2fQ4iMI0ikyUWwt9YKjp6/8KZcpk+FSvZavxWD0gA6hqKksjdsn3u59E2TT7qJlTq4Gn4A==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||
@ -8385,11 +8364,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.3",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.3.tgz",
|
||||
@ -9894,11 +9868,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/unfetch": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-5.0.0.tgz",
|
||||
"integrity": "sha512-3xM2c89siXg0nHvlmYsQ2zkLASvVMBisZm5lF3gFDqfF2xonNStDJyMpvaOBe0a1Edxmqrf2E0HBdmy9QyZaeg=="
|
||||
},
|
||||
"node_modules/unique-filename": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz",
|
||||
@ -10032,11 +10001,6 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-polyfill": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.12.tgz",
|
||||
"integrity": "sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A=="
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@ -11454,11 +11418,6 @@
|
||||
"is-array-buffer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"array-flat-polyfill": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flat-polyfill/-/array-flat-polyfill-1.0.1.tgz",
|
||||
"integrity": "sha512-hfJmKupmQN0lwi0xG6FQ5U8Rd97RnIERplymOv/qpq8AoNKPPAnxJadjFA23FNWm88wykh9HmpLJUUwUtNU/iw=="
|
||||
},
|
||||
"array-includes": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
|
||||
@ -13708,11 +13667,6 @@
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
}
|
||||
},
|
||||
"iframe-worker": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/iframe-worker/-/iframe-worker-1.0.3.tgz",
|
||||
"integrity": "sha512-bzOpLcy1R4vBFGsx2fQ4iMI0ikyUWwt9YKjp6/8KZcpk+FSvZavxWD0gA6hqKksjdsn3u59E2TT7qJlTq4Gn4A=="
|
||||
},
|
||||
"ignore": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||
@ -16304,11 +16258,6 @@
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true
|
||||
},
|
||||
"resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.22.3",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.3.tgz",
|
||||
@ -17401,11 +17350,6 @@
|
||||
"which-boxed-primitive": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"unfetch": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-5.0.0.tgz",
|
||||
"integrity": "sha512-3xM2c89siXg0nHvlmYsQ2zkLASvVMBisZm5lF3gFDqfF2xonNStDJyMpvaOBe0a1Edxmqrf2E0HBdmy9QyZaeg=="
|
||||
},
|
||||
"unique-filename": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz",
|
||||
@ -17494,11 +17438,6 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"url-polyfill": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.12.tgz",
|
||||
"integrity": "sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A=="
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mkdocs-material",
|
||||
"version": "9.1.21",
|
||||
"version": "9.2.0",
|
||||
"description": "Documentation that simply works",
|
||||
"keywords": [
|
||||
"mkdocs",
|
||||
@ -39,18 +39,13 @@
|
||||
"upgrade:install": "npm install"
|
||||
},
|
||||
"dependencies": {
|
||||
"array-flat-polyfill": "^1.0.1",
|
||||
"clipboard": "^2.0.11",
|
||||
"escape-html": "^1.0.3",
|
||||
"focus-visible": "^5.2.0",
|
||||
"fuzzaldrin-plus": "^0.6.0",
|
||||
"iframe-worker": "^1.0.3",
|
||||
"lunr": "^2.3.9",
|
||||
"lunr-languages": "^1.13.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"unfetch": "^5.0.0",
|
||||
"url-polyfill": "^1.1.12"
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
|
@ -57,6 +57,7 @@ Issues = "https://github.com/squidfunk/mkdocs-material/issues"
|
||||
Funding = "https://github.com/sponsors/squidfunk"
|
||||
|
||||
[project.entry-points."mkdocs.plugins"]
|
||||
"material/blog" = "material.plugins.blog.plugin:BlogPlugin"
|
||||
"material/info" = "material.plugins.info.plugin:InfoPlugin"
|
||||
"material/offline" = "material.plugins.offline.plugin:OfflinePlugin"
|
||||
"material/search" = "material.plugins.search.plugin:SearchPlugin"
|
||||
|
@ -21,12 +21,16 @@
|
||||
# Requirements for core
|
||||
jinja2>=3.0
|
||||
markdown>=3.2
|
||||
mkdocs>=1.5.0
|
||||
mkdocs>=1.5.2
|
||||
mkdocs-material-extensions>=1.1
|
||||
pygments>=2.14
|
||||
pymdown-extensions>=9.9.1
|
||||
|
||||
# Requirements for plugins
|
||||
babel>=2.10.3
|
||||
colorama>=0.4
|
||||
lxml>=4.6
|
||||
paginate>=0.5.6
|
||||
readtime>=2.0
|
||||
regex>=2022.4.24
|
||||
requests>=2.26
|
||||
|
@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
@ -80,7 +80,7 @@ export type Translations =
|
||||
*/
|
||||
export interface Versioning {
|
||||
provider: "mike" /* Version provider */
|
||||
default?: string /* Default version */
|
||||
default?: string | string[] /* Default version */
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,11 +27,38 @@ import {
|
||||
fromEvent,
|
||||
map,
|
||||
merge,
|
||||
shareReplay,
|
||||
startWith
|
||||
} from "rxjs"
|
||||
|
||||
import { getActiveElement } from "../_"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Data
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Focus observable
|
||||
*
|
||||
* Previously, this observer used `focus` and `blur` events to determine whether
|
||||
* an element is focused, but this doesn't work if there are focusable elements
|
||||
* within the elements itself. A better solutions are `focusin` and `focusout`
|
||||
* events, which bubble up the tree and allow for more fine-grained control.
|
||||
*
|
||||
* `debounceTime` is necessary, because when a focus change happens inside an
|
||||
* element, the observable would first emit `false` and then `true` again.
|
||||
*/
|
||||
const observer$ = merge(
|
||||
fromEvent(document.body, "focusin"),
|
||||
fromEvent(document.body, "focusout")
|
||||
)
|
||||
.pipe(
|
||||
debounceTime(1),
|
||||
startWith(undefined),
|
||||
map(() => getActiveElement() || document.body),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
@ -39,14 +66,6 @@ import { getActiveElement } from "../_"
|
||||
/**
|
||||
* Watch element focus
|
||||
*
|
||||
* Previously, this function used `focus` and `blur` events to determine whether
|
||||
* an element is focused, but this doesn't work if there are focusable elements
|
||||
* within the elements itself. A better solutions are `focusin` and `focusout`
|
||||
* events, which bubble up the tree and allow for more fine-grained control.
|
||||
*
|
||||
* `debounceTime` is necessary, because when a focus change happens inside an
|
||||
* element, the observable would first emit `false` and then `true` again.
|
||||
*
|
||||
* @param el - Element
|
||||
*
|
||||
* @returns Element focus observable
|
||||
@ -54,19 +73,9 @@ import { getActiveElement } from "../_"
|
||||
export function watchElementFocus(
|
||||
el: HTMLElement
|
||||
): Observable<boolean> {
|
||||
return merge(
|
||||
fromEvent(document.body, "focusin"),
|
||||
fromEvent(document.body, "focusout")
|
||||
)
|
||||
return observer$
|
||||
.pipe(
|
||||
debounceTime(1),
|
||||
map(() => {
|
||||
const active = getActiveElement()
|
||||
return typeof active !== "undefined"
|
||||
? el.contains(active)
|
||||
: false
|
||||
}),
|
||||
startWith(el === getActiveElement()),
|
||||
map(active => el.contains(active)),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
}
|
||||
|
@ -20,7 +20,6 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import ResizeObserver from "resize-observer-polyfill"
|
||||
import {
|
||||
NEVER,
|
||||
Observable,
|
||||
@ -37,6 +36,8 @@ import {
|
||||
tap
|
||||
} from "rxjs"
|
||||
|
||||
import { watchScript } from "../../../script"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
@ -66,15 +67,23 @@ const entry$ = new Subject<ResizeObserverEntry>()
|
||||
* It's quite important to centralize observation in a single `ResizeObserver`,
|
||||
* as the performance difference can be quite dramatic, as the link shows.
|
||||
*
|
||||
* If the browser doesn't have a `ResizeObserver` implementation available, a
|
||||
* polyfill is automatically downloaded from unpkg.com. This is also compatible
|
||||
* with the built-in privacy plugin, which will download the polyfill and put
|
||||
* it alongside the built site for self-hosting.
|
||||
*
|
||||
* @see https://bit.ly/3iIYfEm - Google Groups on performance
|
||||
*/
|
||||
const observer$ = defer(() => of(
|
||||
new ResizeObserver(entries => {
|
||||
for (const entry of entries)
|
||||
entry$.next(entry)
|
||||
})
|
||||
const observer$ = defer(() => (
|
||||
typeof ResizeObserver === "undefined"
|
||||
? watchScript("https://unpkg.com/resize-observer-polyfill")
|
||||
: of(undefined)
|
||||
))
|
||||
.pipe(
|
||||
map(() => new ResizeObserver(entries => {
|
||||
for (const entry of entries)
|
||||
entry$.next(entry)
|
||||
})),
|
||||
switchMap(observer => merge(NEVER, of(observer))
|
||||
.pipe(
|
||||
finalize(() => observer.disconnect())
|
||||
|
@ -43,7 +43,7 @@ import { h } from "~/utilities"
|
||||
* @returns Location hash
|
||||
*/
|
||||
export function getLocationHash(): string {
|
||||
return location.hash.substring(1)
|
||||
return location.hash.slice(1)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -20,7 +20,6 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "iframe-worker/shim"
|
||||
import {
|
||||
Observable,
|
||||
Subject,
|
||||
|
@ -20,10 +20,7 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "array-flat-polyfill"
|
||||
import "focus-visible"
|
||||
import "unfetch/polyfill"
|
||||
import "url-polyfill"
|
||||
|
||||
import {
|
||||
EMPTY,
|
||||
|
@ -25,17 +25,22 @@ import { Observable, merge } from "rxjs"
|
||||
import { Viewport, getElements } from "~/browser"
|
||||
|
||||
import { Component } from "../../_"
|
||||
import { Annotation } from "../annotation"
|
||||
import {
|
||||
Annotation,
|
||||
mountAnnotationBlock
|
||||
} from "../annotation"
|
||||
import {
|
||||
CodeBlock,
|
||||
Mermaid,
|
||||
mountCodeBlock,
|
||||
mountMermaid
|
||||
mountCodeBlock
|
||||
} from "../code"
|
||||
import {
|
||||
Details,
|
||||
mountDetails
|
||||
} from "../details"
|
||||
import {
|
||||
Mermaid,
|
||||
mountMermaid
|
||||
} from "../mermaid"
|
||||
import {
|
||||
DataTable,
|
||||
mountDataTable
|
||||
@ -54,11 +59,11 @@ import {
|
||||
*/
|
||||
export type Content =
|
||||
| Annotation
|
||||
| ContentTabs
|
||||
| CodeBlock
|
||||
| Mermaid
|
||||
| ContentTabs
|
||||
| DataTable
|
||||
| Details
|
||||
| Mermaid
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
@ -93,6 +98,10 @@ export function mountContent(
|
||||
): Observable<Component<Content>> {
|
||||
return merge(
|
||||
|
||||
/* Annotations */
|
||||
...getElements(".annotate:not(.highlight)", el)
|
||||
.map(child => mountAnnotationBlock(child, { target$, print$ })),
|
||||
|
||||
/* Code blocks */
|
||||
...getElements("pre:not(.mermaid) > code", el)
|
||||
.map(child => mountCodeBlock(child, { target$, print$ })),
|
||||
|
@ -102,7 +102,7 @@ export function watchAnnotation(
|
||||
map(([{ x, y }, scroll]): ElementOffset => {
|
||||
const { width, height } = getElementSize(el)
|
||||
return ({
|
||||
x: x - scroll.x + width / 2,
|
||||
x: x - scroll.x + width / 2,
|
||||
y: y - scroll.y + height / 2
|
||||
})
|
||||
})
|
||||
|
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { EMPTY, Observable, defer } from "rxjs"
|
||||
|
||||
import { Component } from "../../../_"
|
||||
import { Annotation } from "../_"
|
||||
import { mountAnnotationList } from "../list"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount options
|
||||
*/
|
||||
interface MountOptions {
|
||||
target$: Observable<HTMLElement> /* Location target observable */
|
||||
print$: Observable<boolean> /* Media print observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Find list element directly following a block
|
||||
*
|
||||
* @param el - Annotation block element
|
||||
*
|
||||
* @returns List element or nothing
|
||||
*/
|
||||
function findList(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 findList(sibling)
|
||||
}
|
||||
|
||||
/* Everything else */
|
||||
return undefined
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount annotation block
|
||||
*
|
||||
* @param el - Annotation block element
|
||||
* @param options - Options
|
||||
*
|
||||
* @returns Annotation component observable
|
||||
*/
|
||||
export function mountAnnotationBlock(
|
||||
el: HTMLElement, options: MountOptions
|
||||
): Observable<Component<Annotation>> {
|
||||
return defer(() => {
|
||||
const list = findList(el)
|
||||
return typeof list !== "undefined"
|
||||
? mountAnnotationList(list, el, options)
|
||||
: EMPTY
|
||||
})
|
||||
}
|
@ -21,4 +21,5 @@
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./block"
|
||||
export * from "./list"
|
||||
|
@ -63,15 +63,28 @@ interface MountOptions {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Find all annotation markers in the given code block
|
||||
* Find all annotation hosts in the containing element
|
||||
*
|
||||
* @param container - Containing element
|
||||
*
|
||||
* @returns Annotation hosts
|
||||
*/
|
||||
function findHosts(container: HTMLElement): HTMLElement[] {
|
||||
return container.tagName === "CODE"
|
||||
? getElements(".c, .c1, .cm", container)
|
||||
: [container]
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all annotation markers in the containing element
|
||||
*
|
||||
* @param container - Containing element
|
||||
*
|
||||
* @returns Annotation markers
|
||||
*/
|
||||
function findAnnotationMarkers(container: HTMLElement): Text[] {
|
||||
function findMarkers(container: HTMLElement): Text[] {
|
||||
const markers: Text[] = []
|
||||
for (const el of getElements(".c, .c1, .cm", container)) {
|
||||
for (const el of findHosts(container)) {
|
||||
const nodes: Text[] = []
|
||||
|
||||
/* Find all text nodes in current element */
|
||||
@ -141,7 +154,7 @@ export function mountAnnotationList(
|
||||
|
||||
/* Find and replace all markers with empty annotations */
|
||||
const annotations = new Map<string, HTMLElement>()
|
||||
for (const marker of findAnnotationMarkers(container)) {
|
||||
for (const marker of findMarkers(container)) {
|
||||
const [, id] = marker.textContent!.match(/\((\d+)\)/)!
|
||||
if (getOptionalElement(`:scope > li:nth-child(${id})`, el)) {
|
||||
annotations.set(id, renderAnnotation(id, prefix))
|
||||
@ -155,7 +168,8 @@ export function mountAnnotationList(
|
||||
|
||||
/* Mount component on subscription */
|
||||
return defer(() => {
|
||||
const done$ = new Subject()
|
||||
const push$ = new Subject()
|
||||
const done$ = push$.pipe(ignoreElements(), endWith(true))
|
||||
|
||||
/* Retrieve container pairs for swapping */
|
||||
const pairs: [HTMLElement, HTMLElement][] = []
|
||||
@ -166,20 +180,20 @@ export function mountAnnotationList(
|
||||
])
|
||||
|
||||
/* Handle print mode - see https://bit.ly/3rgPdpt */
|
||||
print$
|
||||
.pipe(
|
||||
takeUntil(done$.pipe(ignoreElements(), endWith(true)))
|
||||
)
|
||||
.subscribe(active => {
|
||||
el.hidden = !active
|
||||
print$.pipe(takeUntil(done$))
|
||||
.subscribe(active => {
|
||||
el.hidden = !active
|
||||
|
||||
/* Show annotations in code block or list (print) */
|
||||
for (const [inner, child] of pairs)
|
||||
if (!active)
|
||||
swap(child, inner)
|
||||
else
|
||||
swap(inner, child)
|
||||
})
|
||||
/* Add class to discern list element */
|
||||
el.classList.toggle("md-annotation-list", active)
|
||||
|
||||
/* Show annotations in code block or list (print) */
|
||||
for (const [inner, child] of pairs)
|
||||
if (!active)
|
||||
swap(child, inner)
|
||||
else
|
||||
swap(inner, child)
|
||||
})
|
||||
|
||||
/* Create and return component */
|
||||
return merge(...[...annotations]
|
||||
@ -188,7 +202,7 @@ export function mountAnnotationList(
|
||||
))
|
||||
)
|
||||
.pipe(
|
||||
finalize(() => done$.complete()),
|
||||
finalize(() => push$.complete()),
|
||||
share()
|
||||
)
|
||||
})
|
||||
|
@ -21,4 +21,3 @@
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./mermaid"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user